From 68283b3ce67d87903e2108c2cadf04c00026469a Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Fri, 8 May 2026 22:36:37 +0800 Subject: [PATCH 1/7] feat(emulate): parse tls/http2 --- Cargo.toml | 1 + lib/wreq_ruby/{emulation.rb => emulate.rb} | 16 + src/client.rs | 60 +- src/client/req.rs | 18 +- src/emulate.rs | 696 +++++++++++++++++---- src/error.rs | 8 + src/macros.rs | 9 - test/emulate_test.rb | 39 ++ test/emulation_test.rb | 21 - test/results/chrome_147.json | 212 +++++++ 10 files changed, 890 insertions(+), 190 deletions(-) rename lib/wreq_ruby/{emulation.rb => emulate.rb} (86%) create mode 100644 test/emulate_test.rb delete mode 100644 test/emulation_test.rb create mode 100644 test/results/chrome_147.json diff --git a/Cargo.toml b/Cargo.toml index 7324b18..45179a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ wreq = { version = "6.0.0-rc.28", features = [ wreq-util = "3.0.0-rc.10" serde = { version = "1.0", features = ["derive"] } serde_magnus = "0.10.0" +serde_json = "1.0" indexmap = { version = "2.12.1", features = ["serde"] } cookie = "0.18" bytes = "1.11.1" diff --git a/lib/wreq_ruby/emulation.rb b/lib/wreq_ruby/emulate.rb similarity index 86% rename from lib/wreq_ruby/emulation.rb rename to lib/wreq_ruby/emulate.rb index b71cb55..c32292f 100644 --- a/lib/wreq_ruby/emulation.rb +++ b/lib/wreq_ruby/emulate.rb @@ -182,5 +182,21 @@ class Emulation def self.new(device: nil, os: nil, skip_http2: false, skip_headers: false) end end + + unless method_defined?(:parse) + # Parses a string representation of an emulation option. + # @param json [String] String to parse into an TLS/HTTP2 emulation option + # @param permute_extensions [Boolean, nil] Whether to permute extensions (optional) + # @param psk_skip_session_ticket [Boolean] Whether to skip session ticket for PSK + # @param aes_hw_override [Boolean, nil] Override AES hardware support (optional) + # @param random_aes_hw_override [Boolean] Whether to randomly override AES + # @return [Wreq::Emulation] Parsed emulation option + def self.parse(json, + permute_extensions: nil, + psk_skip_session_ticket: false, + aes_hw_override: nil, + random_aes_hw_override: false) + end + end end end diff --git a/src/client.rs b/src/client.rs index aa536e4..839df8e 100644 --- a/src/client.rs +++ b/src/client.rs @@ -25,6 +25,15 @@ use crate::{ http::Method, }; +macro_rules! request { + ($args:expr, $required:ty) => {{ + let args = magnus::scan_args::scan_args::<$required, (), (), (), magnus::RHash, ()>($args)?; + let required = args.required; + let request = crate::client::req::Request::new(&ruby!(), args.keywords)?; + (required, request) + }}; +} + /// A builder for `Client`. #[derive(Default, Deserialize)] struct Builder { @@ -127,31 +136,32 @@ pub struct Client(wreq::Client); impl Builder { /// Create a new [`Builder`] from Ruby keyword arguments. - fn new(ruby: &magnus::Ruby, keyword: &Value) -> Result { - if let Ok(hash) = RHash::try_convert(*keyword) { + fn new(ruby: &magnus::Ruby, keyword: Value) -> Result { + if let Ok(hash) = RHash::try_convert(keyword) { let mut builder: Self = serde_magnus::deserialize(ruby, hash)?; // extra emulation handling - if let Some(v) = hash.get(ruby.to_symbol("emulation")) { - let emulation_obj = Obj::::try_convert(v)?; - builder.emulation = Some((*emulation_obj).clone()); + if let Some(v) = hash.get(ruby.to_symbol(stringify!(emulation))) { + let obj = Obj::::try_convert(v)?; + builder.emulation = Some((*obj).clone()); + } + + // extra cookie store handling + if let Some(jar) = hash.get(ruby.to_symbol(stringify!(cookie_provider))) { + let obj = Obj::::try_convert(jar)?; + builder.cookie_provider = Some((*obj).clone()); } // extra user agent handling - builder.user_agent = Extractor::::try_convert(*keyword)?.into_inner(); + builder.user_agent = Extractor::::try_convert(keyword)?.into_inner(); // extra headers handling - builder.headers = Extractor::::try_convert(*keyword)?.into_inner(); + builder.headers = Extractor::::try_convert(keyword)?.into_inner(); // extra original headers handling - builder.orig_headers = Extractor::::try_convert(*keyword)?.into_inner(); + builder.orig_headers = Extractor::::try_convert(keyword)?.into_inner(); // extra proxy handling - builder.proxy = Extractor::::try_convert(*keyword)?.into_inner(); - - // extra cookie store handling - if let Some(jar) = hash.get(ruby.to_symbol("cookie_provider")) { - builder.cookie_provider = Some((*Obj::::try_convert(jar)?).clone()); - } + builder.proxy = Extractor::::try_convert(keyword)?.into_inner(); return Ok(builder); } @@ -166,12 +176,12 @@ impl Client { /// Create a new [`Client`] with the given keyword arguments. pub fn new(ruby: &Ruby, kwargs: &[Value]) -> Result { if let Some(kwargs) = kwargs.first() { - let mut params = Builder::new(ruby, kwargs)?; + let mut params = Builder::new(ruby, *kwargs)?; gvl::nogvl(|| { let mut builder = wreq::Client::builder(); // Emulation options. - apply_option!(set_if_some_inner, builder, params.emulation, emulation); + apply_option!(set_if_some, builder, params.emulation, emulation); // User agent options. apply_option!(set_if_some, builder, params.user_agent, user_agent); @@ -312,63 +322,63 @@ impl Client { /// Send a HTTP request. #[inline] pub fn request(rb_self: &Self, args: &[Value]) -> Result { - let ((method, url), request) = extract_request!(args, (Obj, String)); + let ((method, url), request) = request!(args, (Obj, String)); execute_request(rb_self.0.clone(), *method, url, request) } /// Send a GET request. #[inline] pub fn get(rb_self: &Self, args: &[Value]) -> Result { - let ((url,), request) = extract_request!(args, (String,)); + let ((url,), request) = request!(args, (String,)); execute_request(rb_self.0.clone(), Method::GET, url, request) } /// Send a POST request. #[inline] pub fn post(rb_self: &Self, args: &[Value]) -> Result { - let ((url,), request) = extract_request!(args, (String,)); + let ((url,), request) = request!(args, (String,)); execute_request(rb_self.0.clone(), Method::POST, url, request) } /// Send a PUT request. #[inline] pub fn put(rb_self: &Self, args: &[Value]) -> Result { - let ((url,), request) = extract_request!(args, (String,)); + let ((url,), request) = request!(args, (String,)); execute_request(rb_self.0.clone(), Method::PUT, url, request) } /// Send a DELETE request. #[inline] pub fn delete(rb_self: &Self, args: &[Value]) -> Result { - let ((url,), request) = extract_request!(args, (String,)); + let ((url,), request) = request!(args, (String,)); execute_request(rb_self.0.clone(), Method::DELETE, url, request) } /// Send a HEAD request. #[inline] pub fn head(rb_self: &Self, args: &[Value]) -> Result { - let ((url,), request) = extract_request!(args, (String,)); + let ((url,), request) = request!(args, (String,)); execute_request(rb_self.0.clone(), Method::HEAD, url, request) } /// Send an OPTIONS request. #[inline] pub fn options(rb_self: &Self, args: &[Value]) -> Result { - let ((url,), request) = extract_request!(args, (String,)); + let ((url,), request) = request!(args, (String,)); execute_request(rb_self.0.clone(), Method::OPTIONS, url, request) } /// Send a TRACE request. #[inline] pub fn trace(rb_self: &Self, args: &[Value]) -> Result { - let ((url,), request) = extract_request!(args, (String,)); + let ((url,), request) = request!(args, (String,)); execute_request(rb_self.0.clone(), Method::TRACE, url, request) } /// Send a PATCH request. #[inline] pub fn patch(rb_self: &Self, args: &[Value]) -> Result { - let ((url,), request) = extract_request!(args, (String,)); + let ((url,), request) = request!(args, (String,)); execute_request(rb_self.0.clone(), Method::PATCH, url, request) } } diff --git a/src/client/req.rs b/src/client/req.rs index c9ff446..c23f47a 100644 --- a/src/client/req.rs +++ b/src/client/req.rs @@ -109,9 +109,14 @@ impl Request { let mut builder: Self = serde_magnus::deserialize(ruby, kwargs)?; // extra emulation handling - if let Some(v) = hash.get(ruby.to_symbol("emulation")) { - let emulation_obj = Obj::::try_convert(v)?; - builder.emulation = Some((*emulation_obj).clone()); + if let Some(v) = hash.get(ruby.to_symbol(stringify!(emulation))) { + let obj = Obj::::try_convert(v)?; + builder.emulation = Some((*obj).clone()); + } + + // extra body handling + if let Some(body) = hash.get(ruby.to_symbol(stringify!(body))) { + builder.body = Some(Body::try_convert(body)?); } // extra version handling @@ -129,11 +134,6 @@ impl Request { // extra proxy handling builder.proxy = Extractor::::try_convert(kwargs)?.into_inner(); - // extra body handling - if let Some(body) = hash.get(ruby.to_symbol("body")) { - builder.body = Some(Body::try_convert(body)?); - } - Ok(builder) } } @@ -148,7 +148,7 @@ pub fn execute_request>( let mut builder = client.request(method.into_ffi(), url.as_ref()); // Emulation options. - apply_option!(set_if_some_inner, builder, request.emulation, emulation); + apply_option!(set_if_some, builder, request.emulation, emulation); // Version options. apply_option!(set_if_some, builder, request.version, version); diff --git a/src/emulate.rs b/src/emulate.rs index b73ede4..a8c7389 100644 --- a/src/emulate.rs +++ b/src/emulate.rs @@ -1,7 +1,10 @@ use magnus::{ - Error, Module, Object, RHash, RModule, Ruby, TryConvert, Value, function, method, + Error, Module, Object, RHash, RModule, Ruby, TryConvert, Value, function, method, scan_args, typed_data::{Inspect, Obj}, }; +use wreq::EmulationFactory; + +use crate::{emulate::parse::ParserOptions, error::serde_json_error_to_magnus}; define_ruby_enum!( /// An emulation. @@ -135,7 +138,10 @@ define_ruby_enum!( /// A struct to represent the `EmulationOption` class. #[derive(Clone)] #[magnus::wrap(class = "Wreq::Emulation", free_immediately, size)] -pub struct Emulation(pub wreq_util::EmulationOption); +pub enum Emulation { + Emulation(Box), + EmulationOption(wreq_util::EmulationOption), +} // ===== impl EmulationDevice ===== @@ -163,16 +169,16 @@ impl Emulation { let mut skip_headers = None; if let Some(hash) = args.first().and_then(|v| RHash::from_value(*v)) { - if let Some(v) = hash.get(ruby.to_symbol("device")) { + if let Some(v) = hash.get(ruby.to_symbol(stringify!(device))) { device = Some(Obj::::try_convert(v)?); } - if let Some(v) = hash.get(ruby.to_symbol("os")) { + if let Some(v) = hash.get(ruby.to_symbol(stringify!(os))) { os = Some(Obj::::try_convert(v)?); } - if let Some(v) = hash.get(ruby.to_symbol("skip_http2")) { + if let Some(v) = hash.get(ruby.to_symbol(stringify!(skip_http2))) { skip_http2 = Some(bool::try_convert(v)?); } - if let Some(v) = hash.get(ruby.to_symbol("skip_headers")) { + if let Some(v) = hash.get(ruby.to_symbol(stringify!(skip_headers))) { skip_headers = Some(bool::try_convert(v)?); } } @@ -184,134 +190,572 @@ impl Emulation { .skip_headers(skip_headers.unwrap_or(false)) .build(); - Ok(Self(emulation)) + Ok(Self::EmulationOption(emulation)) + } + + fn parse(ruby: &Ruby, args: &[Value]) -> Result { + let args = scan_args::scan_args::<(String,), (), (), (), RHash, ()>(args)?; + + let json: serde_json::Value = + serde_json::from_str(args.required.0.as_str()).map_err(serde_json_error_to_magnus)?; + let opts: ParserOptions = serde_magnus::deserialize(ruby, args.keywords)?; + + let mut builder = wreq::Emulation::builder(); + + if let Some(tls_options) = parse::parse_tls(&json, opts) { + builder = builder.tls_options(tls_options); + } + + if let Some((http2_options, headers)) = parse::parse_http2(&json) { + builder = builder.http2_options(http2_options).headers(headers); + } + + Ok(Self::Emulation(Box::new(builder.build()))) + } +} + +impl EmulationFactory for Emulation { + #[inline] + fn emulation(self) -> wreq::Emulation { + match self { + Emulation::Emulation(e) => *e, + Emulation::EmulationOption(opt) => opt.emulation(), + } } } pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> { // EmulationDevice enum binding - let emulation_class = gem_module.define_class("EmulationDevice", ruby.class_object())?; - emulation_class.define_method("to_s", method!(EmulationDevice::to_s, 0))?; - emulation_class.const_set("Chrome100", EmulationDevice::Chrome100)?; - emulation_class.const_set("Chrome101", EmulationDevice::Chrome101)?; - emulation_class.const_set("Chrome104", EmulationDevice::Chrome104)?; - emulation_class.const_set("Chrome105", EmulationDevice::Chrome105)?; - emulation_class.const_set("Chrome106", EmulationDevice::Chrome106)?; - emulation_class.const_set("Chrome107", EmulationDevice::Chrome107)?; - emulation_class.const_set("Chrome108", EmulationDevice::Chrome108)?; - emulation_class.const_set("Chrome109", EmulationDevice::Chrome109)?; - emulation_class.const_set("Chrome110", EmulationDevice::Chrome110)?; - emulation_class.const_set("Chrome114", EmulationDevice::Chrome114)?; - emulation_class.const_set("Chrome116", EmulationDevice::Chrome116)?; - emulation_class.const_set("Chrome117", EmulationDevice::Chrome117)?; - emulation_class.const_set("Chrome118", EmulationDevice::Chrome118)?; - emulation_class.const_set("Chrome119", EmulationDevice::Chrome119)?; - emulation_class.const_set("Chrome120", EmulationDevice::Chrome120)?; - emulation_class.const_set("Chrome123", EmulationDevice::Chrome123)?; - emulation_class.const_set("Chrome124", EmulationDevice::Chrome124)?; - emulation_class.const_set("Chrome126", EmulationDevice::Chrome126)?; - emulation_class.const_set("Chrome127", EmulationDevice::Chrome127)?; - emulation_class.const_set("Chrome128", EmulationDevice::Chrome128)?; - emulation_class.const_set("Chrome129", EmulationDevice::Chrome129)?; - emulation_class.const_set("Chrome130", EmulationDevice::Chrome130)?; - emulation_class.const_set("Chrome131", EmulationDevice::Chrome131)?; - emulation_class.const_set("Chrome132", EmulationDevice::Chrome132)?; - emulation_class.const_set("Chrome133", EmulationDevice::Chrome133)?; - emulation_class.const_set("Chrome134", EmulationDevice::Chrome134)?; - emulation_class.const_set("Chrome135", EmulationDevice::Chrome135)?; - emulation_class.const_set("Chrome136", EmulationDevice::Chrome136)?; - emulation_class.const_set("Chrome137", EmulationDevice::Chrome137)?; - emulation_class.const_set("Chrome138", EmulationDevice::Chrome138)?; - emulation_class.const_set("Chrome139", EmulationDevice::Chrome139)?; - emulation_class.const_set("Chrome140", EmulationDevice::Chrome140)?; - emulation_class.const_set("Chrome141", EmulationDevice::Chrome141)?; - emulation_class.const_set("Chrome142", EmulationDevice::Chrome142)?; - emulation_class.const_set("Chrome143", EmulationDevice::Chrome143)?; - emulation_class.const_set("Chrome144", EmulationDevice::Chrome144)?; - emulation_class.const_set("Chrome145", EmulationDevice::Chrome145)?; - emulation_class.const_set("Edge101", EmulationDevice::Edge101)?; - emulation_class.const_set("Edge122", EmulationDevice::Edge122)?; - emulation_class.const_set("Edge127", EmulationDevice::Edge127)?; - emulation_class.const_set("Edge131", EmulationDevice::Edge131)?; - emulation_class.const_set("Edge134", EmulationDevice::Edge134)?; - emulation_class.const_set("Edge135", EmulationDevice::Edge135)?; - emulation_class.const_set("Edge136", EmulationDevice::Edge136)?; - emulation_class.const_set("Edge137", EmulationDevice::Edge137)?; - emulation_class.const_set("Edge138", EmulationDevice::Edge138)?; - emulation_class.const_set("Edge139", EmulationDevice::Edge139)?; - emulation_class.const_set("Edge140", EmulationDevice::Edge140)?; - emulation_class.const_set("Edge141", EmulationDevice::Edge141)?; - emulation_class.const_set("Edge142", EmulationDevice::Edge142)?; - emulation_class.const_set("Edge143", EmulationDevice::Edge143)?; - emulation_class.const_set("Edge144", EmulationDevice::Edge144)?; - emulation_class.const_set("Edge145", EmulationDevice::Edge145)?; - emulation_class.const_set("Firefox109", EmulationDevice::Firefox109)?; - emulation_class.const_set("Firefox117", EmulationDevice::Firefox117)?; - emulation_class.const_set("Firefox128", EmulationDevice::Firefox128)?; - emulation_class.const_set("Firefox133", EmulationDevice::Firefox133)?; - emulation_class.const_set("Firefox135", EmulationDevice::Firefox135)?; - emulation_class.const_set("FirefoxPrivate135", EmulationDevice::FirefoxPrivate135)?; - emulation_class.const_set("FirefoxAndroid135", EmulationDevice::FirefoxAndroid135)?; - emulation_class.const_set("Firefox136", EmulationDevice::Firefox136)?; - emulation_class.const_set("FirefoxPrivate136", EmulationDevice::FirefoxPrivate136)?; - emulation_class.const_set("Firefox139", EmulationDevice::Firefox139)?; - emulation_class.const_set("Firefox142", EmulationDevice::Firefox142)?; - emulation_class.const_set("Firefox143", EmulationDevice::Firefox143)?; - emulation_class.const_set("Firefox144", EmulationDevice::Firefox144)?; - emulation_class.const_set("Firefox145", EmulationDevice::Firefox145)?; - emulation_class.const_set("Firefox146", EmulationDevice::Firefox146)?; - emulation_class.const_set("Firefox147", EmulationDevice::Firefox147)?; - emulation_class.const_set("SafariIos17_2", EmulationDevice::SafariIos17_2)?; - emulation_class.const_set("SafariIos17_4_1", EmulationDevice::SafariIos17_4_1)?; - emulation_class.const_set("SafariIos16_5", EmulationDevice::SafariIos16_5)?; - emulation_class.const_set("Safari15_3", EmulationDevice::Safari15_3)?; - emulation_class.const_set("Safari15_5", EmulationDevice::Safari15_5)?; - emulation_class.const_set("Safari15_6_1", EmulationDevice::Safari15_6_1)?; - emulation_class.const_set("Safari16", EmulationDevice::Safari16)?; - emulation_class.const_set("Safari16_5", EmulationDevice::Safari16_5)?; - emulation_class.const_set("Safari17_0", EmulationDevice::Safari17_0)?; - emulation_class.const_set("Safari17_2_1", EmulationDevice::Safari17_2_1)?; - emulation_class.const_set("Safari17_4_1", EmulationDevice::Safari17_4_1)?; - emulation_class.const_set("Safari17_5", EmulationDevice::Safari17_5)?; - emulation_class.const_set("Safari17_6", EmulationDevice::Safari17_6)?; - emulation_class.const_set("Safari18", EmulationDevice::Safari18)?; - emulation_class.const_set("SafariIPad18", EmulationDevice::SafariIPad18)?; - emulation_class.const_set("Safari18_2", EmulationDevice::Safari18_2)?; - emulation_class.const_set("Safari18_3", EmulationDevice::Safari18_3)?; - emulation_class.const_set("Safari18_3_1", EmulationDevice::Safari18_3_1)?; - emulation_class.const_set("SafariIos18_1_1", EmulationDevice::SafariIos18_1_1)?; - emulation_class.const_set("Safari18_5", EmulationDevice::Safari18_5)?; - emulation_class.const_set("Safari26", EmulationDevice::Safari26)?; - emulation_class.const_set("Safari26_1", EmulationDevice::Safari26_1)?; - emulation_class.const_set("Safari26_2", EmulationDevice::Safari26_2)?; - emulation_class.const_set("SafariIos26", EmulationDevice::SafariIos26)?; - emulation_class.const_set("SafariIos26_2", EmulationDevice::SafariIos26_2)?; - emulation_class.const_set("SafariIPad26", EmulationDevice::SafariIPad26)?; - emulation_class.const_set("SafariIpad26_2", EmulationDevice::SafariIpad26_2)?; - emulation_class.const_set("OkHttp3_9", EmulationDevice::OkHttp3_9)?; - emulation_class.const_set("OkHttp3_11", EmulationDevice::OkHttp3_11)?; - emulation_class.const_set("OkHttp3_13", EmulationDevice::OkHttp3_13)?; - emulation_class.const_set("OkHttp3_14", EmulationDevice::OkHttp3_14)?; - emulation_class.const_set("OkHttp4_9", EmulationDevice::OkHttp4_9)?; - emulation_class.const_set("OkHttp4_10", EmulationDevice::OkHttp4_10)?; - emulation_class.const_set("OkHttp4_12", EmulationDevice::OkHttp4_12)?; - emulation_class.const_set("OkHttp5", EmulationDevice::OkHttp5)?; - emulation_class.const_set("Opera116", EmulationDevice::Opera116)?; - emulation_class.const_set("Opera117", EmulationDevice::Opera117)?; - emulation_class.const_set("Opera118", EmulationDevice::Opera118)?; - emulation_class.const_set("Opera119", EmulationDevice::Opera119)?; + let device_class = gem_module.define_class("EmulationDevice", ruby.class_object())?; + device_class.define_method("to_s", method!(EmulationDevice::to_s, 0))?; + device_class.const_set("Chrome100", EmulationDevice::Chrome100)?; + device_class.const_set("Chrome101", EmulationDevice::Chrome101)?; + device_class.const_set("Chrome104", EmulationDevice::Chrome104)?; + device_class.const_set("Chrome105", EmulationDevice::Chrome105)?; + device_class.const_set("Chrome106", EmulationDevice::Chrome106)?; + device_class.const_set("Chrome107", EmulationDevice::Chrome107)?; + device_class.const_set("Chrome108", EmulationDevice::Chrome108)?; + device_class.const_set("Chrome109", EmulationDevice::Chrome109)?; + device_class.const_set("Chrome110", EmulationDevice::Chrome110)?; + device_class.const_set("Chrome114", EmulationDevice::Chrome114)?; + device_class.const_set("Chrome116", EmulationDevice::Chrome116)?; + device_class.const_set("Chrome117", EmulationDevice::Chrome117)?; + device_class.const_set("Chrome118", EmulationDevice::Chrome118)?; + device_class.const_set("Chrome119", EmulationDevice::Chrome119)?; + device_class.const_set("Chrome120", EmulationDevice::Chrome120)?; + device_class.const_set("Chrome123", EmulationDevice::Chrome123)?; + device_class.const_set("Chrome124", EmulationDevice::Chrome124)?; + device_class.const_set("Chrome126", EmulationDevice::Chrome126)?; + device_class.const_set("Chrome127", EmulationDevice::Chrome127)?; + device_class.const_set("Chrome128", EmulationDevice::Chrome128)?; + device_class.const_set("Chrome129", EmulationDevice::Chrome129)?; + device_class.const_set("Chrome130", EmulationDevice::Chrome130)?; + device_class.const_set("Chrome131", EmulationDevice::Chrome131)?; + device_class.const_set("Chrome132", EmulationDevice::Chrome132)?; + device_class.const_set("Chrome133", EmulationDevice::Chrome133)?; + device_class.const_set("Chrome134", EmulationDevice::Chrome134)?; + device_class.const_set("Chrome135", EmulationDevice::Chrome135)?; + device_class.const_set("Chrome136", EmulationDevice::Chrome136)?; + device_class.const_set("Chrome137", EmulationDevice::Chrome137)?; + device_class.const_set("Chrome138", EmulationDevice::Chrome138)?; + device_class.const_set("Chrome139", EmulationDevice::Chrome139)?; + device_class.const_set("Chrome140", EmulationDevice::Chrome140)?; + device_class.const_set("Chrome141", EmulationDevice::Chrome141)?; + device_class.const_set("Chrome142", EmulationDevice::Chrome142)?; + device_class.const_set("Chrome143", EmulationDevice::Chrome143)?; + device_class.const_set("Chrome144", EmulationDevice::Chrome144)?; + device_class.const_set("Chrome145", EmulationDevice::Chrome145)?; + device_class.const_set("Edge101", EmulationDevice::Edge101)?; + device_class.const_set("Edge122", EmulationDevice::Edge122)?; + device_class.const_set("Edge127", EmulationDevice::Edge127)?; + device_class.const_set("Edge131", EmulationDevice::Edge131)?; + device_class.const_set("Edge134", EmulationDevice::Edge134)?; + device_class.const_set("Edge135", EmulationDevice::Edge135)?; + device_class.const_set("Edge136", EmulationDevice::Edge136)?; + device_class.const_set("Edge137", EmulationDevice::Edge137)?; + device_class.const_set("Edge138", EmulationDevice::Edge138)?; + device_class.const_set("Edge139", EmulationDevice::Edge139)?; + device_class.const_set("Edge140", EmulationDevice::Edge140)?; + device_class.const_set("Edge141", EmulationDevice::Edge141)?; + device_class.const_set("Edge142", EmulationDevice::Edge142)?; + device_class.const_set("Edge143", EmulationDevice::Edge143)?; + device_class.const_set("Edge144", EmulationDevice::Edge144)?; + device_class.const_set("Edge145", EmulationDevice::Edge145)?; + device_class.const_set("Firefox109", EmulationDevice::Firefox109)?; + device_class.const_set("Firefox117", EmulationDevice::Firefox117)?; + device_class.const_set("Firefox128", EmulationDevice::Firefox128)?; + device_class.const_set("Firefox133", EmulationDevice::Firefox133)?; + device_class.const_set("Firefox135", EmulationDevice::Firefox135)?; + device_class.const_set("FirefoxPrivate135", EmulationDevice::FirefoxPrivate135)?; + device_class.const_set("FirefoxAndroid135", EmulationDevice::FirefoxAndroid135)?; + device_class.const_set("Firefox136", EmulationDevice::Firefox136)?; + device_class.const_set("FirefoxPrivate136", EmulationDevice::FirefoxPrivate136)?; + device_class.const_set("Firefox139", EmulationDevice::Firefox139)?; + device_class.const_set("Firefox142", EmulationDevice::Firefox142)?; + device_class.const_set("Firefox143", EmulationDevice::Firefox143)?; + device_class.const_set("Firefox144", EmulationDevice::Firefox144)?; + device_class.const_set("Firefox145", EmulationDevice::Firefox145)?; + device_class.const_set("Firefox146", EmulationDevice::Firefox146)?; + device_class.const_set("Firefox147", EmulationDevice::Firefox147)?; + device_class.const_set("SafariIos17_2", EmulationDevice::SafariIos17_2)?; + device_class.const_set("SafariIos17_4_1", EmulationDevice::SafariIos17_4_1)?; + device_class.const_set("SafariIos16_5", EmulationDevice::SafariIos16_5)?; + device_class.const_set("Safari15_3", EmulationDevice::Safari15_3)?; + device_class.const_set("Safari15_5", EmulationDevice::Safari15_5)?; + device_class.const_set("Safari15_6_1", EmulationDevice::Safari15_6_1)?; + device_class.const_set("Safari16", EmulationDevice::Safari16)?; + device_class.const_set("Safari16_5", EmulationDevice::Safari16_5)?; + device_class.const_set("Safari17_0", EmulationDevice::Safari17_0)?; + device_class.const_set("Safari17_2_1", EmulationDevice::Safari17_2_1)?; + device_class.const_set("Safari17_4_1", EmulationDevice::Safari17_4_1)?; + device_class.const_set("Safari17_5", EmulationDevice::Safari17_5)?; + device_class.const_set("Safari17_6", EmulationDevice::Safari17_6)?; + device_class.const_set("Safari18", EmulationDevice::Safari18)?; + device_class.const_set("SafariIPad18", EmulationDevice::SafariIPad18)?; + device_class.const_set("Safari18_2", EmulationDevice::Safari18_2)?; + device_class.const_set("Safari18_3", EmulationDevice::Safari18_3)?; + device_class.const_set("Safari18_3_1", EmulationDevice::Safari18_3_1)?; + device_class.const_set("SafariIos18_1_1", EmulationDevice::SafariIos18_1_1)?; + device_class.const_set("Safari18_5", EmulationDevice::Safari18_5)?; + device_class.const_set("Safari26", EmulationDevice::Safari26)?; + device_class.const_set("Safari26_1", EmulationDevice::Safari26_1)?; + device_class.const_set("Safari26_2", EmulationDevice::Safari26_2)?; + device_class.const_set("SafariIos26", EmulationDevice::SafariIos26)?; + device_class.const_set("SafariIos26_2", EmulationDevice::SafariIos26_2)?; + device_class.const_set("SafariIPad26", EmulationDevice::SafariIPad26)?; + device_class.const_set("SafariIpad26_2", EmulationDevice::SafariIpad26_2)?; + device_class.const_set("OkHttp3_9", EmulationDevice::OkHttp3_9)?; + device_class.const_set("OkHttp3_11", EmulationDevice::OkHttp3_11)?; + device_class.const_set("OkHttp3_13", EmulationDevice::OkHttp3_13)?; + device_class.const_set("OkHttp3_14", EmulationDevice::OkHttp3_14)?; + device_class.const_set("OkHttp4_9", EmulationDevice::OkHttp4_9)?; + device_class.const_set("OkHttp4_10", EmulationDevice::OkHttp4_10)?; + device_class.const_set("OkHttp4_12", EmulationDevice::OkHttp4_12)?; + device_class.const_set("OkHttp5", EmulationDevice::OkHttp5)?; + device_class.const_set("Opera116", EmulationDevice::Opera116)?; + device_class.const_set("Opera117", EmulationDevice::Opera117)?; + device_class.const_set("Opera118", EmulationDevice::Opera118)?; + device_class.const_set("Opera119", EmulationDevice::Opera119)?; // EmulationOS enum binding - let emulation_os_class = gem_module.define_class("EmulationOS", ruby.class_object())?; - emulation_os_class.define_method("to_s", method!(EmulationOS::to_s, 0))?; - emulation_os_class.const_set("Windows", EmulationOS::Windows)?; - emulation_os_class.const_set("MacOS", EmulationOS::MacOS)?; - emulation_os_class.const_set("Linux", EmulationOS::Linux)?; - emulation_os_class.const_set("Android", EmulationOS::Android)?; - emulation_os_class.const_set("IOS", EmulationOS::IOS)?; + let os_class = gem_module.define_class("EmulationOS", ruby.class_object())?; + os_class.define_method("to_s", method!(EmulationOS::to_s, 0))?; + os_class.const_set("Windows", EmulationOS::Windows)?; + os_class.const_set("MacOS", EmulationOS::MacOS)?; + os_class.const_set("Linux", EmulationOS::Linux)?; + os_class.const_set("Android", EmulationOS::Android)?; + os_class.const_set("IOS", EmulationOS::IOS)?; // Emulation class binding - let emulation_option_class = gem_module.define_class("Emulation", ruby.class_object())?; - emulation_option_class.define_singleton_method("new", function!(Emulation::new, -1))?; + let class = gem_module.define_class("Emulation", ruby.class_object())?; + class.define_singleton_method("new", function!(Emulation::new, -1))?; + class.define_singleton_method("parse", function!(Emulation::parse, -1))?; Ok(()) } + +mod parse { + //! //! Currently, only newer versions of Chrome support parsing https://tls.peet.ws/api/all. + + use http::{HeaderMap, HeaderName, HeaderValue}; + use serde::Deserialize; + use serde_json::Value; + use std::str::FromStr; + use wreq::{ + http2::{ + Http2Options, PseudoId, PseudoOrder, SettingId, SettingsOrder, StreamDependency, + StreamId, + }, + tls::{ + AlpnProtocol, AlpsProtocol, CertificateCompressionAlgorithm, TlsOptions, TlsVersion, + }, + }; + + macro_rules! get { + ($json:expr, $key:ident, $method:ident) => { + $json.get(stringify!($key)).and_then(|v| v.$method()) + }; + } + + macro_rules! find { + ($array:expr, $key1:ident, $method1:ident, $key2:ident) => { + $array + .iter() + .find(|v| get!(v, $key1, $method1) == Some(stringify!($key2))) + }; + } + + macro_rules! get_and_then { + ($json:expr, $key1:ident, $method1:ident, $key2:ident, $method2:ident) => { + get!($json, $key1, $method1).and_then(|v| get!(v, $key2, $method2)) + }; + } + + macro_rules! find_and_then { + ($array:expr, $key1:ident, $method1:ident, $key2:ident, $key3:ident, $method2:ident) => { + $array + .iter() + .find(|v| get!(v, $key1, $method1) == Some(stringify!($key2))) + .and_then(|v| get!(v, $key3, $method2)) + }; + } + + #[derive(Default, Deserialize)] + #[non_exhaustive] + pub struct ParserOptions { + /// Whether to skip session tickets when using PSK. + #[serde(default)] + psk_skip_session_ticket: bool, + + /// Controls whether ClientHello extensions should be permuted. + #[serde(default)] + permute_extensions: Option, + + /// Overrides AES hardware acceleration. + #[serde(default)] + aes_hw_override: Option, + + /// Overrides the random AES hardware acceleration. + #[serde(default)] + random_aes_hw_override: bool, + } + + pub fn parse_tls(json: &Value, opts: ParserOptions) -> Option { + let tls = get!(json, tls, as_object)?; + let mut tls_builder = TlsOptions::builder() + .aes_hw_override(opts.aes_hw_override) + .random_aes_hw_override(opts.random_aes_hw_override) + .permute_extensions(opts.permute_extensions) + .psk_skip_session_ticket(opts.psk_skip_session_ticket); + + // parse ciphers + if let Some(ciphers) = get!(tls, ciphers, as_array) { + let ciphers_list = ciphers + .iter() + .flat_map(|v| v.as_str()) + .filter(|s| !s.is_empty() && !s.starts_with(stringify!(TLS_GREASE))) + .collect::>() + .join(":"); + tls_builder = tls_builder + .cipher_list(ciphers_list) + .preserve_tls13_cipher_list(true) + } + + for extension in get!(tls, extensions, as_array)? { + let Some(name) = + get!(extension, name, as_str).and_then(|s| s.split_whitespace().next()) + else { + continue; + }; + + tls_builder = match name { + stringify!(session_ticket) => tls_builder.session_ticket(true), + stringify!(extensionEncryptedClientHello) => tls_builder.enable_ech_grease(true), + stringify!(signed_certificate_timestamp) => { + tls_builder.enable_signed_cert_timestamps(true) + } + stringify!(ec_point_formats) | stringify!(extended_master_secret) => { + // todo: parse ec point formats and extended master secret + continue; + } + stringify!(extensionRenegotiationInfo) => tls_builder.renegotiation(true), + stringify!(key_share) => { + // todo: parse key share groups + continue; + } + stringify!(supported_versions) => { + let Some(versions) = get!(extension, versions, as_array) else { + continue; + }; + + for version in versions + .iter() + .filter_map(|s| s.as_str()) + .map(|s| s.split_whitespace().nth(1)) + { + tls_builder = match version { + Some(stringify!(1.0)) => { + tls_builder.min_tls_version(TlsVersion::TLS_1_0) + } + Some(stringify!(1.1)) => { + tls_builder.min_tls_version(TlsVersion::TLS_1_1) + } + Some(stringify!(1.2)) => { + tls_builder.min_tls_version(TlsVersion::TLS_1_2) + } + Some(stringify!(1.3)) => { + tls_builder.max_tls_version(TlsVersion::TLS_1_3) + } + Some(_) | None => { + continue; + } + } + } + + tls_builder + } + stringify!(application_settings) | stringify!(application_settings_old) => { + let Some(protocols) = get!(extension, protocols, as_array) else { + continue; + }; + + let protocols = protocols + .iter() + .filter_map(|v| v.as_str()) + .flat_map(|s| match s { + "http/1.1" => Some(AlpsProtocol::HTTP1), + stringify!(h2) => Some(AlpsProtocol::HTTP2), + stringify!(h3) => Some(AlpsProtocol::HTTP3), + _ => None, + }) + .collect::>(); + + tls_builder + .alps_protocols(protocols) + .alps_use_new_codepoint(name == stringify!(application_settings)) + } + stringify!(application_layer_protocol_negotiation) => { + let Some(protocols) = get!(extension, protocols, as_array) else { + continue; + }; + + let protocols = protocols + .iter() + .filter_map(|v| v.as_str()) + .flat_map(|s| match s { + "http/1.1" => Some(AlpnProtocol::HTTP1), + stringify!(h2) => Some(AlpnProtocol::HTTP2), + stringify!(h3) => Some(AlpnProtocol::HTTP3), + _ => None, + }) + .collect::>(); + + tls_builder.alpn_protocols(protocols) + } + stringify!(status_request) => tls_builder.enable_ocsp_stapling(true), + stringify!(psk_key_exchange_modes) => tls_builder.psk_dhe_ke(true), + stringify!(supported_groups) => { + let Some(groups) = get!(extension, supported_groups, as_array) else { + continue; + }; + + let groups = groups + .iter() + .filter_map(|s| s.as_str()) + .filter(|s| !s.is_empty() && !s.starts_with(stringify!(TLS_GREASE))) + .flat_map(|s| s.split_whitespace().next()) + .collect::>() + .join(":"); + + tls_builder.curves_list(groups) + } + stringify!(compress_certificate) => { + let Some(algorithms) = get!(extension, algorithms, as_array) else { + continue; + }; + + let algorithms = algorithms + .iter() + .filter_map(|s| s.as_str()) + .filter(|s| !s.is_empty()) + .flat_map(|s| match s.split_whitespace().next() { + Some(stringify!(zlib)) => Some(CertificateCompressionAlgorithm::ZLIB), + Some(stringify!(brotli)) => { + Some(CertificateCompressionAlgorithm::BROTLI) + } + Some(stringify!(zstd)) => Some(CertificateCompressionAlgorithm::ZSTD), + Some(_) | None => None, + }) + .collect::>(); + + tls_builder.certificate_compression_algorithms(algorithms) + } + stringify!(signature_algorithms) => { + let Some(algorithms) = get!(extension, signature_algorithms, as_array) else { + continue; + }; + + let algorithms = algorithms + .iter() + .filter_map(|s| s.as_str()) + .filter(|s| !s.is_empty()) + .flat_map(|s| s.split_whitespace().next()) + .collect::>() + .join(":"); + + tls_builder.sigalgs_list(algorithms) + } + stringify!(pre_shared_key) => tls_builder.pre_shared_key(true), + name if name.starts_with(stringify!(TLS_GREASE)) => { + tls_builder.grease_enabled(true) + } + _ => continue, + }; + } + + Some(tls_builder.build()) + } + + pub fn parse_http2(json: &Value) -> Option<(Http2Options, HeaderMap)> { + let sent_frames = get_and_then!(json, http2, as_object, sent_frames, as_array)?; + + let mut http2_builder = Http2Options::builder(); + let mut headers_map = HeaderMap::new(); + + // parse settings frame + if let Some(settings) = find_and_then!( + sent_frames, + frame_type, + as_str, + SETTINGS, + settings, + as_array + ) { + let mut settings_order = SettingsOrder::builder(); + for setting in settings.iter().filter_map(|v| v.as_str()) { + let mut parts = setting.split('='); + let (Some(name), Some(value)) = (parts.next(), parts.next()) else { + continue; + }; + let value = value.trim(); + + match name.trim() { + stringify!(HEADER_TABLE_SIZE) => { + if let Ok(value) = value.parse::() { + http2_builder = http2_builder.header_table_size(value); + settings_order = settings_order.push(SettingId::HeaderTableSize); + } + } + stringify!(ENABLE_PUSH) => { + if let Ok(value) = value.parse::() { + http2_builder = http2_builder.enable_push(value != 0); + settings_order = settings_order.push(SettingId::EnablePush); + } + } + stringify!(INITIAL_WINDOW_SIZE) => { + if let Ok(value) = value.parse::() { + http2_builder = http2_builder.initial_window_size(value); + settings_order = settings_order.push(SettingId::InitialWindowSize); + } + } + stringify!(MAX_FRAME_SIZE) => { + if let Ok(value) = value.parse::() { + http2_builder = http2_builder.max_frame_size(value); + settings_order = settings_order.push(SettingId::MaxFrameSize); + } + } + stringify!(MAX_HEADER_LIST_SIZE) => { + if let Ok(value) = value.parse::() { + http2_builder = http2_builder.max_header_list_size(value); + settings_order = settings_order.push(SettingId::MaxHeaderListSize); + } + } + stringify!(MAX_CONCURRENT_STREAMS) => { + if let Ok(value) = value.parse::() { + http2_builder = http2_builder.max_concurrent_streams(value); + settings_order = settings_order.push(SettingId::MaxConcurrentStreams); + } + } + stringify!(ENABLE_CONNECT_PROTOCOL) | stringify!(UNKNOWN_SETTING_8) => { + if let Ok(value) = value.parse::() { + http2_builder = http2_builder.enable_connect_protocol(value != 0); + settings_order = settings_order.push(SettingId::EnableConnectProtocol); + } + } + stringify!(NO_RFC7540_PRIORITIES) => { + if let Ok(value) = value.parse::() { + http2_builder = http2_builder.no_rfc7540_priorities(value != 0); + settings_order = settings_order.push(SettingId::NoRfc7540Priorities); + } + } + _ => {} + } + } + + http2_builder = http2_builder.settings_order(settings_order.build()); + } + + // parse window update frame + if let Some(window_update) = find_and_then!( + sent_frames, + frame_type, + as_str, + WINDOW_UPDATE, + increment, + as_u64 + ) { + http2_builder = + http2_builder.initial_connection_window_size((window_update + 65535) as u32); + } + + // parse headers frame + if let Some(headers_frame) = find!(sent_frames, frame_type, as_str, HEADERS) { + // parse initial stream id + if let Some(init_stream_id) = get!(headers_frame, stream_id, as_u64).filter(|v| *v != 0) + { + http2_builder = http2_builder.initial_stream_id(init_stream_id as u32); + } + + // parse headers + if let Some(headers) = get!(headers_frame, headers, as_array) { + let mut pseudo_builder = PseudoOrder::builder(); + + for (name, value) in headers + .iter() + .filter_map(|h| h.as_str()) + .filter_map(|h| h.split_once(": ")) + { + match name { + stringify!(:method) => { + pseudo_builder = pseudo_builder.push(PseudoId::Method); + } + stringify!(:path) => { + pseudo_builder = pseudo_builder.push(PseudoId::Path); + } + stringify!(:scheme) => { + pseudo_builder = pseudo_builder.push(PseudoId::Scheme); + } + stringify!(:authority) => { + pseudo_builder = pseudo_builder.push(PseudoId::Authority); + } + stringify!(:status) => { + pseudo_builder = pseudo_builder.push(PseudoId::Status); + } + stringify!(:protocol) => { + pseudo_builder = pseudo_builder.push(PseudoId::Protocol); + } + _ => { + if let (Ok(header_name), Ok(header_value)) = + (HeaderName::from_str(name), HeaderValue::from_str(value)) + { + headers_map.insert(header_name, header_value); + } + } + } + } + + http2_builder = http2_builder.headers_pseudo_order(pseudo_builder.build()); + }; + + // parse header priority + if let Some(priority) = get!(headers_frame, priority, as_object) { + if let (Some(depends_on), Some(weight), Some(exclusive)) = ( + get!(priority, depends_on, as_u64), + get!(priority, weight, as_u64), + get!(priority, exclusive, as_u64), + ) { + http2_builder = http2_builder.headers_stream_dependency(StreamDependency::new( + if depends_on == 0 { + StreamId::zero() + } else { + StreamId::from(depends_on as u32) + }, + (weight - 1) as u8, + exclusive != 0, + )); + } + } + } + + Some((http2_builder.build(), headers_map)) + } +} diff --git a/src/error.rs b/src/error.rs index edb6932..8edc1f3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -109,6 +109,14 @@ pub fn header_value_error_to_magnus(err: wreq::header::InvalidHeaderValue) -> Ma ) } +/// Map [`serde_json::Error`] to corresponding [`magnus::Error`] +pub fn serde_json_error_to_magnus(err: serde_json::Error) -> MagnusError { + MagnusError::new( + ruby!().get_inner(&DECODING_ERROR), + format!("failed to decode JSON: {err}"), + ) +} + /// Map [`wreq::Error`] to corresponding [`magnus::Error`] pub fn wreq_error_to_magnus(err: wreq::Error) -> MagnusError { let error_msg = err.to_string(); diff --git a/src/macros.rs b/src/macros.rs index 3a8e414..705be1b 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -107,12 +107,3 @@ macro_rules! ruby { magnus::Ruby::get().expect("Failed to get Ruby VM instance") }; } - -macro_rules! extract_request { - ($args:expr, $required:ty) => {{ - let args = magnus::scan_args::scan_args::<$required, (), (), (), magnus::RHash, ()>($args)?; - let required = args.required; - let request = crate::client::req::Request::new(&ruby!(), args.keywords)?; - (required, request) - }}; -} diff --git a/test/emulate_test.rb b/test/emulate_test.rb new file mode 100644 index 0000000..cbddde6 --- /dev/null +++ b/test/emulate_test.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "test_helper" + +class EmulationTest < Minitest::Test + def test_all_emulation_device_constants_are_non_nil + Wreq::EmulationDevice.constants.each do |name| + const = Wreq::EmulationDevice.const_get(name) + assert_instance_of Wreq::EmulationDevice, const, + "#{name} should be EmulationDevice, got #{const.inspect}" + end + end + + def test_all_emulation_os_constants_are_non_nil + Wreq::EmulationOS.constants.each do |name| + const = Wreq::EmulationOS.const_get(name) + assert_instance_of Wreq::EmulationOS, const, + "#{name} should be EmulationOS, got #{const.inspect}" + end + end + + def test_http2_parser + str = File.read("test/results/chrome_147.json") + json = JSON.parse(str) + emulation = Wreq::Emulation.parse(JSON.dump(json), permute_extensions: true) + client = Wreq::Client.new(emulation: emulation) + resp = client.get("https://tls.peet.ws/api/all") + # ja4(no psk) + assert_includes resp.bytes, "t13d1516h2_8daaf6152771_d8a2da3f94cd" + # akamai + assert_includes resp.bytes, "52d84b11737d980aef856699f885ca86" + + resp = client.get("https://tls.peet.ws/api/all") + # ja4(psk) + assert_includes resp.bytes, "t13d1517h2_8daaf6152771_b6f405a00624" + # akamai + assert_includes resp.bytes, "52d84b11737d980aef856699f885ca86" + end +end diff --git a/test/emulation_test.rb b/test/emulation_test.rb deleted file mode 100644 index 5c074d8..0000000 --- a/test/emulation_test.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -class EmulationTest < Minitest::Test - def test_all_emulation_device_constants_are_non_nil - Wreq::EmulationDevice.constants.each do |name| - const = Wreq::EmulationDevice.const_get(name) - assert_instance_of Wreq::EmulationDevice, const, - "#{name} should be EmulationDevice, got #{const.inspect}" - end - end - - def test_all_emulation_os_constants_are_non_nil - Wreq::EmulationOS.constants.each do |name| - const = Wreq::EmulationOS.const_get(name) - assert_instance_of Wreq::EmulationOS, const, - "#{name} should be EmulationOS, got #{const.inspect}" - end - end -end diff --git a/test/results/chrome_147.json b/test/results/chrome_147.json new file mode 100644 index 0000000..7e4574a --- /dev/null +++ b/test/results/chrome_147.json @@ -0,0 +1,212 @@ +{ + "donate": "Please consider donating to keep this API running. Visit https://tls.peet.ws", + "ip": "127.0.0.1:52923", + "http_version": "h2", + "method": "GET", + "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36", + "tls": { + "ciphers": [ + "TLS_GREASE (0x2A2A)", + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA" + ], + "extensions": [ + { + "name": "TLS_GREASE (0xfafa)" + }, + { + "name": "session_ticket (35)", + "data": "" + }, + { + "name": "extensionEncryptedClientHello (boringssl) (65037)", + "data": "00000100018d00206cb2d3505d6b460f4a4629c52c04bfd50bccb039a9c76517ee7b96c2d0f7d44a00d03109b7f92d694e9e7b1b8d2a2023f13618653437a17f5cd56efc84abd82259f09bfa6b917c7ceb91f66b8c8c3c8f0e0e06f597f8b54f1413e9d6987e2e8ad0aef1acc6b6215db451472f9d07672e823ae0606c130159dbdb42eeacb954b2192d07d9aeb981f982a72b2eddbf0102be5ea8cc2cadc50d412d1840e899aa329b9e526731f00a5c20d327c0a9edd4de7484679b08c3e4d8043dfa5f3c1ee510a8abcd48a1dec0aeb627fddc1abfe959af5247f0e2ceacaea52edb0e5f0fe014ce4b6cba2f0bd63e91c397e8370585a0de65" + }, + { + "name": "signed_certificate_timestamp (18)" + }, + { + "name": "ec_point_formats (11)", + "elliptic_curves_point_formats": [ + "0x00" + ] + }, + { + "name": "extended_master_secret (23)", + "master_secret_data": "", + "extended_master_secret_data": "" + }, + { + "name": "extensionRenegotiationInfo (boringssl) (65281)", + "data": "00" + }, + { + "name": "key_share (51)", + "shared_keys": [ + { + "TLS_GREASE (0xdada)": "00" + }, + { + "X25519MLKEM768 (4588)": "36137b88327561f3936c11846cb51075a2719c1c91975cbcf3d771be943051a4757004a1ee1b0659074ddd7bce98350375160455e48c12f1543e19b3f0e357e8ba74e4730fd8535da72114b63b76ac605dc13ba6fe64ccb4b2ad177248a8bb610e20281d0132d0da8fc9220db4294bd9a11d2fab0d449489afc3b4f12318dbfc324987a3ae3b35060302cfd0226c7ba4668856d6cb01ba44b2cb6923bffc2622541be8771a5180a7e9871c13a47463fa41d9c3839977c007d73c2bd888acb562b5872c033510a10749afd36faba21cc5068ecdc9c616701886397384d940f8bcac78ac15fe113862d23e93b32776b940573a6ebef79afdf327df64cfecf78cbe6386f8636fd529acbd4928db26954b8204bb47a6b4323df69b4c13447e73f995d3fc6c1b24a4be19765db411fbe6bc2e3b90bc2170afc3352a031ab1d63278c1042af06b5bd6546ca0022e753e37157865b75a81a88ba4f17658b9ae1003a15a6194b8e451aaf13dc444c0fae1352f4b6b7af19ea0e1710b82c6493401fc52ab08349dfc9b8bcb68a2918cc1ef6c190294822cc381b2555df31c53347a240b635c36bb82ba215d7f4bc493c266aaa8887bb1264cf48e7f75bd18f06d330591706738f680bce8636b61e2a090ba759027c1d794bf6653bb665750787a9b72e5b10156c13003398e6c2ac6c422ccb796fc16301d8649835a1c023b120396803da74d9ef2960ef959adec0d50119a403b4a1551cd99a27d36e8b4885a6510ab601684431e5b4178a0ba61aa95aa38adb485880db7a69a98541c922393a7b39610a1a7ba5dbf090144541f5bfc46d24331e73a061cdb07cf0a9d484a134416776a543b320373f70681b39193ee2c0b8e3a3a566519e1e861c9140227648a027c305ad8180df60c9fa37b5498511f129545329ad8f5b3f72bab52b4ba1f14b580563ee89678bbf4021aac1dbe41cb1323a9b252be9ecba6f09347581416e6f0545d3c76a52930d0a89258e6b73bc317b3662b84352699c501dcfb8a6a5c3155b1aeeb86394dd46d575639bb782153d77633e51c6b2052d883069112ac1ce4284bfc7d46ba2238c579966725a75ab61c887b98b74e204aa9f1cc629b47c141aa8c85e17bd37a03b319c966e6a0d11881dce674751643e0fca133e4cd78eabe2801c9832024ea21b04779a512d4ab472c25aa375177523919636f682491103a288abb8663541332f1ae48848e082b78bd267ac4268971150589e31b99239e10f916eba90c45da66cc588d4a341c25844094e9c840a731a1a22643615961a16cd5b979290017311a6067691c40ea4c900802dc389575d74413ecbcf2a46eff26725df0a9a7663815328f76f924b4b136d9fc0fca9bbb87929466e709330705f043c429b36cc5e73476b4190816a20e51923df23401ac9a9bb16a7f46666962bd11b75758f02b68ca548ed768c46414308a90fa77ace20b9b05967bbe14c4bd6294487a4df4d06f0dfba7aac9516fd07dd2cccf831678752838282646446b1ca71b3eb2392badb01b88dbcd89d8314d937186aa5709938639b85844d92f43f478e7e02dbd749a30933f851529796b05654ccdbac18aec407dfd8169764bbf255ab3b8676a191e14b6152fcd1f448e04508ae955b8274de2f690df01abf3a629ddb1750e95136388f99ec10fb4076667bfcbc3556d2ffad6510926a3ac1094a7b4d6fb2e09" + }, + { + "X25519 (29)": "b0a7038312bcb9285da654fc5f255db7a6b841eb5fec511bb375e46865e0300f" + } + ] + }, + { + "name": "supported_versions (43)", + "versions": [ + "TLS_GREASE (0x0a0a)", + "TLS 1.3", + "TLS 1.2" + ] + }, + { + "name": "application_settings (17613)", + "protocols": [ + "h2" + ] + }, + { + "name": "application_layer_protocol_negotiation (16)", + "protocols": [ + "h2", + "http/1.1" + ] + }, + { + "name": "status_request (5)", + "status_request": { + "certificate_status_type": "OSCP (1)", + "responder_id_list_length": 0, + "request_extensions_length": 0 + } + }, + { + "name": "psk_key_exchange_modes (45)", + "PSK_Key_Exchange_Mode": "PSK with (EC)DHE key establishment (psk_dhe_ke) (1)" + }, + { + "name": "supported_groups (10)", + "supported_groups": [ + "TLS_GREASE (0xdada)", + "X25519MLKEM768 (4588)", + "X25519 (29)", + "P-256 (23)", + "P-384 (24)" + ] + }, + { + "name": "compress_certificate (27)", + "algorithms": [ + "brotli (2)" + ] + }, + { + "name": "signature_algorithms (13)", + "signature_algorithms": [ + "ecdsa_secp256r1_sha256", + "rsa_pss_rsae_sha256", + "rsa_pkcs1_sha256", + "ecdsa_secp384r1_sha384", + "rsa_pss_rsae_sha384", + "rsa_pkcs1_sha384", + "rsa_pss_rsae_sha512", + "rsa_pkcs1_sha512" + ] + }, + { + "name": "TLS_GREASE (0xaaaa)" + }, + { + "name": "pre_shared_key (41)", + "data": "00770071b615af90805f1cd6623df343053e86f12aa67622c11ee69f48bd60315a280a7f17a4cfec51f7df9d7336c1edd9ed20df0919d91be008d2d43f4155460be3bf223faa3b92e3b154d6fb77573288870f6022c4f5b4e7ac27f3b0675fe7e5e1873b7fc0d62b42cbfee4a588d67ff7da252d256237e7a60021208b09df84d5b62f70012af04c02c5a2669838b4f17fa297b03973f699a6378536" + } + ], + "tls_version_record": "771", + "tls_version_negotiated": "772", + "ja3": "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,35-65037-18-11-23-65281-51-43-17613-16-5-45-10-27-13,4588-29-23-24,0", + "ja3_hash": "66bd61cd6c5d03877af45cf6cdc82659", + "ja4": "t13d1517h2_8daaf6152771_b6f405a00624", + "ja4_r": "t13d1515h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0005,000a,000b,000d,0012,0017,001b,0023,002b,002d,0033,44cd,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601", + "peetprint": "GREASE-772-771|2-1.1|GREASE-4588-29-23-24|1027-2052-1025-1283-2053-1281-2054-1537|1|2|GREASE-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53|10-11-13-16-17613-18-23-27-35-43-45-5-51-65037-65281-GREASE-GREASE", + "peetprint_hash": "c14d8fcd599c5e24ac9256038d744ad1", + "client_random": "61500eb7aee99ee96f8cc4377a2252d75f78d02dec06747a508cfbdd9e2ad15d", + "session_id": "6fdeeeffb4c45c916a8415f7f1632a3bf5445c991ef1e4956018735b6d086853" + }, + "http2": { + "akamai_fingerprint": "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p", + "akamai_fingerprint_hash": "52d84b11737d980aef856699f885ca86", + "sent_frames": [ + { + "frame_type": "SETTINGS", + "length": 24, + "settings": [ + "HEADER_TABLE_SIZE = 65536", + "ENABLE_PUSH = 0", + "INITIAL_WINDOW_SIZE = 6291456", + "MAX_HEADER_LIST_SIZE = 262144" + ] + }, + { + "frame_type": "WINDOW_UPDATE", + "length": 4, + "increment": 15663105 + }, + { + "frame_type": "HEADERS", + "stream_id": 1, + "length": 481, + "headers": [ + ":method: GET", + ":authority: 127.0.0.1", + ":scheme: https", + ":path: /api/all", + "cache-control: max-age=0", + "sec-ch-ua: \"Google Chrome\";v=\"147\", \"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"147\"", + "sec-ch-ua-mobile: ?0", + "sec-ch-ua-platform: \"Windows\"", + "dnt: 1", + "upgrade-insecure-requests: 1", + "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36", + "accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "sec-fetch-site: none", + "sec-fetch-mode: navigate", + "sec-fetch-user: ?1", + "sec-fetch-dest: document", + "accept-encoding: gzip, deflate, br, zstd", + "accept-language: en,zh-CN;q=0.9,zh;q=0.8", + "priority: u=0, i" + ], + "flags": [ + "EndStream (0x1)", + "EndHeaders (0x4)", + "Priority (0x20)" + ], + "priority": { + "weight": 256, + "depends_on": 0, + "exclusive": 1 + } + } + ] + }, + "tcpip": { + "ip": {}, + "tcp": {} + } +} \ No newline at end of file From 80c6b83a979b1c6735213550a819d7e7005e051d Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Tue, 19 May 2026 11:27:18 +0800 Subject: [PATCH 2/7] test(emulate): Add chrome 148 test --- test/emulate_test.rb | 33 +++--- test/results/chrome_148.json | 212 +++++++++++++++++++++++++++++++++++ 2 files changed, 231 insertions(+), 14 deletions(-) create mode 100644 test/results/chrome_148.json diff --git a/test/emulate_test.rb b/test/emulate_test.rb index cbddde6..81b9299 100644 --- a/test/emulate_test.rb +++ b/test/emulate_test.rb @@ -20,20 +20,25 @@ def test_all_emulation_os_constants_are_non_nil end def test_http2_parser - str = File.read("test/results/chrome_147.json") - json = JSON.parse(str) - emulation = Wreq::Emulation.parse(JSON.dump(json), permute_extensions: true) - client = Wreq::Client.new(emulation: emulation) - resp = client.get("https://tls.peet.ws/api/all") - # ja4(no psk) - assert_includes resp.bytes, "t13d1516h2_8daaf6152771_d8a2da3f94cd" - # akamai - assert_includes resp.bytes, "52d84b11737d980aef856699f885ca86" - resp = client.get("https://tls.peet.ws/api/all") - # ja4(psk) - assert_includes resp.bytes, "t13d1517h2_8daaf6152771_b6f405a00624" - # akamai - assert_includes resp.bytes, "52d84b11737d980aef856699f885ca86" + profiles = ['test/results/chrome_147.json', 'test/results/chrome_148.json'] + + profiles.each do |profile| + str = File.read(profile) + json = JSON.parse(str) + emulation = Wreq::Emulation.parse(JSON.dump(json), permute_extensions: true) + client = Wreq::Client.new(emulation: emulation) + resp = client.get("https://tls.peet.ws/api/all") + # ja4(no psk) + assert_includes resp.bytes, "t13d1516h2_8daaf6152771_d8a2da3f94cd" + # akamai + assert_includes resp.bytes, "52d84b11737d980aef856699f885ca86" + + resp = client.get("https://tls.peet.ws/api/all") + # ja4(psk) + assert_includes resp.bytes, "t13d1517h2_8daaf6152771_b6f405a00624" + # akamai + assert_includes resp.bytes, "52d84b11737d980aef856699f885ca86" + end end end diff --git a/test/results/chrome_148.json b/test/results/chrome_148.json new file mode 100644 index 0000000..d7d3425 --- /dev/null +++ b/test/results/chrome_148.json @@ -0,0 +1,212 @@ +{ + "donate": "Please consider donating to keep this API running. Visit https://tls.peet.ws", + "ip": "142.171.157.68:51784", + "http_version": "h2", + "method": "GET", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36", + "tls": { + "ciphers": [ + "TLS_GREASE (0x6A6A)", + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA" + ], + "extensions": [ + { + "name": "TLS_GREASE (0x9a9a)" + }, + { + "name": "server_name (0)", + "server_name": "tls.peet.ws" + }, + { + "name": "supported_versions (43)", + "versions": [ + "TLS_GREASE (0xbaba)", + "TLS 1.3", + "TLS 1.2" + ] + }, + { + "name": "ec_point_formats (11)", + "elliptic_curves_point_formats": [ + "0x00" + ] + }, + { + "name": "extended_master_secret (23)", + "master_secret_data": "", + "extended_master_secret_data": "" + }, + { + "name": "status_request (5)", + "status_request": { + "certificate_status_type": "OSCP (1)", + "responder_id_list_length": 0, + "request_extensions_length": 0 + } + }, + { + "name": "application_layer_protocol_negotiation (16)", + "protocols": [ + "h2", + "http/1.1" + ] + }, + { + "name": "signed_certificate_timestamp (18)" + }, + { + "name": "supported_groups (10)", + "supported_groups": [ + "TLS_GREASE (0xcaca)", + "X25519MLKEM768 (4588)", + "X25519 (29)", + "P-256 (23)", + "P-384 (24)" + ] + }, + { + "name": "signature_algorithms (13)", + "signature_algorithms": [ + "ecdsa_secp256r1_sha256", + "rsa_pss_rsae_sha256", + "rsa_pkcs1_sha256", + "ecdsa_secp384r1_sha384", + "rsa_pss_rsae_sha384", + "rsa_pkcs1_sha384", + "rsa_pss_rsae_sha512", + "rsa_pkcs1_sha512" + ] + }, + { + "name": "extensionEncryptedClientHello (boringssl) (65037)", + "data": "0000010001f400201c4627df227e98d94373824ff4eed8fb07f1ae69d63eea1691bd41147391c6100090e48a5132264647cb7165b10d4397a64679d3deff996fe04ce50d897b55578e8ef2568a775464b55e99ef5aea7c22b4d34260b40d668c4ab79ce1a0abb8fcc6160f99a9e1292b9f82bef5f204090e4963757866d5f61c016021d294413d4f090a45b4aa1b4ccb839e7d65b90f14a94aa8f6adf29f34a8adf1188c9bdf9dbf6359a4a731882dacd33427af3c5e9150a5ec" + }, + { + "name": "extensionRenegotiationInfo (boringssl) (65281)", + "data": "00" + }, + { + "name": "psk_key_exchange_modes (45)", + "PSK_Key_Exchange_Mode": "PSK with (EC)DHE key establishment (psk_dhe_ke) (1)" + }, + { + "name": "compress_certificate (27)", + "algorithms": [ + "brotli (2)" + ] + }, + { + "name": "application_settings (17613)", + "protocols": [ + "h2" + ] + }, + { + "name": "session_ticket (35)", + "data": "" + }, + { + "name": "key_share (51)", + "shared_keys": [ + { + "TLS_GREASE (0xcaca)": "00" + }, + { + "X25519MLKEM768 (4588)": "5a1742fb30ca9ea09c39dabeb3d85fb1656ff461a7faa5b5d9f77676d7a18e02ae516992b08c7d699a1dc7323e48c264bba64168149288550b77cbc1ef097bd86b33812b0e8d54a1210a69de1298e47794299baa7a19c7c1922af5b6a86ae28cb9f1bed424a722b34079564f1b807e7b490f9423b7a2483db67288f08c2ae6c2ba57d2bcd730c5fdcb75cf13882de5cb5be80003e85524b90f272c04df3b62141b139fd78abe82af9cfb61bdc29fb63a190a9c9fa34ca47502bcd7a89fbf5b76715090ae962241e7152ba438817991ddbb7e0e4c3bb35a4a5c1715076a6ab14c792d5683e260877d07a8f0dccfa783821a32190e8bb7eed9bb3c509c03f79c53c87f8baca2090b16da34531c183903f21cf18b0ab988276f9cc7386b8708cc05d10cb3bed71585c8c5e4ea419e72219b7b2f7274c14816b4c31c00d5858940701954194dfbb70de51b76ce45815c3313e23478305888900191c794b03727cda5231af3dc1ef42ccf8f997ff19033cad56cbc4901ffb1157b711272e201070b49e0918b4ccbc25d4c1d1e4a23114ca434a32b555124ee0cb28c235acdd57a18616b1a8a6a5044c44db17f46e104f053613975c07d9a37c1698f353172ecd420f39909bc565be0b6b8e026781259c2a569b8cb56a1bd6899a283419f428c6fa5242d1b80e5b045c2f801b3b8b6a77ac55948614734525a2935b9f33c64c11267da044106c1f5374d7134c3fa205148b01721b8645a555d0c837a9c0a706849c14e12682ac8c5a8960cc9f867764573cf1640b142be92569fb369cc27a4682b6a82d3598824d393d80905d2132b2c9a0f6ba37336e4aafbf9ad5cf147d9cc170242cdcdb34301944436c26609f79f66bc11e3cc00ec2a5f142bb57df53603c93bb334ccf1b55581bc211167871e08211487b4c78a49eb8887ebb2cf1ac78b2424519a7547fb8a64a36b414e6c556b4a6b5ba36583128b3dd940d6d025d076babccb9f40c4bb2cb56a5c6cacf9fa9a28571e81ec93d9c8c2f2203790271e80f2b93379405100bf87b6bbdd249ed2370099062045187e676ba0973b985ca780614cb16287979b624360d8a48872a254c7ae747cb725a62d5981ca7f3851e313753e78477be710f38578c78751c25294b0e541aeea069142b117a4581c0a520c8994d02873b2840d13478240835826c6c607fc2f23003ece2580f6032eb4fca79eb4b62b535706a79090639afc96c21f892aae839f983026bf694933a5ae9e265601558c8a7279abb2c6b6d8c358fc7e45d486f6553439ebadacc29b15dc142bdab681971c8dc33c06f90bfac923c0d2100f1119d04c0aaad0312471794f906bbdcbbd179ba251503daad926d7a5ac03212f30bb09df6c51d1d779d2968d4214237e77780d83296ee80bf9facece9caee1bb3d2d1602f4739d58991ec16bbb1c06615c6a51dfb492b4e79aade98f01bc8582d845ca2b121d8088722487bd997ac5925e3a36278f734225463c3cc288cd6246b7d534ad2a0da75737b924a6302387e2010ef74798d84a32f70c84d8535e08c628d0fb83d20409401576862a6a77d747a0cc2353783e73369e745a67a925c404966cee0c6742207870182074f9638f7a7fef2ef23ac7b1e4b62ae2d14786dfb28def2b1b3b80e23f91a26022297ade339d8c48c419cf833b7ca76b87553ba99b3bcf096f5a7250e6dd2bbf6025d7157027" + }, + { + "X25519 (29)": "b8c462f31e59270b29b810404e7f6bd8f76f1cf8ce1b82769e130d5a49af9b29" + } + ] + }, + { + "name": "TLS_GREASE (0x3a3a)" + }, + { + "name": "pre_shared_key (41)", + "data": "00770071d95a3c61eb0ac3959991953f069b282f689ee0b885f38b1205480ea4ac6a75cce598aae910454dddf33df83565f8da2ba2e3f8d5196cb7ef1a36badb2e69e631fbd00ca067c8abbef6ee335964e2ff25048a37c4b490debb0e977a81a55a34d28d45c3fba668ad017f1a6dc792a88da49f33b2aa2000212086ff36b4b3e331b92555037ccfad878e9ab286905378985efddbf18df068faa6" + } + ], + "tls_version_record": "771", + "tls_version_negotiated": "772", + "ja3": "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-43-11-23-5-16-18-10-13-65037-65281-45-27-17613-35-51-41,4588-29-23-24,0", + "ja3_hash": "2f2211a8e8e265a3ea0efa2eef9b64aa", + "ja4": "t13d1517h2_8daaf6152771_b6f405a00624", + "ja4_r": "t13d1517h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0005,000a,000b,000d,0012,0017,001b,0023,0029,002b,002d,0033,44cd,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601", + "peetprint": "GREASE-772-771|2-1.1|GREASE-4588-29-23-24|1027-2052-1025-1283-2053-1281-2054-1537|1|2|GREASE-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53|0-10-11-13-16-17613-18-23-27-35-41-43-45-5-51-65037-65281-GREASE-GREASE", + "peetprint_hash": "d44d68f0fce54cd423d6792272a242b8", + "client_random": "d6efb21f7beaf64a320bf951965bbd738d8d5ea4eb25dcfff62c1d836076be97", + "session_id": "640be04b6233f91902d24076700b85bdf7ac53453930e47d87c6780b938eabbc" + }, + "http2": { + "akamai_fingerprint": "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p", + "akamai_fingerprint_hash": "52d84b11737d980aef856699f885ca86", + "sent_frames": [ + { + "frame_type": "SETTINGS", + "length": 24, + "settings": [ + "HEADER_TABLE_SIZE = 65536", + "ENABLE_PUSH = 0", + "INITIAL_WINDOW_SIZE = 6291456", + "MAX_HEADER_LIST_SIZE = 262144" + ] + }, + { + "frame_type": "WINDOW_UPDATE", + "length": 4, + "increment": 15663105 + }, + { + "frame_type": "HEADERS", + "stream_id": 1, + "length": 501, + "headers": [ + ":method: GET", + ":authority: tls.peet.ws", + ":scheme: https", + ":path: /api/all", + "sec-ch-ua: \"Chromium\";v=\"148\", \"Google Chrome\";v=\"148\", \"Not/A)Brand\";v=\"99\"", + "sec-ch-ua-mobile: ?0", + "sec-ch-ua-platform: \"macOS\"", + "dnt: 1", + "upgrade-insecure-requests: 1", + "user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36", + "sec-purpose: prefetch;prerender", + "accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "sec-fetch-site: none", + "sec-fetch-mode: navigate", + "sec-fetch-user: ?1", + "sec-fetch-dest: document", + "accept-encoding: gzip, deflate, br, zstd", + "accept-language: en,zh-CN;q=0.9,zh;q=0.8", + "priority: u=0, i" + ], + "flags": [ + "EndStream (0x1)", + "EndHeaders (0x4)", + "Priority (0x20)" + ], + "priority": { + "weight": 256, + "depends_on": 0, + "exclusive": 1 + } + } + ] + } +} \ No newline at end of file From 47f83e453f998f9e11a826eb9313f5cd15920ac3 Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Tue, 19 May 2026 11:32:42 +0800 Subject: [PATCH 3/7] fmt --- lib/wreq_ruby/emulate.rb | 5 +++++ src/emulate.rs | 3 ++- test/emulate_test.rb | 3 +-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/wreq_ruby/emulate.rb b/lib/wreq_ruby/emulate.rb index 2af9147..f12ba45 100644 --- a/lib/wreq_ruby/emulate.rb +++ b/lib/wreq_ruby/emulate.rb @@ -52,6 +52,7 @@ class EmulationDevice Chrome145 = nil Chrome146 = nil Chrome147 = nil + Edge101 = nil Edge122 = nil Edge127 = nil @@ -70,6 +71,7 @@ class EmulationDevice Edge145 = nil Edge146 = nil Edge147 = nil + Firefox109 = nil Firefox117 = nil Firefox128 = nil @@ -88,6 +90,7 @@ class EmulationDevice Firefox147 = nil Firefox148 = nil Firefox149 = nil + SafariIos17_2 = nil SafariIos17_4_1 = nil SafariIos16_5 = nil @@ -115,6 +118,7 @@ class EmulationDevice SafariIos26_2 = nil SafariIPad26 = nil SafariIpad26_2 = nil + OkHttp3_9 = nil OkHttp3_11 = nil OkHttp3_13 = nil @@ -123,6 +127,7 @@ class EmulationDevice OkHttp4_10 = nil OkHttp4_12 = nil OkHttp5 = nil + Opera116 = nil Opera117 = nil Opera118 = nil diff --git a/src/emulate.rs b/src/emulate.rs index 423adde..b22a457 100644 --- a/src/emulate.rs +++ b/src/emulate.rs @@ -643,7 +643,6 @@ mod parse { let sent_frames = get_and_then!(json, http2, as_object, sent_frames, as_array)?; let mut http2_builder = Http2Options::builder(); - let mut headers_map = HeaderMap::new(); // parse settings frame if let Some(settings) = find_and_then!( @@ -731,6 +730,8 @@ mod parse { http2_builder.initial_connection_window_size((window_update + 65535) as u32); } + let mut headers_map = HeaderMap::new(); + // parse headers frame if let Some(headers_frame) = find!(sent_frames, frame_type, as_str, HEADERS) { // parse initial stream id diff --git a/test/emulate_test.rb b/test/emulate_test.rb index 81b9299..51e20c0 100644 --- a/test/emulate_test.rb +++ b/test/emulate_test.rb @@ -20,8 +20,7 @@ def test_all_emulation_os_constants_are_non_nil end def test_http2_parser - - profiles = ['test/results/chrome_147.json', 'test/results/chrome_148.json'] + profiles = ["test/results/chrome_147.json", "test/results/chrome_148.json"] profiles.each do |profile| str = File.read(profile) From e1c6129f6294d9a2d93a278c774ac18f1f09ced9 Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Wed, 20 May 2026 09:01:16 +0800 Subject: [PATCH 4/7] feat(http2): record http2 header order --- src/emulate.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/emulate.rs b/src/emulate.rs index b22a457..2f57659 100644 --- a/src/emulate.rs +++ b/src/emulate.rs @@ -228,8 +228,11 @@ impl Emulation { builder = builder.tls_options(tls_options); } - if let Some((http2_options, headers)) = parse::parse_http2(&json) { - builder = builder.http2_options(http2_options).headers(headers); + if let Some((http2_options, headers, orig_headers)) = parse::parse_http2(&json) { + builder = builder + .http2_options(http2_options) + .headers(headers) + .orig_headers(orig_headers); } Ok(Self::Emulation(Box::new(builder.build()))) @@ -405,6 +408,7 @@ mod parse { use serde_json::Value; use std::str::FromStr; use wreq::{ + header::OrigHeaderMap, http2::{ Http2Options, PseudoId, PseudoOrder, SettingId, SettingsOrder, StreamDependency, StreamId, @@ -639,7 +643,7 @@ mod parse { Some(tls_builder.build()) } - pub fn parse_http2(json: &Value) -> Option<(Http2Options, HeaderMap)> { + pub fn parse_http2(json: &Value) -> Option<(Http2Options, HeaderMap, OrigHeaderMap)> { let sent_frames = get_and_then!(json, http2, as_object, sent_frames, as_array)?; let mut http2_builder = Http2Options::builder(); @@ -731,6 +735,7 @@ mod parse { } let mut headers_map = HeaderMap::new(); + let mut orig_headers_map = OrigHeaderMap::new(); // parse headers frame if let Some(headers_frame) = find!(sent_frames, frame_type, as_str, HEADERS) { @@ -772,7 +777,8 @@ mod parse { if let (Ok(header_name), Ok(header_value)) = (HeaderName::from_str(name), HeaderValue::from_str(value)) { - headers_map.insert(header_name, header_value); + headers_map.insert(&header_name, header_value); + orig_headers_map.insert(header_name); } } } @@ -801,6 +807,6 @@ mod parse { } } - Some((http2_builder.build(), headers_map)) + Some((http2_builder.build(), headers_map, orig_headers_map)) } } From 9bc8e77b8598afd996b6fca9e7cce648a0150263 Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Wed, 20 May 2026 21:53:57 +0800 Subject: [PATCH 5/7] Update emulate_test.rb --- test/emulate_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/emulate_test.rb b/test/emulate_test.rb index 51e20c0..988a95b 100644 --- a/test/emulate_test.rb +++ b/test/emulate_test.rb @@ -19,7 +19,7 @@ def test_all_emulation_os_constants_are_non_nil end end - def test_http2_parser + def test_chrome_parser profiles = ["test/results/chrome_147.json", "test/results/chrome_148.json"] profiles.each do |profile| From 9000526cdab78eda17eca284218b09b00980ef6c Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Thu, 21 May 2026 07:50:45 +0800 Subject: [PATCH 6/7] fix(emulate): fix missing order when parsing JSON TLS extensions --- Cargo.lock | 1 + Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 71a836e..5e39a50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1011,6 +1011,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ + "indexmap", "itoa", "memchr", "serde", diff --git a/Cargo.toml b/Cargo.toml index a7c2fef..1931682 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ wreq = { version = "6.0.0-rc.28", features = [ ] } wreq-util = "3.0.0-rc.11" serde = { version = "1.0.228", features = ["derive"] } -serde_json = "1.0" +serde_json = { version = "1.0", features = ["preserve_order"] } serde_magnus = "0.11.0" indexmap = { version = "2.14.0", features = ["serde"] } cookie = "0.18.1" From b7d02475526b4c78283f5482892be9d1af8ada42 Mon Sep 17 00:00:00 2001 From: 0x676e67 Date: Thu, 21 May 2026 08:59:43 +0800 Subject: [PATCH 7/7] test(emulate): update tests to support TLS/HTTP2 fingerprints for Edge and Opera --- test/emulate_test.rb | 44 ++++++++ test/results/edge_148.json | 210 ++++++++++++++++++++++++++++++++++++ test/results/opera_131.json | 210 ++++++++++++++++++++++++++++++++++++ 3 files changed, 464 insertions(+) create mode 100644 test/results/edge_148.json create mode 100644 test/results/opera_131.json diff --git a/test/emulate_test.rb b/test/emulate_test.rb index 988a95b..3832a1b 100644 --- a/test/emulate_test.rb +++ b/test/emulate_test.rb @@ -40,4 +40,48 @@ def test_chrome_parser assert_includes resp.bytes, "52d84b11737d980aef856699f885ca86" end end + + def test_edge_parser + profiles = ["test/results/edge_148.json"] + + profiles.each do |profile| + str = File.read(profile) + json = JSON.parse(str) + emulation = Wreq::Emulation.parse(JSON.dump(json), permute_extensions: true) + client = Wreq::Client.new(emulation: emulation) + resp = client.get("https://tls.peet.ws/api/all") + # ja4(no psk) + assert_includes resp.bytes, "t13d1516h2_8daaf6152771_d8a2da3f94cd" + # akamai + assert_includes resp.bytes, "52d84b11737d980aef856699f885ca86" + + resp = client.get("https://tls.peet.ws/api/all") + # ja4(psk) + assert_includes resp.bytes, "t13d1517h2_8daaf6152771_b6f405a00624" + # akamai + assert_includes resp.bytes, "52d84b11737d980aef856699f885ca86" + end + end + + def test_opera_parser + profiles = ["test/results/opera_131.json"] + + profiles.each do |profile| + str = File.read(profile) + json = JSON.parse(str) + emulation = Wreq::Emulation.parse(JSON.dump(json), permute_extensions: true) + client = Wreq::Client.new(emulation: emulation) + resp = client.get("https://tls.peet.ws/api/all") + # ja4(no psk) + assert_includes resp.bytes, "t13d1516h2_8daaf6152771_d8a2da3f94cd" + # akamai + assert_includes resp.bytes, "52d84b11737d980aef856699f885ca86" + + resp = client.get("https://tls.peet.ws/api/all") + # ja4(psk) + assert_includes resp.bytes, "t13d1517h2_8daaf6152771_b6f405a00624" + # akamai + assert_includes resp.bytes, "52d84b11737d980aef856699f885ca86" + end + end end diff --git a/test/results/edge_148.json b/test/results/edge_148.json new file mode 100644 index 0000000..85d8126 --- /dev/null +++ b/test/results/edge_148.json @@ -0,0 +1,210 @@ +{ + "donate": "Please consider donating to keep this API running. Visit https://tls.peet.ws", + "ip": "142.171.157.68:42540", + "http_version": "h2", + "method": "GET", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0", + "tls": { + "ciphers": [ + "TLS_GREASE (0x7A7A)", + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA" + ], + "extensions": [ + { + "name": "TLS_GREASE (0x8a8a)" + }, + { + "name": "signature_algorithms (13)", + "signature_algorithms": [ + "ecdsa_secp256r1_sha256", + "rsa_pss_rsae_sha256", + "rsa_pkcs1_sha256", + "ecdsa_secp384r1_sha384", + "rsa_pss_rsae_sha384", + "rsa_pkcs1_sha384", + "rsa_pss_rsae_sha512", + "rsa_pkcs1_sha512" + ] + }, + { + "name": "application_settings (17613)", + "protocols": [ + "h2" + ] + }, + { + "name": "extensionEncryptedClientHello (boringssl) (65037)", + "data": "0000010001960020888e1ebac8955a8ccad9c4efbae82fa0087388d3ed275aa894c108efab40eb1200b0d2aaed0c4c8bb4c9f1826c6e0e1220fc225276928b31586c024d6f1abe3788cd78a4d5dfbbac146b87b14dbe98bf74d229fbdea3ddd37204b31930df058878eb6f2a3aa94901e580b846f3ee0c5b107b065a5638cdf488d4acac97321d63a9ed8b697b207c7f1a7b13347438ed663c2e761e3232e663642ead9f865759fc97428fe5f20bf44150ab080300c47eebed875511e12ab894bb341fc876f650b6ad12553631cb77fdd199a976af244096c19f" + }, + { + "name": "psk_key_exchange_modes (45)", + "PSK_Key_Exchange_Mode": "PSK with (EC)DHE key establishment (psk_dhe_ke) (1)" + }, + { + "name": "extensionRenegotiationInfo (boringssl) (65281)", + "data": "00" + }, + { + "name": "key_share (51)", + "shared_keys": [ + { + "TLS_GREASE (0xcaca)": "00" + }, + { + "X25519MLKEM768 (4588)": "dab4390c7c6730a03dfd498a7227bbb30491ad38033c0a9d1ad9226df04b385b9272a97a8573a002da8a58fabdc385084c63351f1cb4847c9ab6d14f0f9301796527a8708cbd49a886a31990285336a7b0d5379c5be91af77130df53084055b269f28d6ab7b7f6f41d4653a5ad9561ae53a0baa466e2333d8fc01801986dc2658b0bfb1da5e990c473563c8722c7199a22cc3b7e665a69d1663bb4760354b6f767439d527b212426adf238ef92b1dda00ca749479c536350b8be5f7c4ca6d5788ad7648e96c337447517662bdef35df789c7b0798e5c4870d65722a9420896d757148901574c1895d5523e10782ab9baa8608e94f2880ed390761a42116312e06045f8622da748b9ebf05a7a1a99a7b82b2527ba362c0d99b8c81808456e05c3e3d3890daa68a64a06269838e2560ce12c4e3f833961a31c4e220d10d1250965bb81c6452bb676d92a9d671988592943e491a37271ae4ff38c267269d3ebcba23715fbf265f1eb580ae765b98432bcd24267511b1268871732072ca1cc7a681e77514d30265ce452b10a9c285691cf809a7067423910f65c319cc2d3236c1a67bb2c98a062541ec4b4650b0322f86a97bd26b438f271954593367c32dd65c3dfec5832b08af1ca5973aa2fef966b0fa483c16a80e0a84a60c054a8c797680197701ca2d982aec5332ad63b2961d6688f9458876a03f05c96aca6addccb1c72ac3a667558e9009743c6baddd0c38a8badebd514b4f974dd619c15647eeb00014a818a84d243c8c5b00e6c1e844663989a5e225b52ace5bb77d74cd0130b3aba2b9dd6968691c498323fae017dec496a06a2282e299ebb647dee6706acd42026ba21524a423a911129baa93af70630d44b4e846c1c8b98f3779720d95a9ea4594eaac96ee529bda0ad5a776f7e1399e7703a2cac6932e71c61551fe130832c179807da35faa9b09acca49286c12eab39905782376a187f45c981774162467a04404e8b473ce6491eec888a337393e9798adfe972c9aacd10e2738953a918196ea6b937644003e4fa6b92549d386ccfab654a01896e2808b9b89bb42da768e72056de33b4e0e7898f877824c47b40c279d374130fcc3e40fb216051c964a32bbc672f90260039d9a67d89166cb100bf69a160261ee5a209aaf818962a2edef0a0b99a9f4e212e26ab5289eb71d7e78937a6c1d0e42e50411417440a538c21c84c82af02b68f7659052a5dab191de74568790157aba2347da92c9d37be4b995b70c7b02f1b21a513cf54954d78512e38d18242ca5437f49ec694aace89483e99ce1be189d212c41014b8c383af78e186310b4cd8f266c6235d58f44e3c93462a7727be7257b9bba1fc6405485375f45b62b3394056928908a52303974d430c7d61423039bc0abeb51ab87b2535d3545e483a7f2c969a311c6627624ca1bbf7700ad5502064034a550cb1591a9271b23fcc3814df6562e096c7258a9843e60bf7eb0569abbba46280cc332ca9eb012e0ca74c949e5d3958bc5c7eaefcccf2180f59f3729e5a0189984dd938b3c951c853954deac215257cb219933cfd1668280282b2054c8f3146bb4312aea2b89cebc2cb957c034a238c298f63f2cbf715c803f424ea3acc1d87e0d35e1d00664c73f5a457bbab09cb763e022bc0b9b75910712b8029f174e291f3114c9cd16eaf705603aa6e47a20b2ada967231a5c049e114f97c" + }, + { + "X25519 (29)": "50dc1dd767001f4a753cf15499d84e0b9aab2c077e6fd391f594c8837c005019" + } + ] + }, + { + "name": "signed_certificate_timestamp (18)" + }, + { + "name": "ec_point_formats (11)", + "elliptic_curves_point_formats": [ + "0x00" + ] + }, + { + "name": "supported_groups (10)", + "supported_groups": [ + "TLS_GREASE (0xcaca)", + "X25519MLKEM768 (4588)", + "X25519 (29)", + "P-256 (23)", + "P-384 (24)" + ] + }, + { + "name": "supported_versions (43)", + "versions": [ + "TLS_GREASE (0x8a8a)", + "TLS 1.3", + "TLS 1.2" + ] + }, + { + "name": "compress_certificate (27)", + "algorithms": [ + "brotli (2)" + ] + }, + { + "name": "application_layer_protocol_negotiation (16)", + "protocols": [ + "h2", + "http/1.1" + ] + }, + { + "name": "session_ticket (35)", + "data": "" + }, + { + "name": "server_name (0)", + "server_name": "tls.peet.ws" + }, + { + "name": "status_request (5)", + "status_request": { + "certificate_status_type": "OSCP (1)", + "responder_id_list_length": 0, + "request_extensions_length": 0 + } + }, + { + "name": "extended_master_secret (23)", + "master_secret_data": "", + "extended_master_secret_data": "" + }, + { + "name": "TLS_GREASE (0xdada)" + }, + { + "name": "pre_shared_key (41)", + "data": "00770071bc720a9a949145a5444f3b3a3df3240b8ddb488dbc1beb257e44a30df3027610049cd3fbe55d8cbfa5d4f679037817465766bd2edac72c46f33f6a668f04c723a9e4d404acd6421076fbf8cdf8b8e435befc9570a73722f39319c754d954d47040b2aa3921d7f45a6afba5de337a9668fb59a1fcd7002120aad5ff622a0f1e03a55f381249ced5823fd8d86a6467580d5c9bf6a9c59d11e7" + } + ], + "tls_version_record": "771", + "tls_version_negotiated": "772", + "ja3": "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,13-17613-65037-45-65281-51-18-11-10-43-27-16-35-0-5-23,4588-29-23-24,0", + "ja3_hash": "9ca91f0991f36c6485cbdcb9dacd5a11", + "ja4": "t13d1516h2_8daaf6152771_d8a2da3f94cd", + "ja4_r": "t13d1516h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0005,000a,000b,000d,0012,0017,001b,0023,002b,002d,0033,44cd,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601", + "peetprint": "GREASE-772-771|2-1.1|GREASE-4588-29-23-24|1027-2052-1025-1283-2053-1281-2054-1537|1|2|GREASE-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53|0-10-11-13-16-17613-18-23-27-35-43-45-5-51-65037-65281-GREASE-GREASE", + "peetprint_hash": "1d4ffe9b0e34acac0bd883fa7f79d7b5", + "client_random": "9ff973b0b29555d829ca662d1f3ca4a0aac0d84b8ca39ad8f3dc4e05eb582aa6", + "session_id": "1e3cfc410988fe1daed453507262aece9e4e4c098227f53268ea3743bc013e2c" + }, + "http2": { + "akamai_fingerprint": "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p", + "akamai_fingerprint_hash": "52d84b11737d980aef856699f885ca86", + "sent_frames": [ + { + "frame_type": "SETTINGS", + "length": 24, + "settings": [ + "HEADER_TABLE_SIZE = 65536", + "ENABLE_PUSH = 0", + "INITIAL_WINDOW_SIZE = 6291456", + "MAX_HEADER_LIST_SIZE = 262144" + ] + }, + { + "frame_type": "WINDOW_UPDATE", + "length": 4, + "increment": 15663105 + }, + { + "frame_type": "HEADERS", + "stream_id": 1, + "length": 500, + "headers": [ + ":method: GET", + ":authority: tls.peet.ws", + ":scheme: https", + ":path: /api/all", + "sec-ch-ua: \"Chromium\";v=\"148\", \"Microsoft Edge\";v=\"148\", \"Not/A)Brand\";v=\"99\"", + "sec-ch-ua-mobile: ?0", + "sec-ch-ua-platform: \"macOS\"", + "upgrade-insecure-requests: 1", + "user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36 Edg/148.0.0.0", + "accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "sec-fetch-site: none", + "sec-fetch-mode: navigate", + "sec-fetch-user: ?1", + "sec-fetch-dest: document", + "accept-encoding: gzip, deflate, br, zstd", + "accept-language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6", + "priority: u=0, i" + ], + "flags": [ + "EndStream (0x1)", + "EndHeaders (0x4)", + "Priority (0x20)" + ], + "priority": { + "weight": 256, + "depends_on": 0, + "exclusive": 1 + } + } + ] + } +} \ No newline at end of file diff --git a/test/results/opera_131.json b/test/results/opera_131.json new file mode 100644 index 0000000..1920fc4 --- /dev/null +++ b/test/results/opera_131.json @@ -0,0 +1,210 @@ +{ + "donate": "Please consider donating to keep this API running. Visit https://tls.peet.ws", + "ip": "142.171.157.68:39164", + "http_version": "h2", + "method": "GET", + "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 OPR/131.0.0.0", + "tls": { + "ciphers": [ + "TLS_GREASE (0x5A5A)", + "TLS_AES_128_GCM_SHA256", + "TLS_AES_256_GCM_SHA384", + "TLS_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", + "TLS_RSA_WITH_AES_128_GCM_SHA256", + "TLS_RSA_WITH_AES_256_GCM_SHA384", + "TLS_RSA_WITH_AES_128_CBC_SHA", + "TLS_RSA_WITH_AES_256_CBC_SHA" + ], + "extensions": [ + { + "name": "TLS_GREASE (0xfafa)" + }, + { + "name": "application_layer_protocol_negotiation (16)", + "protocols": [ + "h2", + "http/1.1" + ] + }, + { + "name": "ec_point_formats (11)", + "elliptic_curves_point_formats": [ + "0x00" + ] + }, + { + "name": "supported_versions (43)", + "versions": [ + "TLS_GREASE (0x7a7a)", + "TLS 1.3", + "TLS 1.2" + ] + }, + { + "name": "extended_master_secret (23)", + "master_secret_data": "", + "extended_master_secret_data": "" + }, + { + "name": "status_request (5)", + "status_request": { + "certificate_status_type": "OSCP (1)", + "responder_id_list_length": 0, + "request_extensions_length": 0 + } + }, + { + "name": "extensionRenegotiationInfo (boringssl) (65281)", + "data": "00" + }, + { + "name": "key_share (51)", + "shared_keys": [ + { + "TLS_GREASE (0x4a4a)": "00" + }, + { + "X25519MLKEM768 (4588)": "4af038d4f22f258ab43cc05900727fa1c239a24561fe87995701364f5c14272b78489328991cb9996104540800354bab1b8943160728f9f37b4ed985d97c7f19f600f84a25c4260ac4140a48215479b9a19ceb36cc937657c32cd3a5bd05a67ec1264284c74ae5981b3e47abde0981baebb4e252529127855be0c4d67aa45071a7ea685100b54522e267ae30c7368a021e092e237cc786c3526929301013357d89b7096b8dc5c537c4baa21925ae2eb07ab24694c31535d01a61cb983689db256d585bd3f2c3d0aa15767a57ad55aa1722944224030b96b84bfa0ac35264aca5149f61950488446e54b74aaac73d155f2836b61cbb9844a73f393464592b621832bcb0817b7057c122e8b57f91401ab94fa8d13d29a78104c12a3f6c304c93ad9076510c88cee7927ce9eb1f8ae98cda144c52bcc2513b7b233c7af86c094fe1bddfb78e50a7c851f0763f5a947cd322181030c85260bc33ab070c7de4c092a1a82c5dc386800b633886c3fda656e44aa324905336490560fc32740641a4982de9a90257133960f12aaba74e27941cb9778f86b45327d3c390830222912bbbd577135c5b20ba7e88305e17cc4a3c175a37b7b6dc247c9fc2c1794079a11003788849da48b09e1209a489bc6288c6514920ad87940582527503834e4cc88bc0410ab15aaa6638dd01502f54326a70b0b4063fb6e2415877444bb50ce8588dd4bbc0179210c1068ea0004e6dd6ab166a9070a04be083364238afae12c2fecca3bb40a46f499b09f3b29175acb7c9ae2a040b9816336d042b4d3aac810104c56abbdec22185f0ce48fb0e34c7af17a0748a3a6a90418cbd83797098719d9b8fe151a997962c6db616668bbb15c6532918954ff092642c14ac84505b8cae6473b42de3a0b5d4ba383a3c01836320897a8d02119fd730979519e9da923121ad18d178a2f78012c4bf8db847d4a80d2c9843aea0c70917c95ee5164321912c2a7e7df1bc4452b2c8a7762aab93c1ba9830c2b44f22aba9c85bec433d0e4a36983543393837c6bb1495b688c78821e0ca3943a75bc539c991a85ece00c37c299eecc65eaee66f8c4ab320eb98f3a274c7175f97ab3de84a3086c5b352fa17c9a3acb0f5c9dc990d1513771528cdda3775999484428a5f1a83232be552db4aa10ac1ac677369c1655648f7c88926af70b94a93a062a0f34495a0b9e259835f2303f9ab6abc9817ae300e99015fd3655e0a85b69d78377df56f49d6c755ba86a6a02f389b15dbeb8e9882b739766f71092c91dc1e0f60b99b390b470950588cca9e77450dec3a9c50756cbb8b229a4c37910e05fb67a9acc92d042060ecc0f3680c6194a7d37bc92f378aa6eb8641095625c16dccf2312c8bacb95628de241704ec84aa497a67e9ccc2a884adc5289f22be59b651fd71abe42a02409c63faea3054eb2e913a5ce0508f70751e877b4e3929c669e998ac821dc309c0d38a69a5d375b14ac8fca6ccc5f4caf97cb39cc7048b570122cbb9f3c550094569875820b199873ba947a6875aace11e4493732047bf2b43840a6b9f5b0c69fd5a64f4986c80eb8e02644b4b692b48526bfa78b90874372c5c5b0900c04c1a4f7a0a3063283692f32d84dc37524b264761cf4d35d9b892e76b179c8aab54d7485788a6dac2c34acf561cfac10bc01311dc2f1237bba7e97eb77deaca556df6eca0eb8e764c11b23778" + }, + { + "X25519 (29)": "6ef52cd6445ad422f9e955a76a7c170671d65c490e0c85fe23ada51f26bb605b" + } + ] + }, + { + "name": "signed_certificate_timestamp (18)" + }, + { + "name": "signature_algorithms (13)", + "signature_algorithms": [ + "ecdsa_secp256r1_sha256", + "rsa_pss_rsae_sha256", + "rsa_pkcs1_sha256", + "ecdsa_secp384r1_sha384", + "rsa_pss_rsae_sha384", + "rsa_pkcs1_sha384", + "rsa_pss_rsae_sha512", + "rsa_pkcs1_sha512" + ] + }, + { + "name": "compress_certificate (27)", + "algorithms": [ + "brotli (2)" + ] + }, + { + "name": "supported_groups (10)", + "supported_groups": [ + "TLS_GREASE (0x4a4a)", + "X25519MLKEM768 (4588)", + "X25519 (29)", + "P-256 (23)", + "P-384 (24)" + ] + }, + { + "name": "psk_key_exchange_modes (45)", + "PSK_Key_Exchange_Mode": "PSK with (EC)DHE key establishment (psk_dhe_ke) (1)" + }, + { + "name": "extensionEncryptedClientHello (boringssl) (65037)", + "data": "0000010001b00020b3ccbdac47864a302aad479e976bb23a41c6d38c138ebdd007328ca4811c710e009018a73e49b0ce8a2432d1037493b4c150862ef246e4e0023cf76b0be3209a1fce506e04829217e91dc884aa45b0b257c5da61d2b3faad76b7867c6a78360aabff8febf742531f33c2ae52ee3bb506da1787144fc9e4243569b1a030b4cef1519b1b063105e94d513a08986009ff1affcc5fe910612dffbbac4e980ac365108374db49420f621df9eabfbe42a6ca8d7d25" + }, + { + "name": "server_name (0)", + "server_name": "tls.peet.ws" + }, + { + "name": "application_settings (17613)", + "protocols": [ + "h2" + ] + }, + { + "name": "session_ticket (35)", + "data": "" + }, + { + "name": "TLS_GREASE (0x3a3a)" + }, + { + "name": "pre_shared_key (41)", + "data": "00770071bc720a9a949145a5444f3b3a3df3240b8439557e4c5f242b9ba6b5f9bdf105d851abddca9d7fe0286ae2589a073458efce1199396f0da8291b8983929d0c0fd2b6081071a6cc683cc1a7eaedc2a87858ff38fec9272a7f58c4422a269b5de270a53d9eb11eea4466de0dbe8a58dea40cb281267e80002120588b489981ed53454d820e243e5ea755b1c8b398d3897d292fd00fc43dbd00f6" + } + ], + "tls_version_record": "771", + "tls_version_negotiated": "772", + "ja3": "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,16-11-43-23-5-65281-51-18-13-27-10-45-65037-0-17613-35,4588-29-23-24,0", + "ja3_hash": "9507fb88f7f9d338b7763282dbb33681", + "ja4": "t13d1516h2_8daaf6152771_d8a2da3f94cd", + "ja4_r": "t13d1516h2_002f,0035,009c,009d,1301,1302,1303,c013,c014,c02b,c02c,c02f,c030,cca8,cca9_0005,000a,000b,000d,0012,0017,001b,0023,002b,002d,0033,44cd,fe0d,ff01_0403,0804,0401,0503,0805,0501,0806,0601", + "peetprint": "GREASE-772-771|2-1.1|GREASE-4588-29-23-24|1027-2052-1025-1283-2053-1281-2054-1537|1|2|GREASE-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53|0-10-11-13-16-17613-18-23-27-35-43-45-5-51-65037-65281-GREASE-GREASE", + "peetprint_hash": "1d4ffe9b0e34acac0bd883fa7f79d7b5", + "client_random": "6c255f054b9dae0c74aec24950706c61a41c2f6857224e44e82e5c6b46d8998c", + "session_id": "17b7bfecab259f8b68971c801a18587ab93f21c647ef5d3058c5fb089d196370" + }, + "http2": { + "akamai_fingerprint": "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p", + "akamai_fingerprint_hash": "52d84b11737d980aef856699f885ca86", + "sent_frames": [ + { + "frame_type": "SETTINGS", + "length": 24, + "settings": [ + "HEADER_TABLE_SIZE = 65536", + "ENABLE_PUSH = 0", + "INITIAL_WINDOW_SIZE = 6291456", + "MAX_HEADER_LIST_SIZE = 262144" + ] + }, + { + "frame_type": "WINDOW_UPDATE", + "length": 4, + "increment": 15663105 + }, + { + "frame_type": "HEADERS", + "stream_id": 1, + "length": 466, + "headers": [ + ":method: GET", + ":authority: tls.peet.ws", + ":scheme: https", + ":path: /api/all", + "sec-ch-ua: \"Opera\";v=\"131\", \"Not.A/Brand\";v=\"8\", \"Chromium\";v=\"147\"", + "sec-ch-ua-mobile: ?0", + "sec-ch-ua-platform: \"macOS\"", + "upgrade-insecure-requests: 1", + "user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 OPR/131.0.0.0", + "accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", + "sec-fetch-site: none", + "sec-fetch-mode: navigate", + "sec-fetch-user: ?1", + "sec-fetch-dest: document", + "accept-encoding: gzip, deflate, br, zstd", + "accept-language: zh-CN,zh;q=0.9", + "priority: u=0, i" + ], + "flags": [ + "EndStream (0x1)", + "EndHeaders (0x4)", + "Priority (0x20)" + ], + "priority": { + "weight": 256, + "depends_on": 0, + "exclusive": 1 + } + } + ] + } +} \ No newline at end of file