├── .gitignore ├── .github ├── workflows │ └── rust.yml └── dependabot.yml ├── Dockerfile ├── src ├── error.rs ├── cagen.rs ├── proxy │ ├── client.rs │ ├── mod.rs │ ├── rewind.rs │ ├── handler.rs │ ├── ca.rs │ └── mitm.rs ├── main.rs ├── serve.rs └── daemon.rs ├── LICENSE ├── Cargo.toml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | /target 3 | ca -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM rust:alpine3.20 AS builder 3 | 4 | # Install build dependencies 5 | RUN apk add --no-cache musl-dev upx 6 | 7 | # Set the working directory 8 | WORKDIR /app 9 | 10 | # Copy the project files 11 | COPY . . 12 | 13 | # Build the project 14 | RUN cargo build --release 15 | RUN upx /app/target/release/devicecheck 16 | 17 | # Runtime stage 18 | FROM alpine:3.16 19 | 20 | # Copy the built binary from the builder stage 21 | COPY --from=builder /app/target/release/devicecheck /bin/devicecheck 22 | 23 | # Set the entrypoint 24 | ENTRYPOINT ["/bin/devicecheck"] -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use rcgen::Error as RcgenError; 2 | use std::io; 3 | use thiserror::Error; 4 | 5 | #[derive(Debug, Error)] 6 | pub enum Error { 7 | #[error(transparent)] 8 | Tls(#[from] RcgenError), 9 | 10 | #[error(transparent)] 11 | HyperError(#[from] hyper::Error), 12 | 13 | #[error(transparent)] 14 | BodyErrpr(#[from] http::Error), 15 | 16 | #[error(transparent)] 17 | RequestConnectError(#[from] reqwest::Error), 18 | 19 | #[error(transparent)] 20 | IO(#[from] io::Error), 21 | 22 | #[error(transparent)] 23 | SerdeError(#[from] serde_json::Error), 24 | } 25 | -------------------------------------------------------------------------------- /src/cagen.rs: -------------------------------------------------------------------------------- 1 | use rcgen::Certificate; 2 | 3 | use std::{fs, path::Path}; 4 | 5 | use crate::proxy::CertificateAuthority; 6 | 7 | pub fn gen_ca>(ca: T, key: T) -> Certificate { 8 | let cert = CertificateAuthority::gen_ca().expect("generate cert"); 9 | let cert_crt = cert.serialize_pem().unwrap(); 10 | 11 | fs::create_dir("ca").unwrap(); 12 | 13 | println!("{}", cert_crt); 14 | if let Err(err) = fs::write(ca, cert_crt) { 15 | tracing::error!("cert file write failed: {}", err); 16 | } 17 | 18 | let private_key = cert.serialize_private_key_pem(); 19 | println!("{}", private_key); 20 | if let Err(err) = fs::write(key, private_key) { 21 | tracing::error!("private key file write failed: {}", err); 22 | } 23 | 24 | cert 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Penumbria-X 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/proxy/client.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use http::{response::Builder, Request, Response}; 3 | use hyper::Body; 4 | use reqwest::{redirect::Policy, Client, Url}; 5 | 6 | #[derive(Clone)] 7 | pub struct HttpClient { 8 | inner: Client, 9 | } 10 | 11 | impl HttpClient { 12 | pub fn new(proxy: Option) -> Result { 13 | let mut builder = Client::builder(); 14 | 15 | if let Some(proxy) = proxy { 16 | let proxy = reqwest::Proxy::all(proxy)?; 17 | builder = builder.proxy(proxy); 18 | } 19 | 20 | builder 21 | .redirect(Policy::none()) 22 | .build() 23 | .map(|inner| Self { inner }) 24 | .map_err(Into::into) 25 | } 26 | 27 | pub async fn http(&self, req: Request) -> Result, Error> { 28 | let (parts, body) = req.into_parts(); 29 | 30 | // Send request 31 | let mut resp = self 32 | .inner 33 | .request(parts.method, parts.uri.to_string()) 34 | .headers(parts.headers) 35 | .body(reqwest::Body::wrap_stream(body)) 36 | .send() 37 | .await?; 38 | 39 | // Create response builder 40 | let mut builder = Builder::new() 41 | .status(resp.status()) 42 | .version(resp.version()) 43 | .extension(parts.extensions); 44 | 45 | // Move headers 46 | builder 47 | .headers_mut() 48 | .map(|headers| headers.extend(std::mem::take(resp.headers_mut()))); 49 | 50 | // Build response 51 | builder 52 | .body(Body::wrap_stream(resp.bytes_stream())) 53 | .map_err(Into::into) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "devicecheck" 3 | description = "chatgpt preauth devicecheck server" 4 | version = "1.0.2" 5 | edition = "2021" 6 | rust-version = "1.75" 7 | license = "MIT" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | anyhow = "1" 13 | thiserror = "1" 14 | serde = { version = "1", features = ["derive"]} 15 | serde_json = "1" 16 | typed-builder = "0.20.0" 17 | time = "0.3.36" 18 | rand = "0.8.5" 19 | moka = { version = "0.12.8", default-features = false, features = ["sync"] } 20 | tokio = { version = "1.40.0", default-features = false, features = ["macros", "signal", "rt-multi-thread"] } 21 | hyper = { version = "0.14", features = ["client", "http1", "server", "tcp", "stream"] } 22 | bytes = "1.7.2" 23 | http = "0.2.12" 24 | 25 | clap = { version = "4", features = ["derive", "env"] } 26 | futures-util = "0.3" 27 | 28 | # ca 29 | rcgen = { version = "0.12.1", features = ["x509-parser"] } 30 | tokio-rustls = { version = "0.24.1", default-features = false, features = ["tls12"] } 31 | rustls = { version = "0.21.8", features = ["dangerous_configuration"] } 32 | rustls-pemfile = "1.0.4" 33 | 34 | # client 35 | reqwest = { version ="0.11", default-features = false, features = ["stream", "socks", "json", "cookies", "rustls-tls"]} 36 | 37 | # alloc 38 | mimalloc = { version = "0.1.39", default-features = false } 39 | 40 | # log 41 | tracing = { version = "0.1.40" } 42 | tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } 43 | 44 | [target.'cfg(target_family = "unix")'.dependencies] 45 | daemonize = "0.5.0" 46 | nix = { version = "0.27.1", features = ["signal", "user", "ptrace"]} 47 | 48 | [profile.release] 49 | lto = true 50 | opt-level = 'z' 51 | codegen-units = 1 52 | strip = true 53 | panic = "abort" -------------------------------------------------------------------------------- /src/proxy/mod.rs: -------------------------------------------------------------------------------- 1 | mod ca; 2 | mod client; 3 | pub mod handler; 4 | mod mitm; 5 | mod rewind; 6 | 7 | use self::client::HttpClient; 8 | use crate::error::Error; 9 | pub use ca::CertificateAuthority; 10 | use handler::DeviceCheckHandler; 11 | pub use hyper; 12 | use hyper::{ 13 | server::conn::AddrStream, 14 | service::{make_service_fn, service_fn}, 15 | Server, 16 | }; 17 | use mitm::MitmProxy; 18 | use reqwest::Url; 19 | use std::{convert::Infallible, future::Future, net::SocketAddr, sync::Arc}; 20 | use typed_builder::TypedBuilder; 21 | 22 | #[derive(TypedBuilder)] 23 | pub struct Proxy { 24 | /// The address to listen on. 25 | pub listen_addr: SocketAddr, 26 | 27 | /// Upstream proxy 28 | pub proxy: Option, 29 | 30 | /// The certificate authority to use. 31 | pub ca: Arc, 32 | } 33 | 34 | impl Proxy { 35 | pub async fn start>(self, shutdown_signal: F) -> Result<(), Error> { 36 | let proxy = self.proxy; 37 | let client = HttpClient::new(proxy.clone())?; 38 | let handler = DeviceCheckHandler::new(proxy)?; 39 | let make_service = make_service_fn(move |_conn: &AddrStream| { 40 | let ca = Arc::clone(&self.ca); 41 | let client = client.clone(); 42 | let handler = handler.clone(); 43 | async move { 44 | Ok::<_, Infallible>(service_fn(move |req| { 45 | let mitm_proxy = MitmProxy { 46 | ca: Arc::clone(&ca), 47 | client: client.clone(), 48 | handler: handler.clone(), 49 | }; 50 | mitm_proxy.proxy(req) 51 | })) 52 | } 53 | }); 54 | 55 | Server::bind(&self.listen_addr) 56 | .http1_preserve_header_case(true) 57 | .http1_title_case_headers(true) 58 | .serve(make_service) 59 | .with_graceful_shutdown(shutdown_signal) 60 | .await 61 | .map_err(Into::into) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod cagen; 2 | mod daemon; 3 | mod error; 4 | mod proxy; 5 | mod serve; 6 | 7 | use anyhow::Result; 8 | use clap::{Args, Parser, Subcommand}; 9 | use reqwest::Url; 10 | use std::{net::SocketAddr, path::PathBuf}; 11 | 12 | #[global_allocator] 13 | static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; 14 | 15 | #[derive(Parser)] 16 | #[clap(author, version, about, arg_required_else_help = true)] 17 | #[command(args_conflicts_with_subcommands = true)] 18 | pub struct Opt { 19 | #[clap(subcommand)] 20 | pub commands: Commands, 21 | } 22 | 23 | #[derive(Subcommand)] 24 | pub enum Commands { 25 | /// Run server 26 | Run(BootArgs), 27 | /// Start server daemon 28 | #[cfg(target_family = "unix")] 29 | Start(BootArgs), 30 | /// Restart server daemon 31 | #[cfg(target_family = "unix")] 32 | Restart(BootArgs), 33 | /// Stop server daemon 34 | #[cfg(target_family = "unix")] 35 | Stop, 36 | /// Show the server daemon log 37 | #[cfg(target_family = "unix")] 38 | Log, 39 | /// Show the server daemon process 40 | #[cfg(target_family = "unix")] 41 | PS, 42 | } 43 | 44 | #[derive(Args, Clone, Debug)] 45 | pub struct BootArgs { 46 | /// Debug mode 47 | #[clap(short, long)] 48 | pub debug: bool, 49 | 50 | /// Bind address 51 | #[clap(short, long, default_value = "0.0.0.0:1080")] 52 | pub bind: SocketAddr, 53 | 54 | /// Upstream proxy 55 | #[clap(short, long)] 56 | pub proxy: Option, 57 | 58 | /// MITM server CA certificate file path 59 | #[clap(long, default_value = "ca/cert.crt", requires = "bind")] 60 | pub cert: PathBuf, 61 | 62 | /// MITM server CA private key file path 63 | #[clap(long, default_value = "ca/key.pem", requires = "bind")] 64 | pub key: PathBuf, 65 | } 66 | 67 | fn main() -> Result<()> { 68 | let opt = Opt::parse(); 69 | 70 | match opt.commands { 71 | Commands::Run(args) => serve::Serve(args).run()?, 72 | #[cfg(target_family = "unix")] 73 | Commands::Start(args) => daemon::start(args)?, 74 | #[cfg(target_family = "unix")] 75 | Commands::Restart(args) => daemon::restart(args)?, 76 | #[cfg(target_family = "unix")] 77 | Commands::Stop => daemon::stop()?, 78 | #[cfg(target_family = "unix")] 79 | Commands::PS => daemon::status()?, 80 | #[cfg(target_family = "unix")] 81 | Commands::Log => daemon::log()?, 82 | }; 83 | 84 | Ok(()) 85 | } 86 | -------------------------------------------------------------------------------- /src/proxy/rewind.rs: -------------------------------------------------------------------------------- 1 | // adapted from https://github.com/hyperium/hyper/blob/master/src/common/io/rewind.rs 2 | 3 | use bytes::{Buf, Bytes}; 4 | use std::{ 5 | cmp, io, 6 | marker::Unpin, 7 | pin::Pin, 8 | task::{self, Poll}, 9 | }; 10 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 11 | 12 | /// Combine a buffer with an IO, rewinding reads to use the buffer. 13 | #[derive(Debug)] 14 | pub(crate) struct Rewind { 15 | pre: Option, 16 | inner: T, 17 | } 18 | 19 | impl Rewind { 20 | #[allow(dead_code)] 21 | pub(crate) fn new(io: T) -> Self { 22 | Rewind { 23 | pre: None, 24 | inner: io, 25 | } 26 | } 27 | 28 | pub(crate) fn new_buffered(io: T, buf: Bytes) -> Self { 29 | Rewind { 30 | pre: Some(buf), 31 | inner: io, 32 | } 33 | } 34 | } 35 | 36 | impl AsyncRead for Rewind 37 | where 38 | T: AsyncRead + Unpin, 39 | { 40 | fn poll_read( 41 | mut self: Pin<&mut Self>, 42 | cx: &mut task::Context<'_>, 43 | buf: &mut ReadBuf<'_>, 44 | ) -> Poll> { 45 | if let Some(mut prefix) = self.pre.take() { 46 | // If there are no remaining bytes, let the bytes get dropped. 47 | if !prefix.is_empty() { 48 | let copy_len = cmp::min(prefix.len(), buf.remaining()); 49 | // TODO: There should be a way to do following two lines cleaner... 50 | buf.put_slice(&prefix[..copy_len]); 51 | prefix.advance(copy_len); 52 | // Put back whats left 53 | if !prefix.is_empty() { 54 | self.pre = Some(prefix); 55 | } 56 | 57 | return Poll::Ready(Ok(())); 58 | } 59 | } 60 | Pin::new(&mut self.inner).poll_read(cx, buf) 61 | } 62 | } 63 | 64 | impl AsyncWrite for Rewind 65 | where 66 | T: AsyncWrite + Unpin, 67 | { 68 | fn poll_write( 69 | mut self: Pin<&mut Self>, 70 | cx: &mut task::Context<'_>, 71 | buf: &[u8], 72 | ) -> Poll> { 73 | Pin::new(&mut self.inner).poll_write(cx, buf) 74 | } 75 | 76 | fn poll_write_vectored( 77 | mut self: Pin<&mut Self>, 78 | cx: &mut task::Context<'_>, 79 | bufs: &[io::IoSlice<'_>], 80 | ) -> Poll> { 81 | Pin::new(&mut self.inner).poll_write_vectored(cx, bufs) 82 | } 83 | 84 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { 85 | Pin::new(&mut self.inner).poll_flush(cx) 86 | } 87 | 88 | fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { 89 | Pin::new(&mut self.inner).poll_shutdown(cx) 90 | } 91 | 92 | fn is_write_vectored(&self) -> bool { 93 | self.inner.is_write_vectored() 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/serve.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::proxy::{CertificateAuthority, Proxy}; 4 | use crate::{cagen, BootArgs}; 5 | use anyhow::{Context, Result}; 6 | use tokio::fs; 7 | use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; 8 | 9 | pub struct Serve(pub BootArgs); 10 | 11 | impl Serve { 12 | #[tokio::main] 13 | pub async fn run(self) -> Result<()> { 14 | if self.0.debug { 15 | std::env::set_var("RUST_LOG", "debug"); 16 | } else { 17 | std::env::set_var("RUST_LOG", "info"); 18 | } 19 | // Init tracing 20 | tracing_subscriber::registry() 21 | .with( 22 | tracing_subscriber::EnvFilter::try_from_default_env() 23 | .unwrap_or_else(|_| "RUST_LOG=info".into()), 24 | ) 25 | .with(tracing_subscriber::fmt::layer()) 26 | .init(); 27 | 28 | // Generate a certificate authority 29 | if !self.0.cert.exists() || !self.0.key.exists() { 30 | cagen::gen_ca(&self.0.cert, &self.0.key); 31 | } 32 | 33 | // Read the private key and certificate 34 | tracing::info!("CA Private key use: {}", self.0.key.display()); 35 | let private_key_bytes = fs::read(self.0.key) 36 | .await 37 | .context("ca private key file path not valid!")?; 38 | let private_key = rustls_pemfile::pkcs8_private_keys(&mut private_key_bytes.as_slice()) 39 | .context("Failed to parse private key")?; 40 | let key = rustls::PrivateKey(private_key[0].clone()); 41 | 42 | // Read the certificate 43 | tracing::info!("CA Certificate use: {}", self.0.cert.display()); 44 | let ca_cert_bytes = fs::read(self.0.cert) 45 | .await 46 | .context("ca cert file path not valid!")?; 47 | let ca_cert = rustls_pemfile::certs(&mut ca_cert_bytes.as_slice()) 48 | .context("Failed to parse CA certificate")?; 49 | let cert = rustls::Certificate(ca_cert[0].clone()); 50 | 51 | // Gnerate a certificate authority 52 | let ca = CertificateAuthority::new( 53 | key, 54 | cert, 55 | String::from_utf8(ca_cert_bytes).context("Failed to parse CA certificate")?, 56 | 1_000, 57 | ) 58 | .context("Failed to create Certificate Authority")?; 59 | 60 | tracing::info!("Http MITM Proxy listen on: http://{}", self.0.bind); 61 | 62 | // Start the server 63 | Proxy::builder() 64 | .ca(Arc::new(ca)) 65 | .listen_addr(self.0.bind) 66 | .proxy(self.0.proxy) 67 | .build() 68 | .start(shutdown_signal()) 69 | .await 70 | .map_err(Into::into) 71 | } 72 | } 73 | 74 | async fn shutdown_signal() { 75 | tokio::signal::ctrl_c() 76 | .await 77 | .expect("Failed to install CTRL+C signal handler"); 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # devicecheck 2 | 3 | 这是一个适用于`iOS`/`iPad`设备的`HTTP`中间人代理,用于抓取`device_token` 4 | 5 | ### 前言 6 | 7 | 最新版的`ChatGPT` APP已上[`SSL pinning`](https://medium.com/trendyol-tech/securing-ios-applications-with-ssl-pinning-38d551945306)验证,使用前提: 8 | 9 | - `iOS`/`iPad`设备需要越狱或者已经安装[`巨魔`](https://github.com/opa334/TrollStore)(**越狱后也可以安装**) 10 | - 在[`巨魔`](https://github.com/opa334/TrollStore)商店安装[`TrollFools`](https://github.com/Lessica/TrollFools),下载[`👉 动态库`](https://github.com/penumbra-x/devicecheck/releases/download/lib/SSLKillSwitch2.dylib)注入到`ChatGPT` 11 | 12 | 以上只是推荐的方法,当然也有其它方法,目的是绕过[`SSL pinning`](https://medium.com/trendyol-tech/securing-ios-applications-with-ssl-pinning-38d551945306) 13 | 14 | ### 命令 15 | 16 | ```bash 17 | $ devicecheck -h 18 | chatgpt preauth devicecheck server 19 | 20 | Usage: devicecheck 21 | devicecheck 22 | 23 | Commands: 24 | run Run server 25 | start Start server daemon 26 | restart Restart server daemon 27 | stop Stop server daemon 28 | log Show the server daemon log 29 | ps Show the server daemon process 30 | help Print this message or the help of the given subcommand(s) 31 | 32 | Options: 33 | -h, --help Print help 34 | -V, --version Print version 35 | 36 | $ devicecheck run -h 37 | Run server 38 | 39 | Usage: devicecheck run [OPTIONS] 40 | 41 | Options: 42 | -d, --debug Debug mode 43 | -b, --bind Bind address [default: 0.0.0.0:1080] 44 | -p, --proxy Upstream proxy 45 | --cert MITM server CA certificate file path [default: ca/cert.crt] 46 | --key MITM server CA private key file path [default: ca/key.pem] 47 | -h, --help Print help 48 | ``` 49 | 50 | ### 安装 51 | 52 | - 编译安装 53 | 54 | ```bash 55 | # 需要先安装rust 56 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 57 | 58 | cargo install --git https://github.com/penumbra-x/devicecheck 59 | ``` 60 | 61 | ### 使用 62 | 63 | 该代理不会像正常代理一样提供网络代理,目的是抓包`device_token`。如果害怕使用多了会被封设备,我建议是使用一些一键换机之类的仿冒设备的软件。 64 | 65 | 1. 启动服务 66 | 67 | - 运行服务 68 | 69 | ```bash 70 | devicecheck run 71 | # 带代理 72 | devicecheck run --proxy http://192.168.1.1:1080 73 | ``` 74 | 75 | - 守护进程 76 | 77 | ```bash 78 | devicecheck start 79 | # 带代理 80 | devicecheck start --proxy http://192.168.1.1:1080 81 | ``` 82 | 83 | 2. 设置代理 84 | 85 | `Wi-Fi`/`Shadowrocket`设置`HTTP`代理 86 | 87 | 3. 信任证书 88 | 89 | 浏览器打开`http://192.168.1.100:1080/mitm/cert`,替换你的代理`IP`以及`端口`,打开下载安装以及信任证书。到这里就彻底完成了,由于`Hook`了`ChatGPT`的网络请求,有以下两种抓取更新`device_token`的动作: 90 | 91 | - 每次打开和关闭`APP`都会抓取一次, 92 | - 打开`APP`任意点击登录会抓取一次,同理点击取消往复操作也生效。 93 | 94 | 4. 获取`preauth_cookie` 95 | 96 | 请求接口`http://192.168.1.100:1080/auth/preauth`,替换你的代理`IP`以及`端口`,示例: 97 | 98 | - Request 99 | 100 | ```bash 101 | curl http://127.0.0.1:1080/auth/preauth 102 | ``` 103 | 104 | - Response 105 | 106 | ```json 107 | { 108 | "preauth_cookie": "900175BB-61C4-4AA2-B400-4DE3B2E1FD7E:1726892032-9nYJ1mU4JSUAEyhACbVOxYoCATD4uXX8H1HZRJzYQ4E%3D" 109 | } 110 | ``` 111 | 112 | 到这里项目的使命已经完成,你可以将`preauth_cookie`用在`ios.chat.openai.com`的接口或者登录。 113 | 114 | ### 注意 115 | 116 | - 自动化操作APP使用不需要太频繁,`cookie`大概会在一段时间内过期(具体不记得什么时间了,24小时?) 117 | - 建议不要把服务放到公网,内网使用Cloudflare [Tunnel](https://www.cloudflare.com/zh-cn/products/tunnel/)开放`/auth/preauth`接口 118 | -------------------------------------------------------------------------------- /src/daemon.rs: -------------------------------------------------------------------------------- 1 | use crate::{serve::Serve, BootArgs}; 2 | use anyhow::Result; 3 | use daemonize::Daemonize; 4 | use std::{ 5 | fs::{File, Permissions}, 6 | os::unix::fs::PermissionsExt, 7 | path::Path, 8 | }; 9 | 10 | const PID_PATH: &str = "/var/run/auth.pid"; 11 | const DEFAULT_STDOUT_PATH: &str = "/var/run/auth.out"; 12 | const DEFAULT_STDERR_PATH: &str = "/var/run/auth.err"; 13 | 14 | /// Get the pid of the daemon 15 | fn get_pid() -> Option { 16 | if let Ok(data) = std::fs::read(PID_PATH) { 17 | let binding = String::from_utf8(data).expect("pid file is not utf8"); 18 | return Some(binding.trim().to_string()); 19 | } 20 | None 21 | } 22 | 23 | /// Check if the current user is root 24 | fn check_root() { 25 | if !nix::unistd::Uid::effective().is_root() { 26 | println!("You must run this executable with root permissions"); 27 | std::process::exit(-1) 28 | } 29 | } 30 | 31 | /// Start the daemon 32 | pub fn start(args: BootArgs) -> Result<()> { 33 | if let Some(pid) = get_pid() { 34 | println!("auth is already running with pid: {}", pid); 35 | return Ok(()); 36 | } 37 | 38 | check_root(); 39 | 40 | let pid_file = File::create(PID_PATH)?; 41 | pid_file.set_permissions(Permissions::from_mode(0o755))?; 42 | 43 | let stdout = File::create(DEFAULT_STDOUT_PATH)?; 44 | stdout.set_permissions(Permissions::from_mode(0o755))?; 45 | 46 | let stderr = File::create(DEFAULT_STDERR_PATH)?; 47 | stdout.set_permissions(Permissions::from_mode(0o755))?; 48 | 49 | let mut daemonize = Daemonize::new() 50 | .pid_file(PID_PATH) 51 | .chown_pid_file(true) 52 | .umask(0o777) 53 | .stdout(stdout) 54 | .stderr(stderr) 55 | .privileged_action(|| "Executed before drop privileges"); 56 | 57 | if let Ok(user) = std::env::var("SUDO_USER") { 58 | if let Ok(Some(real_user)) = nix::unistd::User::from_name(&user) { 59 | daemonize = daemonize 60 | .user(real_user.name.as_str()) 61 | .group(real_user.gid.as_raw()); 62 | } 63 | } 64 | 65 | if let Some(err) = daemonize.start().err() { 66 | eprintln!("Error: {err}"); 67 | std::process::exit(-1) 68 | } 69 | 70 | Serve(args).run() 71 | } 72 | 73 | /// Stop the daemon 74 | pub fn stop() -> Result<()> { 75 | use nix::sys::signal; 76 | use nix::unistd::Pid; 77 | 78 | check_root(); 79 | 80 | if let Some(pid) = get_pid() { 81 | let pid = pid.parse::()?; 82 | for _ in 0..360 { 83 | if signal::kill(Pid::from_raw(pid), signal::SIGINT).is_err() { 84 | break; 85 | } 86 | std::thread::sleep(std::time::Duration::from_secs(1)) 87 | } 88 | let _ = std::fs::remove_file(PID_PATH); 89 | } 90 | 91 | Ok(()) 92 | } 93 | 94 | /// Restart the daemon 95 | pub fn restart(args: BootArgs) -> Result<()> { 96 | stop()?; 97 | start(args) 98 | } 99 | 100 | /// Show the status of the daemon 101 | pub fn status() -> Result<()> { 102 | match get_pid() { 103 | Some(pid) => { 104 | println!("auth is running with pid: {}", pid); 105 | Ok(()) 106 | } 107 | None => anyhow::bail!("auth is not running"), 108 | } 109 | } 110 | 111 | /// Show the log of the daemon 112 | pub fn log() -> Result<()> { 113 | fn read_and_print_file(file_path: &Path, placeholder: &str) -> Result<()> { 114 | if !file_path.exists() { 115 | return Ok(()); 116 | } 117 | 118 | // Check if the file is empty before opening it 119 | let metadata = std::fs::metadata(file_path)?; 120 | if metadata.len() == 0 { 121 | return Ok(()); 122 | } 123 | 124 | let file = File::open(file_path)?; 125 | let reader = std::io::BufReader::new(file); 126 | let mut start = true; 127 | 128 | use std::io::BufRead; 129 | 130 | for line in reader.lines() { 131 | if let Ok(content) = line { 132 | if start { 133 | start = false; 134 | println!("{placeholder}"); 135 | } 136 | println!("{}", content); 137 | } else if let Err(err) = line { 138 | eprintln!("Error reading line: {}", err); 139 | } 140 | } 141 | 142 | Ok(()) 143 | } 144 | 145 | let stdout_path = Path::new(DEFAULT_STDOUT_PATH); 146 | read_and_print_file(stdout_path, "STDOUT>")?; 147 | 148 | let stderr_path = Path::new(DEFAULT_STDERR_PATH); 149 | read_and_print_file(stderr_path, "STDERR>")?; 150 | 151 | Ok(()) 152 | } 153 | -------------------------------------------------------------------------------- /src/proxy/handler.rs: -------------------------------------------------------------------------------- 1 | use super::mitm::RequestOrResponse; 2 | use http::{header, HeaderMap, Method, Request, Response, StatusCode, Uri}; 3 | pub use hyper; 4 | use hyper::{body, Body}; 5 | use moka::sync::Cache; 6 | use rand::seq::IteratorRandom; 7 | use reqwest::{Client, Error, Url}; 8 | use serde::{Deserialize, Serialize}; 9 | use std::{future::Future, time::Duration}; 10 | use tokio::task::JoinHandle; 11 | use tracing::{Instrument, Span}; 12 | 13 | #[derive(Clone)] 14 | pub struct DeviceCheckHandler { 15 | client: Client, 16 | cache: Cache, 17 | } 18 | 19 | impl DeviceCheckHandler { 20 | pub fn new(proxy: Option) -> Result { 21 | Ok(DeviceCheckHandler { 22 | client: Client::builder() 23 | .proxy(reqwest::Proxy::custom(move |_| { 24 | proxy.as_ref().cloned().map_or(None, Some) 25 | })) 26 | .build()?, 27 | cache: Cache::builder() 28 | .max_capacity(u64::MAX) 29 | .time_to_live(Duration::from_secs(3600 * 24 * 7)) 30 | .build(), 31 | }) 32 | } 33 | 34 | pub fn get_cookie_res(&self) -> Result, crate::error::Error> { 35 | let preauth_cookie = PreAuthCookie { 36 | preauth_cookie: self 37 | .cache 38 | .iter() 39 | .choose(&mut rand::thread_rng()) 40 | .map(|(_, cookie)| cookie), 41 | }; 42 | 43 | Response::builder() 44 | .status(StatusCode::OK) 45 | .header(header::CONTENT_TYPE, "application/json") 46 | .body(Body::from(serde_json::to_string_pretty(&preauth_cookie)?)) 47 | .map_err(Into::into) 48 | } 49 | 50 | async fn hook_request(&self, req: Request) -> RequestOrResponse { 51 | let (parts, body) = req.into_parts(); 52 | match body::to_bytes(body) 53 | .await 54 | .map(|bytes| serde_json::from_slice::(&bytes).ok()) 55 | { 56 | Ok(None) => { 57 | tracing::error!("parse preauth_devicecheck request error") 58 | } 59 | Err(err) => { 60 | tracing::error!("invalid preauth_devicecheck request: {}", err) 61 | } 62 | Ok(Some(body)) => { 63 | // Build request 64 | let req = DeviceCheckRequest { 65 | uri: parts.uri, 66 | method: parts.method, 67 | headers: parts.headers, 68 | body, 69 | }; 70 | 71 | // Spwan background fetch task 72 | spawn_with_trace( 73 | self.clone().fetch_preauth_cookie(req), 74 | tracing::info_span!("preauth_devicecheck"), 75 | ); 76 | } 77 | } 78 | 79 | // Hook return invalid request 80 | RequestOrResponse::Response(Response::new(Body::empty())) 81 | } 82 | 83 | async fn fetch_preauth_cookie(self, mut req: DeviceCheckRequest) { 84 | tracing::info!("preauth_devicecheck request: {req:#?}"); 85 | 86 | let device_id = req.body.device_id.clone(); 87 | 88 | let resp = async { 89 | tracing::info!("send preauth_devicecheck request.."); 90 | 91 | req.headers.remove(header::CONTENT_LENGTH); 92 | req.headers.remove(header::ACCEPT_ENCODING); 93 | 94 | let resp = self 95 | .client 96 | .request(req.method, req.uri.to_string()) 97 | .headers(req.headers) 98 | .json(&req.body) 99 | .send() 100 | .await?; 101 | 102 | Ok::<_, reqwest::Error>(resp) 103 | }; 104 | 105 | match resp.await { 106 | Ok(resp) => { 107 | if let Some(cookie) = resp 108 | .cookies() 109 | .find(|c| c.name().eq("_preauth_devicecheck")) 110 | .map(|c| c.value().to_owned()) 111 | { 112 | tracing::info!("preauth_devicecheck: {cookie}"); 113 | self.cache.insert(device_id, cookie); 114 | } 115 | } 116 | Err(err) => { 117 | tracing::error!("invalid preauth_devicecheck request: {}", err) 118 | } 119 | } 120 | } 121 | } 122 | 123 | impl DeviceCheckHandler { 124 | pub async fn handle_request(&self, req: http::Request) -> RequestOrResponse { 125 | if req.uri().path().eq("/backend-api/preauth_devicecheck") { 126 | // Hook request 127 | return self.hook_request(req).await; 128 | } 129 | 130 | // Pass request 131 | RequestOrResponse::Request(req) 132 | } 133 | } 134 | 135 | #[derive(Debug)] 136 | struct DeviceCheckRequest { 137 | uri: Uri, 138 | method: Method, 139 | headers: HeaderMap, 140 | body: DeviceCheckBody, 141 | } 142 | 143 | #[derive(Debug, Clone, Serialize, Deserialize)] 144 | struct DeviceCheckBody { 145 | pub bundle_id: String, 146 | pub device_id: String, 147 | pub device_token: String, 148 | pub request_flag: bool, 149 | } 150 | 151 | #[derive(Serialize)] 152 | struct PreAuthCookie { 153 | preauth_cookie: Option, 154 | } 155 | 156 | fn spawn_with_trace( 157 | fut: impl Future + Send + 'static, 158 | span: Span, 159 | ) -> JoinHandle { 160 | tokio::spawn(fut.instrument(span)) 161 | } 162 | -------------------------------------------------------------------------------- /src/proxy/ca.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use moka::sync::Cache; 3 | use rand::{thread_rng, Rng}; 4 | use rcgen::{ 5 | BasicConstraints, Certificate, CertificateParams, DistinguishedName, DnType, 6 | Error as RcgenError, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, SanType, 7 | }; 8 | use std::sync::Arc; 9 | use time::{ext::NumericalDuration, OffsetDateTime}; 10 | use tokio_rustls::rustls::{ 11 | server::{ClientHello, ResolvesServerCert}, 12 | sign::CertifiedKey, 13 | ServerConfig, 14 | }; 15 | 16 | const CERT_TTL_DAYS: u64 = 365; 17 | const CERT_CACHE_TTL_SECONDS: u64 = CERT_TTL_DAYS * 24 * 60 * 60 / 2; 18 | 19 | /// Issues certificates for use when communicating with clients. 20 | /// 21 | /// Issues certificates for communicating with clients over TLS. Certificates are cached in memory 22 | /// up to a max size that is provided when creating the authority. Clients should be configured to 23 | /// either trust the provided root certificate, or to ignore certificate errors. 24 | #[derive(Clone)] 25 | pub struct CertificateAuthority { 26 | private_key: rustls::PrivateKey, 27 | ca_cert: rustls::Certificate, 28 | ca_cert_string: String, 29 | cache: Cache>, 30 | } 31 | 32 | impl CertificateAuthority { 33 | pub fn gen_ca() -> Result { 34 | let mut params = CertificateParams::default(); 35 | let mut distinguished_name = DistinguishedName::new(); 36 | distinguished_name.push(DnType::CommonName, "devicecheck-mitm"); 37 | distinguished_name.push(DnType::OrganizationName, "devicecheck-mitm"); 38 | distinguished_name.push(DnType::CountryName, "CN"); 39 | distinguished_name.push(DnType::LocalityName, "CN"); 40 | params.distinguished_name = distinguished_name; 41 | params.key_usages = vec![ 42 | KeyUsagePurpose::DigitalSignature, 43 | KeyUsagePurpose::KeyCertSign, 44 | KeyUsagePurpose::CrlSign, 45 | ]; 46 | params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); 47 | Certificate::from_params(params) 48 | } 49 | 50 | /// Attempts to create a new certificate authority. 51 | /// 52 | /// This will fail if the provided key or certificate is invalid, or if the key does not match 53 | /// the certificate. 54 | pub fn new( 55 | private_key: rustls::PrivateKey, 56 | ca_cert: rustls::Certificate, 57 | ca_cert_string: String, 58 | cache_size: u64, 59 | ) -> Result { 60 | let ca = CertificateAuthority { 61 | private_key, 62 | ca_cert, 63 | ca_cert_string, 64 | cache: Cache::builder() 65 | .max_capacity(cache_size) 66 | .time_to_live(std::time::Duration::from_secs(CERT_CACHE_TTL_SECONDS)) 67 | .build(), 68 | }; 69 | 70 | ca.validate()?; 71 | Ok(ca) 72 | } 73 | 74 | pub(crate) fn get_certified_key(&self, server_name: &str) -> Arc { 75 | if let Some(server_cfg) = self.cache.get(server_name) { 76 | return server_cfg; 77 | } 78 | 79 | let certs = vec![self.gen_cert(server_name)]; 80 | let key = rustls::sign::any_supported_type(&self.private_key) 81 | .expect("parse any supported private key"); 82 | let certified_key = Arc::new(CertifiedKey::new(certs, key)); 83 | 84 | self.cache 85 | .insert(server_name.to_string(), certified_key.clone()); 86 | 87 | certified_key 88 | } 89 | 90 | fn gen_cert(&self, server_name: &str) -> rustls::Certificate { 91 | let mut params = rcgen::CertificateParams::default(); 92 | 93 | params.serial_number = Some(thread_rng().gen::().into()); 94 | params.not_before = OffsetDateTime::now_utc().saturating_sub(1.days()); 95 | params.not_after = OffsetDateTime::now_utc().saturating_add((CERT_TTL_DAYS as i64).days()); 96 | params 97 | .subject_alt_names 98 | .push(SanType::DnsName(server_name.to_string())); 99 | let mut distinguished_name = DistinguishedName::new(); 100 | distinguished_name.push(DnType::CommonName, server_name); 101 | params.distinguished_name = distinguished_name; 102 | 103 | params.key_usages = vec![KeyUsagePurpose::DigitalSignature]; 104 | params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth]; 105 | 106 | let key_pair = KeyPair::from_der(&self.private_key.0).expect("Failed to parse private key"); 107 | params.alg = key_pair 108 | .compatible_algs() 109 | .next() 110 | .expect("Failed to find compatible algorithm"); 111 | params.key_pair = Some(key_pair); 112 | 113 | let key_pair = KeyPair::from_der(&self.private_key.0).expect("Failed to parse private key"); 114 | 115 | let ca_cert_params = rcgen::CertificateParams::from_ca_cert_der(&self.ca_cert.0, key_pair) 116 | .expect("Failed to parse CA certificate"); 117 | let ca_cert = rcgen::Certificate::from_params(ca_cert_params) 118 | .expect("Failed to generate CA certificate"); 119 | 120 | let cert = rcgen::Certificate::from_params(params).expect("Failed to generate certificate"); 121 | 122 | rustls::Certificate( 123 | cert.serialize_der_with_signer(&ca_cert) 124 | .expect("Failed to serialize certificate"), 125 | ) 126 | } 127 | 128 | fn validate(&self) -> Result<(), RcgenError> { 129 | let key_pair = rcgen::KeyPair::from_der(&self.private_key.0)?; 130 | rcgen::CertificateParams::from_ca_cert_der(&self.ca_cert.0, key_pair)?; 131 | Ok(()) 132 | } 133 | 134 | pub fn get_cert(&self) -> String { 135 | self.ca_cert_string.clone() 136 | } 137 | 138 | pub fn gen_server_config(self: Arc) -> Arc { 139 | let server_cfg = ServerConfig::builder() 140 | .with_safe_defaults() 141 | .with_no_client_auth() 142 | .with_cert_resolver(self); 143 | Arc::new(server_cfg) 144 | } 145 | } 146 | 147 | impl ResolvesServerCert for CertificateAuthority { 148 | fn resolve(&self, client_hello: ClientHello) -> Option> { 149 | client_hello 150 | .server_name() 151 | .map(|name| self.get_certified_key(name)) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/proxy/mitm.rs: -------------------------------------------------------------------------------- 1 | use super::handler::DeviceCheckHandler; 2 | use super::{ca::CertificateAuthority, client::HttpClient, rewind::Rewind}; 3 | use http::uri::Authority; 4 | use http::StatusCode; 5 | use http::{header, uri::Scheme, Uri}; 6 | use hyper::{server::conn::Http, service::service_fn, Body, Method, Request, Response}; 7 | use std::sync::Arc; 8 | use tokio::io::AsyncReadExt; 9 | use tokio::io::{AsyncRead, AsyncWrite}; 10 | use tokio::net::TcpStream; 11 | use tokio_rustls::TlsAcceptor; 12 | 13 | /// Enum representing either an HTTP request or response. 14 | #[allow(dead_code)] 15 | #[derive(Debug)] 16 | pub enum RequestOrResponse { 17 | Request(Request), 18 | Response(Response), 19 | } 20 | 21 | #[derive(Clone)] 22 | pub struct MitmProxy { 23 | pub handler: DeviceCheckHandler, 24 | pub ca: Arc, 25 | pub client: HttpClient, 26 | } 27 | 28 | impl MitmProxy { 29 | pub(crate) async fn proxy(self, req: Request) -> Result, hyper::Error> { 30 | tracing::debug!("{req:?}"); 31 | if req.method() == Method::CONNECT { 32 | Ok(self.process_connect(req)) 33 | } else { 34 | self.process_request(normalize_request(req), Scheme::HTTP) 35 | .await 36 | } 37 | } 38 | 39 | async fn process_request( 40 | self, 41 | mut req: Request, 42 | scheme: Scheme, 43 | ) -> Result, hyper::Error> { 44 | if req.uri().path().starts_with("/mitm/cert") { 45 | return Ok(self.get_cert_res()); 46 | } 47 | 48 | if req.uri().path().starts_with("/auth/preauth") { 49 | return Ok(self.get_preauth_res()); 50 | } 51 | 52 | if req.version() == http::Version::HTTP_10 || req.version() == http::Version::HTTP_11 { 53 | let (mut parts, body) = req.into_parts(); 54 | 55 | if let Some(Ok(authority)) = parts 56 | .headers 57 | .get(http::header::HOST) 58 | .map(|host| host.to_str()) 59 | { 60 | let mut uri = parts.uri.into_parts(); 61 | uri.scheme = Some(scheme.clone()); 62 | uri.authority = authority.try_into().ok(); 63 | parts.uri = Uri::from_parts(uri).expect("build uri"); 64 | } 65 | 66 | req = Request::from_parts(parts, body); 67 | }; 68 | 69 | // Fix VPN signature recognition 70 | { 71 | let headers = req.headers_mut(); 72 | headers.remove(http::header::HOST); 73 | headers.remove(http::header::CONNECTION); 74 | } 75 | 76 | // Http request Handler 77 | let req = match self.handler.handle_request(req).await { 78 | RequestOrResponse::Request(request) => request, 79 | RequestOrResponse::Response(response) => { 80 | return Ok(response); 81 | } 82 | }; 83 | 84 | // Send Http request 85 | let mut res = match self.client.http(req).await { 86 | Ok(res) => res, 87 | Err(err) => { 88 | tracing::debug!("Http proxy request failed: {err:?}"); 89 | bad_request() 90 | } 91 | }; 92 | 93 | let header_mut = res.headers_mut(); 94 | // Remove `Strict-Transport-Security` to avoid HSTS 95 | // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security 96 | header_mut.remove(header::STRICT_TRANSPORT_SECURITY); 97 | 98 | Ok(res) 99 | } 100 | 101 | fn process_connect(self, mut req: Request) -> Response { 102 | match req.uri().authority().cloned() { 103 | Some(authority) => { 104 | let fut = async move { 105 | match hyper::upgrade::on(&mut req).await { 106 | Ok(mut upgraded) => { 107 | let mut buffer = [0; 4]; 108 | let bytes_read = match upgraded.read(&mut buffer).await { 109 | Ok(bytes_read) => bytes_read, 110 | Err(e) => { 111 | tracing::error!( 112 | "Failed to read from upgraded connection: {}", 113 | e 114 | ); 115 | return; 116 | } 117 | }; 118 | 119 | let mut upgraded = Rewind::new_buffered( 120 | upgraded, 121 | bytes::Bytes::copy_from_slice(buffer[..bytes_read].as_ref()), 122 | ); 123 | 124 | if buffer[..2] == *b"\x16\x03" { 125 | let server_config = self.ca.clone().gen_server_config(); 126 | 127 | let stream = 128 | match TlsAcceptor::from(server_config).accept(upgraded).await { 129 | Ok(stream) => stream, 130 | Err(e) => { 131 | tracing::debug!( 132 | "Failed to establish TLS connection: {}", 133 | e 134 | ); 135 | return; 136 | } 137 | }; 138 | 139 | if let Err(e) = 140 | self.serve_stream(stream, Scheme::HTTPS, authority).await 141 | { 142 | if !e.to_string().starts_with("error shutting down connection") 143 | { 144 | tracing::error!("HTTPS connect error: {}", e); 145 | } 146 | } 147 | 148 | return; 149 | } else { 150 | tracing::warn!( 151 | "Unknown protocol, read '{:02X?}' from upgraded connection", 152 | &buffer[..bytes_read] 153 | ); 154 | } 155 | 156 | let mut server = match TcpStream::connect(authority.as_ref()).await { 157 | Ok(server) => server, 158 | Err(e) => { 159 | tracing::error!("Failed to connect to {}: {}", authority, e); 160 | return; 161 | } 162 | }; 163 | 164 | if let Err(e) = 165 | tokio::io::copy_bidirectional(&mut upgraded, &mut server).await 166 | { 167 | tracing::error!("Failed to tunnel to {}: {}", authority, e); 168 | } 169 | } 170 | Err(e) => tracing::error!("Upgrade error: {}", e), 171 | }; 172 | }; 173 | 174 | tokio::spawn(fut); 175 | 176 | Response::new(Body::empty()) 177 | } 178 | None => bad_request(), 179 | } 180 | } 181 | 182 | async fn serve_stream( 183 | self, 184 | stream: I, 185 | scheme: Scheme, 186 | authority: Authority, 187 | ) -> Result<(), hyper::Error> 188 | where 189 | I: AsyncRead + AsyncWrite + Unpin + Send + 'static, 190 | { 191 | let service = service_fn(|mut req| { 192 | if req.version() == hyper::Version::HTTP_10 || req.version() == hyper::Version::HTTP_11 193 | { 194 | let (mut parts, body) = req.into_parts(); 195 | 196 | parts.uri = { 197 | let mut parts = parts.uri.into_parts(); 198 | parts.scheme = Some(scheme.clone()); 199 | parts.authority = Some(authority.clone()); 200 | Uri::from_parts(parts).expect("Failed to build URI") 201 | }; 202 | 203 | req = Request::from_parts(parts, body); 204 | }; 205 | 206 | self.clone().proxy(req) 207 | }); 208 | 209 | Http::new() 210 | .serve_connection(stream, service) 211 | .with_upgrades() 212 | .await 213 | } 214 | 215 | fn get_cert_res(&self) -> Response { 216 | Response::builder() 217 | .header( 218 | http::header::CONTENT_DISPOSITION, 219 | "attachment; filename=auth-mitm.crt", 220 | ) 221 | .header(http::header::CONTENT_TYPE, "application/octet-stream") 222 | .status(http::StatusCode::OK) 223 | .body(Body::from(self.ca.clone().get_cert())) 224 | .expect("Failed build response") 225 | } 226 | 227 | fn get_preauth_res(&self) -> Response { 228 | match self.handler.get_cookie_res() { 229 | Ok(res) => res, 230 | Err(_) => bad_request(), 231 | } 232 | } 233 | } 234 | 235 | fn bad_request() -> Response { 236 | Response::builder() 237 | .status(StatusCode::BAD_REQUEST) 238 | .body(Body::empty()) 239 | .expect("Failed to build response") 240 | } 241 | 242 | fn normalize_request(mut req: Request) -> Request { 243 | // Hyper will automatically add a Host header if needed. 244 | req.headers_mut().remove(hyper::header::HOST); 245 | *req.version_mut() = hyper::Version::HTTP_11; 246 | req 247 | } 248 | --------------------------------------------------------------------------------