pingap_core/
http_header.rs

1// Copyright 2024-2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use super::{get_hostname, Ctx};
16use bytes::BytesMut;
17use http::header;
18use http::{HeaderName, HeaderValue};
19use once_cell::sync::Lazy;
20use pingora::http::RequestHeader;
21use pingora::proxy::Session;
22use snafu::{ResultExt, Snafu};
23use std::collections::HashMap;
24use std::str::FromStr;
25use url::Url;
26use urlencoding::encode;
27
28pub static HTTP_HEADER_X_FORWARDED_FOR: Lazy<http::HeaderName> =
29    Lazy::new(|| HeaderName::from_str("X-Forwarded-For").unwrap());
30
31pub static HTTP_HEADER_X_REAL_IP: Lazy<http::HeaderName> =
32    Lazy::new(|| HeaderName::from_str("X-Real-Ip").unwrap());
33
34pub const HOST_NAME_TAG: &[u8] = b"$hostname";
35const HOST_TAG: &[u8] = b"$host";
36const SCHEME_TAG: &[u8] = b"$scheme";
37const REMOTE_ADDR_TAG: &[u8] = b"$remote_addr";
38const REMOTE_PORT_TAG: &[u8] = b"$remote_port";
39const SERVER_ADDR_TAG: &[u8] = b"$server_addr";
40const SERVER_PORT_TAG: &[u8] = b"$server_port";
41const PROXY_ADD_FORWARDED_TAG: &[u8] = b"$proxy_add_x_forwarded_for";
42const UPSTREAM_ADDR_TAG: &[u8] = b"$upstream_addr";
43
44static SCHEME_HTTPS: HeaderValue = HeaderValue::from_static("https");
45static SCHEME_HTTP: HeaderValue = HeaderValue::from_static("http");
46
47#[derive(Debug, Snafu)]
48pub enum Error {
49    #[snafu(display("Invalid header value: {value} - {source}"))]
50    InvalidHeaderValue {
51        value: String,
52        source: header::InvalidHeaderValue,
53    },
54    #[snafu(display("Invalid header name: {value} - {source}"))]
55    InvalidHeaderName {
56        value: String,
57        source: header::InvalidHeaderName,
58    },
59}
60type Result<T, E = Error> = std::result::Result<T, E>;
61
62pub type HttpHeader = (HeaderName, HeaderValue);
63
64/// Get request host in this order of precedence:
65/// host name from the request line,
66/// or host name from the "Host" request header field
67pub fn get_host(header: &RequestHeader) -> Option<&str> {
68    if let Some(host) = header.uri.host() {
69        return Some(host);
70    }
71    if let Some(host) = header.headers.get(http::header::HOST) {
72        if let Ok(value) = host.to_str().map(|host| host.split(':').next()) {
73            return value;
74        }
75    }
76    None
77}
78
79/// Converts a string in "name: value" format into an HTTP header tuple.
80/// Returns None if the input string doesn't contain a colon separator.
81///
82/// # Arguments
83/// * `value` - A string in the format "header_name: header_value"
84///
85/// # Returns
86/// * `Result<Option<HttpHeader>>` - The parsed header tuple or None if invalid format
87pub fn convert_header(value: &str) -> Result<Option<HttpHeader>> {
88    value
89        .split_once(':')
90        .map(|(k, v)| {
91            let name = HeaderName::from_str(k.trim())
92                .context(InvalidHeaderNameSnafu { value: k })?;
93            let value = HeaderValue::from_str(v.trim())
94                .context(InvalidHeaderValueSnafu { value: v })?;
95            Ok(Some((name, value)))
96        })
97        .unwrap_or(Ok(None))
98}
99
100/// Converts a slice of strings into HTTP headers.
101/// Each string should be in "name: value" format.
102///
103/// # Arguments
104/// * `header_values` - Slice of strings representing headers
105///
106/// # Returns
107/// * `Result<Vec<HttpHeader>>` - Vector of parsed HTTP headers
108pub fn convert_headers(header_values: &[String]) -> Result<Vec<HttpHeader>> {
109    let mut arr = vec![];
110    for item in header_values {
111        if let Some(item) = convert_header(item)? {
112            arr.push(item);
113        }
114    }
115    Ok(arr)
116}
117
118pub static HTTP_HEADER_NO_STORE: Lazy<HttpHeader> = Lazy::new(|| {
119    (
120        header::CACHE_CONTROL,
121        HeaderValue::from_str("private, no-store").unwrap(),
122    )
123});
124
125pub static HTTP_HEADER_NO_CACHE: Lazy<HttpHeader> = Lazy::new(|| {
126    (
127        header::CACHE_CONTROL,
128        HeaderValue::from_str("private, no-cache").unwrap(),
129    )
130});
131
132pub static HTTP_HEADER_CONTENT_JSON: Lazy<HttpHeader> = Lazy::new(|| {
133    (
134        header::CONTENT_TYPE,
135        HeaderValue::from_str("application/json; charset=utf-8").unwrap(),
136    )
137});
138
139pub static HTTP_HEADER_CONTENT_HTML: Lazy<HttpHeader> = Lazy::new(|| {
140    (
141        header::CONTENT_TYPE,
142        HeaderValue::from_str("text/html; charset=utf-8").unwrap(),
143    )
144});
145
146pub static HTTP_HEADER_CONTENT_TEXT: Lazy<HttpHeader> = Lazy::new(|| {
147    (
148        header::CONTENT_TYPE,
149        HeaderValue::from_str("text/plain; charset=utf-8").unwrap(),
150    )
151});
152
153pub static HTTP_HEADER_TRANSFER_CHUNKED: Lazy<HttpHeader> = Lazy::new(|| {
154    (
155        header::TRANSFER_ENCODING,
156        HeaderValue::from_str("chunked").unwrap(),
157    )
158});
159
160pub static HTTP_HEADER_NAME_X_REQUEST_ID: Lazy<HeaderName> =
161    Lazy::new(|| HeaderName::from_str("X-Request-Id").unwrap());
162
163/// Processes special header values that contain dynamic variables.
164/// Supports variables like $host, $scheme, $remote_addr etc.
165///
166/// # Arguments
167/// * `value` - The header value to process
168/// * `session` - The HTTP session context
169/// * `ctx` - The application state
170///
171/// # Returns
172/// * `Option<HeaderValue>` - The processed header value or None if no special handling needed
173#[inline]
174pub fn convert_header_value(
175    value: &HeaderValue,
176    session: &Session,
177    ctx: &Ctx,
178) -> Option<HeaderValue> {
179    let buf = value.as_bytes();
180
181    // Early return if not a special header (moved this check earlier)
182    if buf.is_empty() || !(buf[0] == b'$' || buf[0] == b':') {
183        return None;
184    }
185
186    // Helper closure to convert string to HeaderValue
187    let to_header_value = |s: &str| HeaderValue::from_str(s).ok();
188
189    match buf {
190        HOST_TAG => get_host(session.req_header()).and_then(to_header_value),
191        SCHEME_TAG => Some(if ctx.tls_version.is_some() {
192            SCHEME_HTTPS.clone()
193        } else {
194            SCHEME_HTTP.clone()
195        }),
196        HOST_NAME_TAG => to_header_value(get_hostname()),
197        REMOTE_ADDR_TAG => ctx.remote_addr.as_deref().and_then(to_header_value),
198        REMOTE_PORT_TAG => ctx
199            .remote_port
200            .map(|p| p.to_string())
201            .and_then(|s| to_header_value(&s)),
202        SERVER_ADDR_TAG => ctx.server_addr.as_deref().and_then(to_header_value),
203        SERVER_PORT_TAG => ctx
204            .server_port
205            .map(|p| p.to_string())
206            .and_then(|s| to_header_value(&s)),
207        UPSTREAM_ADDR_TAG => {
208            if !ctx.upstream_address.is_empty() {
209                to_header_value(&ctx.upstream_address)
210            } else {
211                None
212            }
213        },
214        PROXY_ADD_FORWARDED_TAG => {
215            ctx.remote_addr.as_deref().and_then(|remote_addr| {
216                let value = match session
217                    .get_header(HTTP_HEADER_X_FORWARDED_FOR.clone())
218                {
219                    Some(existing) => format!(
220                        "{}, {}",
221                        existing.to_str().unwrap_or_default(),
222                        remote_addr
223                    ),
224                    None => remote_addr.to_string(),
225                };
226                to_header_value(&value)
227            })
228        },
229        _ => handle_special_headers(buf, session, ctx),
230    }
231}
232
233const HTTP_HEADER_PREFIX: &[u8] = b"$http_";
234const HTTP_HEADER_PREFIX_LEN: usize = HTTP_HEADER_PREFIX.len();
235
236#[inline]
237fn handle_special_headers(
238    buf: &[u8],
239    session: &Session,
240    ctx: &Ctx,
241) -> Option<HeaderValue> {
242    // Handle headers that reference other HTTP headers (e.g., $http_origin)
243    if buf.starts_with(HTTP_HEADER_PREFIX) {
244        return handle_http_header(buf, session);
245    }
246    // Handle environment variable references (e.g., $HOME)
247    if buf.starts_with(b"$") {
248        return handle_env_var(buf);
249    }
250    // Handle context value references (e.g., :connection_id)
251    if buf.starts_with(b":") {
252        return handle_context_value(buf, ctx);
253    }
254    None
255}
256
257#[inline]
258fn handle_http_header(buf: &[u8], session: &Session) -> Option<HeaderValue> {
259    // Skip the "$http_" prefix (6 bytes) and convert remaining bytes to header key
260    let key = std::str::from_utf8(&buf[HTTP_HEADER_PREFIX_LEN..]).ok()?;
261    // Look up and clone the header value from the session
262    session.get_header(key).cloned()
263}
264
265#[inline]
266fn handle_env_var(buf: &[u8]) -> Option<HeaderValue> {
267    // Skip the "$" prefix and convert to environment variable name
268    let var_name = std::str::from_utf8(&buf[1..]).ok()?;
269    // Look up environment variable and convert to HeaderValue if found
270    std::env::var(var_name)
271        .ok()
272        .and_then(|v| HeaderValue::from_str(&v).ok())
273}
274
275#[inline]
276fn handle_context_value(buf: &[u8], ctx: &Ctx) -> Option<HeaderValue> {
277    // Skip the ":" prefix and convert to context key
278    let key = std::str::from_utf8(&buf[1..]).ok()?;
279    // Pre-allocate buffer for value
280    let mut value = BytesMut::with_capacity(20);
281    // Append context value to buffer
282    value = ctx.append_value(value, key);
283    // Convert to HeaderValue if buffer is not empty
284    if !value.is_empty() {
285        HeaderValue::from_bytes(&value).ok()
286    } else {
287        None
288    }
289}
290
291/// Get remote addr from session
292pub fn get_remote_addr(session: &Session) -> Option<(String, u16)> {
293    session
294        .client_addr()
295        .and_then(|addr| addr.as_inet())
296        .map(|addr| (addr.ip().to_string(), addr.port()))
297}
298
299/// Gets client ip from X-Forwarded-For,
300/// If none, get from X-Real-Ip,
301/// If none, get remote addr.
302pub fn get_client_ip(session: &Session) -> String {
303    if let Some(value) = session.get_header(HTTP_HEADER_X_FORWARDED_FOR.clone())
304    {
305        let arr: Vec<&str> =
306            value.to_str().unwrap_or_default().split(',').collect();
307        if !arr.is_empty() {
308            return arr[0].trim().to_string();
309        }
310    }
311    if let Some(value) = session.get_header(HTTP_HEADER_X_REAL_IP.clone()) {
312        return value.to_str().unwrap_or_default().to_string();
313    }
314    if let Some((addr, _)) = get_remote_addr(session) {
315        return addr;
316    }
317    "".to_string()
318}
319
320/// Gets string value from req header.
321///
322/// # Arguments
323/// * `req_header` - The HTTP request header
324/// * `key` - The header key to look up
325///
326/// # Returns
327/// The header value as a string slice if found and valid UTF-8, None otherwise
328pub fn get_req_header_value<'a>(
329    req_header: &'a RequestHeader,
330    key: &str,
331) -> Option<&'a str> {
332    if let Some(value) = req_header.headers.get(key) {
333        if let Ok(value) = value.to_str() {
334            return Some(value);
335        }
336    }
337    None
338}
339
340/// Gets cookie value from req header.
341///
342/// # Arguments
343/// * `req_header` - The HTTP request header
344/// * `cookie_name` - Name of the cookie to find
345///
346/// # Returns
347/// The cookie value as a string slice if found, None otherwise
348pub fn get_cookie_value<'a>(
349    req_header: &'a RequestHeader,
350    cookie_name: &str,
351) -> Option<&'a str> {
352    if let Some(cookie_value) = get_req_header_value(req_header, "Cookie") {
353        for item in cookie_value.split(';') {
354            if let Some((k, v)) = item.split_once('=') {
355                if k == cookie_name {
356                    return Some(v.trim());
357                }
358            }
359        }
360    }
361    None
362}
363
364/// Converts query string to key-value map.
365///
366/// # Arguments
367/// * `value` - Query string or http url to parse (without leading '?')
368///
369/// # Returns
370/// HashMap containing the parsed query parameters
371pub fn convert_query_map(value: &str) -> HashMap<String, String> {
372    let mut m = HashMap::new();
373    let value = if !value.contains('?') {
374        format!("http://host?{value}")
375    } else {
376        value.to_string()
377    };
378    let Ok(value) = Url::parse(&value) else {
379        return m;
380    };
381    for item in value.query().unwrap_or_default().split('&') {
382        if let Some((key, value)) = item.split_once('=') {
383            m.insert(key.to_string(), encode(value).to_string());
384        } else {
385            m.insert(item.to_string(), "".to_string());
386        }
387    }
388    m
389}
390
391/// Gets query parameter value from request header.
392///
393/// # Arguments
394/// * `req_header` - The HTTP request header
395/// * `name` - Name of the query parameter to find
396///
397/// # Returns
398/// The parameter value as a string slice if found, None otherwise
399pub fn get_query_value<'a>(
400    req_header: &'a RequestHeader,
401    name: &str,
402) -> Option<&'a str> {
403    if let Some(query) = req_header.uri.query() {
404        for item in query.split('&') {
405            if let Some((k, v)) = item.split_once('=') {
406                if k == name {
407                    return Some(v.trim());
408                }
409            }
410        }
411    }
412    None
413}
414
415/// Remove query parameter from request header URI
416///
417/// # Arguments
418/// * `req_header` - The HTTP request header to modify
419/// * `name` - Name of the query parameter to remove
420///
421/// # Returns
422/// Result indicating success or failure of the URI modification
423pub fn remove_query_from_header(
424    req_header: &mut RequestHeader,
425    name: &str,
426) -> Result<(), http::uri::InvalidUri> {
427    if let Some(query) = req_header.uri.query() {
428        let mut query_list = vec![];
429        for item in query.split('&') {
430            if let Some((k, v)) = item.split_once('=') {
431                if k != name {
432                    query_list.push(format!("{k}={v}"));
433                }
434            } else if item != name {
435                query_list.push(item.to_string());
436            }
437        }
438        let query = query_list.join("&");
439        let mut new_path = req_header.uri.path().to_string();
440        if !query.is_empty() {
441            new_path = format!("{new_path}?{query}");
442        }
443        return new_path
444            .parse::<http::Uri>()
445            .map(|uri| req_header.set_uri(uri));
446    }
447
448    Ok(())
449}
450
451#[cfg(test)]
452mod tests {
453    use super::*;
454    use pretty_assertions::assert_eq;
455    use tokio_test::io::Builder;
456
457    #[test]
458    fn test_convert_headers() {
459        let headers = convert_headers(&[
460            "Content-Type: application/octet-stream".to_string(),
461            "X-Server: $hostname".to_string(),
462            "X-User: $USER".to_string(),
463        ])
464        .unwrap();
465        assert_eq!(3, headers.len());
466        assert_eq!("content-type", headers[0].0.to_string());
467        assert_eq!("application/octet-stream", headers[0].1.to_str().unwrap());
468        assert_eq!("x-server", headers[1].0.to_string());
469        assert_eq!(false, headers[1].1.to_str().unwrap().is_empty());
470        assert_eq!("x-user", headers[2].0.to_string());
471        assert_eq!(false, headers[2].1.to_str().unwrap().is_empty());
472    }
473
474    #[test]
475    fn test_static_value() {
476        assert_eq!(
477            "cache-control: private, no-store",
478            format!(
479                "{}: {}",
480                HTTP_HEADER_NO_STORE.0.to_string(),
481                HTTP_HEADER_NO_STORE.1.to_str().unwrap_or_default()
482            )
483        );
484
485        assert_eq!(
486            "cache-control: private, no-cache",
487            format!(
488                "{}: {}",
489                HTTP_HEADER_NO_CACHE.0.to_string(),
490                HTTP_HEADER_NO_CACHE.1.to_str().unwrap_or_default()
491            )
492        );
493
494        assert_eq!(
495            "content-type: application/json; charset=utf-8",
496            format!(
497                "{}: {}",
498                HTTP_HEADER_CONTENT_JSON.0.to_string(),
499                HTTP_HEADER_CONTENT_JSON.1.to_str().unwrap_or_default()
500            )
501        );
502
503        assert_eq!(
504            "content-type: text/html; charset=utf-8",
505            format!(
506                "{}: {}",
507                HTTP_HEADER_CONTENT_HTML.0.to_string(),
508                HTTP_HEADER_CONTENT_HTML.1.to_str().unwrap_or_default()
509            )
510        );
511
512        assert_eq!(
513            "transfer-encoding: chunked",
514            format!(
515                "{}: {}",
516                HTTP_HEADER_TRANSFER_CHUNKED.0.to_string(),
517                HTTP_HEADER_TRANSFER_CHUNKED.1.to_str().unwrap_or_default()
518            )
519        );
520
521        assert_eq!(
522            "x-request-id",
523            format!("{}", HTTP_HEADER_NAME_X_REQUEST_ID.to_string(),)
524        );
525
526        assert_eq!(
527            "content-type: text/plain; charset=utf-8",
528            format!(
529                "{}: {}",
530                HTTP_HEADER_CONTENT_TEXT.0.to_string(),
531                HTTP_HEADER_CONTENT_TEXT.1.to_str().unwrap_or_default()
532            )
533        );
534    }
535
536    #[tokio::test]
537    async fn test_convert_header_value() {
538        let headers = ["Host: pingap.io"].join("\r\n");
539        let input_header =
540            format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
541        let mock_io = Builder::new().read(input_header.as_bytes()).build();
542        let mut session = Session::new_h1(Box::new(mock_io));
543        session.read_request().await.unwrap();
544        let default_state = Ctx {
545            tls_version: Some("tls1.3".to_string()),
546            remote_addr: Some("10.1.1.1".to_string()),
547            remote_port: Some(6000),
548            server_addr: Some("10.1.1.2".to_string()),
549            server_port: Some(6001),
550            upstream_address: "10.1.1.3:4123".to_string(),
551            connection_id: 102,
552            ..Default::default()
553        };
554
555        let value = convert_header_value(
556            &HeaderValue::from_str("$host").unwrap(),
557            &session,
558            &Ctx {
559                ..Default::default()
560            },
561        );
562        assert_eq!(true, value.is_some());
563        assert_eq!("pingap.io", value.unwrap().to_str().unwrap());
564
565        let value = convert_header_value(
566            &HeaderValue::from_str("$scheme").unwrap(),
567            &session,
568            &Ctx {
569                ..Default::default()
570            },
571        );
572        assert_eq!(true, value.is_some());
573        assert_eq!("http", value.unwrap().to_str().unwrap());
574        let value = convert_header_value(
575            &HeaderValue::from_str("$scheme").unwrap(),
576            &session,
577            &default_state,
578        );
579        assert_eq!(true, value.is_some());
580        assert_eq!("https", value.unwrap().to_str().unwrap());
581
582        let value = convert_header_value(
583            &HeaderValue::from_str("$remote_addr").unwrap(),
584            &session,
585            &default_state,
586        );
587        assert_eq!(true, value.is_some());
588        assert_eq!("10.1.1.1", value.unwrap().to_str().unwrap());
589
590        let value = convert_header_value(
591            &HeaderValue::from_str("$remote_port").unwrap(),
592            &session,
593            &default_state,
594        );
595        assert_eq!(true, value.is_some());
596        assert_eq!("6000", value.unwrap().to_str().unwrap());
597
598        let value = convert_header_value(
599            &HeaderValue::from_str("$server_addr").unwrap(),
600            &session,
601            &default_state,
602        );
603        assert_eq!(true, value.is_some());
604        assert_eq!("10.1.1.2", value.unwrap().to_str().unwrap());
605
606        let value = convert_header_value(
607            &HeaderValue::from_str("$server_port").unwrap(),
608            &session,
609            &default_state,
610        );
611        assert_eq!(true, value.is_some());
612        assert_eq!("6001", value.unwrap().to_str().unwrap());
613
614        let value = convert_header_value(
615            &HeaderValue::from_str("$upstream_addr").unwrap(),
616            &session,
617            &default_state,
618        );
619        assert_eq!(true, value.is_some());
620        assert_eq!("10.1.1.3:4123", value.unwrap().to_str().unwrap());
621
622        let value = convert_header_value(
623            &HeaderValue::from_str(":connection_id").unwrap(),
624            &session,
625            &default_state,
626        );
627        assert_eq!(true, value.is_some());
628        assert_eq!("102", value.unwrap().to_str().unwrap());
629
630        let headers = ["X-Forwarded-For: 1.1.1.1, 2.2.2.2"].join("\r\n");
631        let input_header =
632            format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
633        let mock_io = Builder::new().read(input_header.as_bytes()).build();
634        let mut session = Session::new_h1(Box::new(mock_io));
635        session.read_request().await.unwrap();
636        let value = convert_header_value(
637            &HeaderValue::from_str("$proxy_add_x_forwarded_for").unwrap(),
638            &session,
639            &Ctx {
640                remote_addr: Some("10.1.1.1".to_string()),
641                ..Default::default()
642            },
643        );
644        assert_eq!(true, value.is_some());
645        assert_eq!(
646            "1.1.1.1, 2.2.2.2, 10.1.1.1",
647            value.unwrap().to_str().unwrap()
648        );
649
650        let headers = [""].join("\r\n");
651        let input_header =
652            format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
653        let mock_io = Builder::new().read(input_header.as_bytes()).build();
654        let mut session = Session::new_h1(Box::new(mock_io));
655        session.read_request().await.unwrap();
656        let value = convert_header_value(
657            &HeaderValue::from_str("$proxy_add_x_forwarded_for").unwrap(),
658            &session,
659            &Ctx {
660                remote_addr: Some("10.1.1.1".to_string()),
661                ..Default::default()
662            },
663        );
664        assert_eq!(true, value.is_some());
665        assert_eq!("10.1.1.1", value.unwrap().to_str().unwrap());
666
667        let headers = [""].join("\r\n");
668        let input_header =
669            format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
670        let mock_io = Builder::new().read(input_header.as_bytes()).build();
671        let mut session = Session::new_h1(Box::new(mock_io));
672        session.read_request().await.unwrap();
673        let value = convert_header_value(
674            &HeaderValue::from_str("$upstream_addr").unwrap(),
675            &session,
676            &Ctx {
677                upstream_address: "10.1.1.1:8001".to_string(),
678                ..Default::default()
679            },
680        );
681        assert_eq!(true, value.is_some());
682        assert_eq!("10.1.1.1:8001", value.unwrap().to_str().unwrap());
683
684        let headers = ["Origin: https://github.com"].join("\r\n");
685        let input_header =
686            format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
687        let mock_io = Builder::new().read(input_header.as_bytes()).build();
688        let mut session = Session::new_h1(Box::new(mock_io));
689        session.read_request().await.unwrap();
690        let value = convert_header_value(
691            &HeaderValue::from_str("$http_origin").unwrap(),
692            &session,
693            &Ctx::default(),
694        );
695        assert_eq!(true, value.is_some());
696        assert_eq!("https://github.com", value.unwrap().to_str().unwrap());
697
698        let headers = ["Origin: https://github.com"].join("\r\n");
699        let input_header =
700            format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
701        let mock_io = Builder::new().read(input_header.as_bytes()).build();
702        let mut session = Session::new_h1(Box::new(mock_io));
703        session.read_request().await.unwrap();
704        let value = convert_header_value(
705            &HeaderValue::from_str("$hostname").unwrap(),
706            &session,
707            &Ctx::default(),
708        );
709        assert_eq!(true, value.is_some());
710
711        let headers = ["Origin: https://github.com"].join("\r\n");
712        let input_header =
713            format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
714        let mock_io = Builder::new().read(input_header.as_bytes()).build();
715        let mut session = Session::new_h1(Box::new(mock_io));
716        session.read_request().await.unwrap();
717        let value = convert_header_value(
718            &HeaderValue::from_str("$HOME").unwrap(),
719            &session,
720            &Ctx::default(),
721        );
722        assert_eq!(true, value.is_some());
723
724        let headers = ["Origin: https://github.com"].join("\r\n");
725        let input_header =
726            format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
727        let mock_io = Builder::new().read(input_header.as_bytes()).build();
728        let mut session = Session::new_h1(Box::new(mock_io));
729        session.read_request().await.unwrap();
730        let value = convert_header_value(
731            &HeaderValue::from_str("UUID").unwrap(),
732            &session,
733            &Ctx::default(),
734        );
735        assert_eq!(false, value.is_some());
736    }
737
738    #[tokio::test]
739    async fn test_get_host() {
740        let headers = ["Host: pingap.io"].join("\r\n");
741        let input_header =
742            format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
743        let mock_io = Builder::new().read(input_header.as_bytes()).build();
744        let mut session = Session::new_h1(Box::new(mock_io));
745        session.read_request().await.unwrap();
746        assert_eq!(get_host(session.req_header()), Some("pingap.io"));
747    }
748
749    #[test]
750    fn test_remove_query_from_header() {
751        let mut req =
752            RequestHeader::build("GET", b"/?apikey=123", None).unwrap();
753        remove_query_from_header(&mut req, "apikey").unwrap();
754        assert_eq!("/", req.uri.to_string());
755
756        let mut req =
757            RequestHeader::build("GET", b"/?apikey=123&name=pingap", None)
758                .unwrap();
759        remove_query_from_header(&mut req, "apikey").unwrap();
760        assert_eq!("/?name=pingap", req.uri.to_string());
761    }
762
763    #[tokio::test]
764    async fn test_get_client_ip() {
765        let headers = ["X-Forwarded-For:192.168.1.1"].join("\r\n");
766        let input_header =
767            format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
768        let mock_io = Builder::new().read(input_header.as_bytes()).build();
769        let mut session = Session::new_h1(Box::new(mock_io));
770        session.read_request().await.unwrap();
771        assert_eq!(get_client_ip(&session), "192.168.1.1");
772
773        let headers = ["X-Real-Ip:192.168.1.2"].join("\r\n");
774        let input_header =
775            format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
776        let mock_io = Builder::new().read(input_header.as_bytes()).build();
777        let mut session = Session::new_h1(Box::new(mock_io));
778        session.read_request().await.unwrap();
779        assert_eq!(get_client_ip(&session), "192.168.1.2");
780    }
781
782    #[tokio::test]
783    async fn test_get_header_value() {
784        let headers = ["Host: pingap.io"].join("\r\n");
785        let input_header =
786            format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
787        let mock_io = Builder::new().read(input_header.as_bytes()).build();
788        let mut session = Session::new_h1(Box::new(mock_io));
789        session.read_request().await.unwrap();
790        assert_eq!(
791            get_req_header_value(session.req_header(), "Host"),
792            Some("pingap.io")
793        );
794    }
795
796    #[tokio::test]
797    async fn test_get_cookie_value() {
798        let headers = ["Cookie: name=pingap"].join("\r\n");
799        let input_header =
800            format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
801        let mock_io = Builder::new().read(input_header.as_bytes()).build();
802        let mut session = Session::new_h1(Box::new(mock_io));
803        session.read_request().await.unwrap();
804        assert_eq!(
805            get_cookie_value(session.req_header(), "name"),
806            Some("pingap")
807        );
808    }
809
810    #[test]
811    fn test_convert_query_map() {
812        let query = "apikey=123&name=pingap";
813        let map = convert_query_map(query);
814        assert_eq!(map.len(), 2);
815        assert_eq!(map["apikey"], "123");
816        assert_eq!(map["name"], "pingap");
817
818        let query = "https://pingap.io/vicanso/pingap?apikey=123&name=pingap";
819        let map = convert_query_map(query);
820        assert_eq!(map.len(), 2);
821        assert_eq!(map["apikey"], "123");
822        assert_eq!(map["name"], "pingap");
823    }
824
825    #[tokio::test]
826    async fn test_get_query_value() {
827        let headers = ["X-Forwarded-For:192.168.1.1"].join("\r\n");
828        let input_header =
829            format!("GET /vicanso/pingap?size=1 HTTP/1.1\r\n{headers}\r\n\r\n");
830        let mock_io = Builder::new().read(input_header.as_bytes()).build();
831        let mut session = Session::new_h1(Box::new(mock_io));
832        session.read_request().await.unwrap();
833        assert_eq!(get_query_value(session.req_header(), "size"), Some("1"));
834    }
835}