Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wss:// protocol websocket connections cannot be intercepted #133

Open
jiesia opened this issue Dec 17, 2024 · 17 comments
Open

wss:// protocol websocket connections cannot be intercepted #133

jiesia opened this issue Dec 17, 2024 · 17 comments

Comments

@jiesia
Copy link

jiesia commented Dec 17, 2024

Hi, I'm using hudsucker to make a small tool similar to Charles. I found that it seems unable to proxy the websocket connection of the wss:// protocol. I implemented the handle_message method for WebSocketHandler, but it was not executed.

@omjadas
Copy link
Owner

omjadas commented Dec 17, 2024

Is there a specific error you are seeing?

@jiesia
Copy link
Author

jiesia commented Dec 17, 2024

Is there a specific error you are seeing?

There is no error. I debugged it and found that it was not intercepted. Do you know why? Normal https requests can be intercepted.

@jiesia jiesia changed the title wss:// protocol websocket connections cannot be proxied wss:// protocol websocket connections cannot be intercepted Dec 17, 2024
@Wyn1996
Copy link

Wyn1996 commented Dec 18, 2024

Core as follow: let proxy with_websocket_handler, implements handle_message and handle_websocket.Then mobile phone initiates a websocket request, which can be simulated using this website(echo.websocket.org),you will find that no relevant logs are printed.However, python mitmproxy can do it.

let proxy = hudsucker::Proxy::builder()
            .with_addr(SocketAddr::from(([0, 0, 0, 0], PROXY_PORT)))
            .with_ca(ca)
            .with_rustls_client(aws_lc_rs::default_provider())
            .with_http_handler(handler::LogHandler {
                request_id: None,
                app_handle: self.app_handle.clone(),
            })
            .with_websocket_handler(handler::LogHandler {
                request_id: None,
                app_handle: self.app_handle.clone(),
            })
            .with_graceful_shutdown(shutdown_signal())
            .build()
            .expect("Failed to create proxy");
        println!("Proxy created");
impl WebSocketHandler for LogHandler {
    async fn handle_message(&mut self, _ctx: &WebSocketContext, msg: Message) -> Option<Message> {
        println!("{:?}", msg);
        Some(msg)
    }

    async fn handle_websocket(
        mut self,
        ctx: WebSocketContext,
        mut stream: impl Stream<Item = Result<Message, tungstenite::Error>> + Unpin + Send + 'static,
        sink: impl Sink<Message, Error = tungstenite::Error> + Unpin + Send + 'static,
    ) {
        println!("handle_websocket");
    }
}

@omjadas
Copy link
Owner

omjadas commented Dec 18, 2024

It looks like you are incorrectly implementing handle_websocket, in most cases I would recommend using the default implementation. If you remove that from your impl do you see the websocket messages come through?

@Wyn1996
Copy link

Wyn1996 commented Dec 18, 2024

Remove the implementing handle_websocket, still not working.

impl WebSocketHandler for LogHandler {
    async fn handle_message(&mut self, _ctx: &WebSocketContext, msg: Message) -> Option<Message> {
        println!("{:?}", msg);
        Some(msg)
    }
}

@omjadas
Copy link
Owner

omjadas commented Dec 18, 2024

Could you also share your HttpHandler impl?

@Wyn1996
Copy link

Wyn1996 commented Dec 18, 2024

impl HttpHandler for LogHandler {
    
    async fn should_intercept(&mut self, _ctx: &HttpContext, req: &Request<Body>) -> bool {
        
        let uri = req.uri();
        let host = uri.host().unwrap_or("");

        // 只代理在域名在代理名单内的请求
        if PROXY_DOMAINS.iter().any(|domain| host.contains(domain)) {
            true
        } else {
            false
        }
    }

    
    async fn handle_request(
        &mut self,
        _ctx: &HttpContext,
        req: Request<Body>,
    ) -> RequestOrResponse {
        // 解析出 uri, method, version
        let (pairs, body) = req.into_parts();
        let uri = pairs.uri.clone();
        let method = pairs.method.clone();
        let version = pairs.version;

        // 从 uri 中解析出 scheme, host, port
        let scheme = uri.scheme_str().unwrap_or("http");
        let host = uri.host().unwrap_or("");
        let port = uri.port_u16();

        // 去除原始 uri 中 https 的默认端口 443, 以及 http 的默认端口 80
        let uri = {
            let include_port = match (scheme, port) {
                ("http", Some(80)) => true,
                ("https", Some(443)) => true,
                (_, Some(_)) => false,
                (_, None) => false,
            };
            if include_port {
                Uri::builder()
                    .scheme(scheme)
                    .authority(format!("{}", host))
                    .path_and_query(uri.path_and_query().unwrap().as_str())
                    .build()
                    .unwrap()
            } else {
                uri.clone()
            }
        };

       
        let url = uri.to_string();

        
        if method != Method::CONNECT && PROXY_DOMAINS.iter().any(|domain| host.contains(domain)) {
            self.request_id = Some(uuid::Uuid::new_v4());

            let method = method.to_string();
            let headers = pairs
                .headers
                .iter()
                .map(|(k, v)| (k.to_string(), v.to_str().unwrap().to_string()))
                .collect::<HashMap<String, String>>();
            let bytes = body.collect().await.unwrap().to_bytes();

            let body = if headers
                .get("content-type")
                .map(|ct| ct.contains("application/json"))
                .unwrap_or(false)
            {
                serde_json::from_slice(&bytes).unwrap_or(serde_json::Value::String(
                    String::from_utf8_lossy(&bytes).to_string(),
                ))
            } else {
                serde_json::Value::String(String::from_utf8_lossy(&bytes).to_string())
            };

            
            self.app_handle
                .emit(
                    "on-request",
                    RequestLog {
                        request_id: self.request_id.unwrap().to_string(),
                        version: util::get_http_version(version),
                        url: url.clone(),
                        method: method.clone(),
                        headers,
                        body,
                        timestamp: chrono::Utc::now().timestamp_millis(),
                    },
                )
                .expect("Failed to emit event");

            
            let mock_response = {
                let state = self.app_handle.state::<Mutex<AppState>>();
                let state = state.lock().unwrap();
                state
                    .mock_rules
                    .iter()
                    .find(|rule| rule.url == url && rule.method == method)
                    .map(|mock_rule| {
                        let response: MockResponse =
                            serde_json::from_value(mock_rule.response.clone()).unwrap();
                        (response, mock_rule.timeout)
                    })
            };

            if let Some((mock_response, timeout)) = mock_response {
                
                let mut response = Response::builder()
                    .status(mock_response.status)
                    .version(util::get_http_version_from_str(&mock_response.version));

                mock_response.headers.iter().for_each(|(k, v)| {
                    response.headers_mut().unwrap().insert(
                        HeaderName::from_str(k).unwrap(),
                        HeaderValue::from_str(v).unwrap(),
                    );
                });

                let response = response
                    .body(Body::from(mock_response.body.to_string()))
                    .unwrap();

                tokio::time::sleep(tokio::time::Duration::from_millis(timeout)).await;

                self.app_handle
                    .emit(
                        "on-response",
                        ResponseLog {
                            request_id: self.request_id.unwrap().to_string(),
                            version: mock_response.version,
                            status: mock_response.status,
                            headers: mock_response.headers,
                            body: mock_response.body,
                            timestamp: chrono::Utc::now().timestamp_millis(),
                        },
                    )
                    .expect("Failed to emit event");

                return RequestOrResponse::Response(response);
            }

            return RequestOrResponse::Request(Request::from_parts(
                pairs,
                Body::from(Full::new(bytes)),
            ));
        }

        RequestOrResponse::Request(Request::from_parts(pairs, body))
    }

