// SPDX-FileCopyrightText: 2026 Echolot contributors // SPDX-License-Identifier: GPL-3.0-or-later package control import ( "crypto/tls" "encoding/base64" "io" "net/http" "strings" ) // httpEcho implements spec §4 HTTP echo: return the exact received request // (request line + headers + body, base64) plus the TLS parameters the server // observed. The client diffs this against what it sent to detect header // injection/stripping, transparent proxying, or TLS interception // (sec.http_echo). Served on the control HTTPS listener and, optionally, on a // cleartext listener to test plaintext-path tampering. func (s *Server) httpEcho(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(io.LimitReader(r.Body, 1<<20)) // Reconstruct the received request head verbatim (as close as net/http // exposes it — header order is lost, but names/values and the request // line survive, which is what tampering changes). var head strings.Builder head.WriteString(r.Method + " " + r.RequestURI + " " + r.Proto + "\r\n") head.WriteString("Host: " + r.Host + "\r\n") for name, vals := range r.Header { for _, v := range vals { head.WriteString(name + ": " + v + "\r\n") } } head.WriteString("\r\n") resp := map[string]any{ "observed_src": r.RemoteAddr, "request_head_b64": base64.StdEncoding.EncodeToString([]byte(head.String())), "body_b64": base64.StdEncoding.EncodeToString(body), "body_len": len(body), "scheme": schemeOf(r), } if r.TLS != nil { resp["tls"] = tlsParams(r.TLS) } writeJSON(w, http.StatusOK, resp) } func schemeOf(r *http.Request) string { if r.TLS != nil { return "https" } return "http" } func tlsParams(cs *tls.ConnectionState) map[string]any { return map[string]any{ "version": tlsVersionName(cs.Version), "cipher": tls.CipherSuiteName(cs.CipherSuite), "sni": cs.ServerName, "alpn": cs.NegotiatedProtocol, "resumed": cs.DidResume, } } func tlsVersionName(v uint16) string { switch v { case tls.VersionTLS13: return "TLS1.3" case tls.VersionTLS12: return "TLS1.2" case tls.VersionTLS11: return "TLS1.1" case tls.VersionTLS10: return "TLS1.0" } return "unknown" } // tlsReference implements spec §4: return the exact certificate chain this // server serves (DER, base64), so the app can compare it against a copy it // obtained out-of-band and against what its own direct handshake yielded // (sec.tls_reference). Not a capability — always available on the control // plane. No auth: the chain is public information a handshake already reveals. func (s *Server) tlsReference(w http.ResponseWriter, r *http.Request) { chain := make([]string, 0, len(s.CertChain)) for _, der := range s.CertChain { chain = append(chain, base64.StdEncoding.EncodeToString(der)) } writeJSON(w, http.StatusOK, map[string]any{ "pin_sha256": s.PinB64, "chain_der": chain, // leaf first, as served }) }