1use crate::http;
2use crate::observable::{ObservableHttpRequest, ObservableHttpResponse};
3use huginn_net_db::db_matching_trait::FingerprintDb;
4use huginn_net_db::{Database, Label};
5
6pub struct SignatureMatcher<'a> {
7 database: &'a Database,
8}
9
10impl<'a> SignatureMatcher<'a> {
11 pub fn new(database: &'a Database) -> Self {
12 Self { database }
13 }
14
15 pub fn matching_by_http_request(
16 &self,
17 signature: &ObservableHttpRequest,
18 ) -> Option<(&'a Label, &'a http::Signature, f32)> {
19 self.database
20 .http_request
21 .find_best_match(&signature.matching)
22 }
23
24 pub fn matching_by_http_response(
25 &self,
26 signature: &ObservableHttpResponse,
27 ) -> Option<(&'a Label, &'a http::Signature, f32)> {
28 self.database
29 .http_response
30 .find_best_match(&signature.matching)
31 }
32
33 pub fn matching_by_user_agent(
34 &self,
35 user_agent: String,
36 ) -> Option<(&'a String, &'a Option<String>)> {
37 for (ua, ua_family) in &self.database.ua_os {
38 if user_agent.contains(ua) {
39 return Some((ua, ua_family));
40 }
41 }
42 None
43 }
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49 use crate::http::Version as HttpVersion;
50 use huginn_net_db::Type;
51 #[test]
52 fn matching_firefox2_by_http_request() {
53 let db = match Database::load_default() {
54 Ok(db) => db,
55 Err(e) => {
56 panic!("Failed to create default database: {e}");
57 }
58 };
59
60 let firefox_signature = ObservableHttpRequest {
61 matching: huginn_net_db::observable_signals::HttpRequestObservation {
62 version: HttpVersion::V10,
63 horder: vec![
64 http::Header::new("Host"),
65 http::Header::new("User-Agent"),
66 http::Header::new("Accept").with_value(",*/*;q="),
67 http::Header::new("Accept-Language").optional(),
68 http::Header::new("Accept-Encoding").with_value("gzip,deflate"),
69 http::Header::new("Accept-Charset").with_value("utf-8;q=0.7,*;q=0.7"),
70 http::Header::new("Keep-Alive").with_value("300"),
71 http::Header::new("Connection").with_value("keep-alive"),
72 ],
73 habsent: vec![],
74 expsw: "Firefox/".to_string(),
75 },
76 lang: None,
77 user_agent: None,
78 headers: vec![],
79 cookies: vec![],
80 referer: None,
81 method: Some("GET".to_string()),
82 uri: Some("/".to_string()),
83 };
84
85 let matcher = SignatureMatcher::new(&db);
86
87 if let Some((label, _matched_db_sig, quality)) =
88 matcher.matching_by_http_request(&firefox_signature)
89 {
90 assert_eq!(label.name, "Firefox");
91 assert_eq!(label.class, None);
92 assert_eq!(label.flavor, Some("2.x".to_string()));
93 assert_eq!(label.ty, Type::Specified);
94 assert_eq!(quality, 1.0);
95 } else {
96 panic!("No match found for Firefox 2.x HTTP signature");
97 }
98 }
99
100 #[test]
101 fn matching_apache_by_http_response() {
102 let db = match Database::load_default() {
103 Ok(db) => db,
104 Err(e) => {
105 panic!("Failed to create default database: {e}");
106 }
107 };
108
109 let apache_signature = ObservableHttpResponse {
110 matching: huginn_net_db::observable_signals::HttpResponseObservation {
111 version: HttpVersion::V11,
112 horder: vec![
113 http::Header::new("Date"),
114 http::Header::new("Server"),
115 http::Header::new("Last-Modified").optional(),
116 http::Header::new("Accept-Ranges")
117 .optional()
118 .with_value("bytes"),
119 http::Header::new("Content-Length").optional(),
120 http::Header::new("Content-Range").optional(),
121 http::Header::new("Keep-Alive").with_value("timeout"),
122 http::Header::new("Connection").with_value("Keep-Alive"),
123 http::Header::new("Transfer-Encoding")
124 .optional()
125 .with_value("chunked"),
126 http::Header::new("Content-Type"),
127 ],
128 habsent: vec![],
129 expsw: "Apache".to_string(),
130 },
131 headers: vec![],
132 status_code: Some(200),
133 };
134
135 let matcher = SignatureMatcher::new(&db);
136
137 if let Some((label, _matched_db_sig, quality)) =
138 matcher.matching_by_http_response(&apache_signature)
139 {
140 assert_eq!(label.name, "Apache");
141 assert_eq!(label.class, None);
142 assert_eq!(label.flavor, Some("2.x".to_string()));
143 assert_eq!(label.ty, Type::Specified);
144 assert_eq!(quality, 1.0);
145 } else {
146 panic!("No match found for Apache 2.x HTTP response signature");
147 }
148 }
149
150 #[test]
151 fn matching_android_chrome_by_http_request() {
152 let db = match Database::load_default() {
153 Ok(db) => db,
154 Err(e) => {
155 panic!("Failed to create default database: {e}");
156 }
157 };
158
159 let android_chrome_signature = ObservableHttpRequest {
160 matching: huginn_net_db::observable_signals::HttpRequestObservation {
161 version: HttpVersion::V11, horder: vec![
163 http::Header::new("Host"),
164 http::Header::new("Connection").with_value("keep-alive"),
165 http::Header::new("User-Agent"),
166 http::Header::new("Accept").with_value("image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8"),
167 http::Header::new("Referer").optional(), http::Header::new("Accept-Encoding").with_value("gzip, deflate"),
169 http::Header::new("Accept-Language").with_value("en-US,en;q=0.9,es;q=0.8"),
170 ],
171 habsent: vec![
172 http::Header::new("Accept-Charset"),
173 http::Header::new("Keep-Alive"),
174 ],
175 expsw: "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36".to_string(),
176 },
177 lang: Some("English".to_string()),
178 user_agent: Some("Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Mobile Safari/537.36".to_string()),
179 headers: vec![],
180 cookies: vec![],
181 referer: None,
182 method: Some("GET".to_string()),
183 uri: Some("/".to_string()),
184 };
185
186 let matcher = SignatureMatcher::new(&db);
187
188 match matcher.matching_by_http_request(&android_chrome_signature) {
189 Some((label, _matched_db_sig, quality)) => {
190 assert_eq!(label.name, "Chrome");
191 assert_eq!(label.class, None);
192 assert_eq!(label.flavor, Some("11 or newer".to_string()));
193 assert_eq!(label.ty, Type::Specified);
194 assert_eq!(quality, 0.7);
195 }
196 None => {
197 panic!("No HTTP match found for Android Chrome signature");
198 }
199 }
200 }
201}