    async fn handle_response(&mut self, _ctx: &HttpContext, res: Response<Body>) -> Response<Body> {
        if let Some(request_id) = self.request_id {
            let (parts, body) = res.into_parts();

            let headers = parts
                .headers
                .iter()
                .map(|(k, v)| (k.to_string(), v.to_str().unwrap().to_string()))
                .collect::<HashMap<String, String>>();

            // let body = hyper::body::to_bytes(body).await.unwrap();
            let body = body.collect().await.unwrap().to_bytes();

            let body_value = if headers
                .get("content-type")
                .map(|ct| ct.contains("application/json"))
                .unwrap_or(false)
            {
                serde_json::from_slice(&body).unwrap_or(serde_json::Value::String(
                    String::from_utf8_lossy(&body).to_string(),
                ))
            } else {
                serde_json::Value::String(String::from_utf8_lossy(&body).to_string())
            };

            self.app_handle
                .emit(
                    "on-response",
                    ResponseLog {
                        request_id: request_id.to_string(),
                        version: util::get_http_version(parts.version),
                        status: parts.status.as_u16(),
                        headers,
                        body: body_value,
                        timestamp: chrono::Utc::now().timestamp_millis(),
                    },
                )
                .expect("Failed to emit event");

            return Response::from_parts(parts, Body::from(Full::new(body)));
        }

        res
    }
}

@omjadas
Copy link
Owner

omjadas commented Dec 18, 2024

Is echo.websocket.org included in PROXY_DOMAINS?

@Wyn1996
Copy link

Wyn1996 commented Dec 19, 2024

The problem has been identified: for ws(s) requests in the backend interface that require login verification, the package cannot be captured (log printing), while for those that do not require login verification, the package can be printed normally.Do you know why? Thank you very much for your support.

@omjadas
Copy link
Owner

omjadas commented Dec 20, 2024

I'm not sure what you mean by login verification, can you clarify?

@Wyn1996
Copy link

Wyn1996 commented Dec 23, 2024

This means that the request header will contain Auth, such as Bear xxxx.

@omjadas
Copy link
Owner

omjadas commented Dec 23, 2024

Thanks, could you also confirm what features you have enabled? If possible, could you also provide an example of a website where you are seeing the issue?

@Wyn1996
Copy link

Wyn1996 commented Dec 24, 2024

This is our privately deployed backend service, using golang's gin. Sorry, no online tools have been found to reproduce this issue.

@omjadas
Copy link
Owner

omjadas commented Dec 24, 2024

No worries, and I assume the Authorization header is sent along with the wesocket upgrade request?

@Wyn1996
Copy link

Wyn1996 commented Dec 24, 2024

Yes, the authentication information is sent along with the ws request header, such as Authorization:"Bear xxxxx"

@Wyn1996
Copy link

Wyn1996 commented Dec 24, 2024

Or to be more precise, ws carries the header information before establishing a connection, and the header cannot be carried during the actual ws interaction process.

@omjadas
Copy link
Owner

omjadas commented Dec 30, 2024

I have spent some time looking into this and have not been able to replicate the issue. I thought that maybe the authorization header was not being correctly forwarded to the server, so I modified the integration tests to verify and it turned out that they are indeed being forwarded correctly. The modifications I made to the tests are as follows, could you confirm that this somewhat matches your actual scenario?

if hyper_tungstenite::is_upgrade_request(&req) {
    if req.headers().get(AUTHORIZATION).is_none_or(|v| v != "password") {
        return Ok(Response::builder()
            .status(StatusCode::UNAUTHORIZED)
            .body(Body::empty())
            .unwrap());
    }

    // spawn handler
}

Could you also confirm that the domain the WebSocket connection is being established with is included in your PROXY_DOMAINS?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants