package main import ( "bytes" "context" crand "crypto/rand" "crypto/tls" "encoding/pem" "fmt" "io" "log" "net" "net/http" "sort" "strconv" "strings" "sync" "time" "github.com/elazarl/goproxy" ) const proxyPort = 8888 var ( stateMu sync.Mutex currentLat float64 currentLon float64 currentEnabled bool currentAccuracy int globalCACert *tls.Certificate verifyToken string logMu sync.Mutex logEntries []string ) func logEvent(msg string) { line := time.Now().Format("15:04:05.000") + " " + msg logMu.Lock() logEntries = append(logEntries, line) if len(logEntries) > 200 { logEntries = logEntries[len(logEntries)-200:] } logMu.Unlock() log.Printf("%s", msg) } func drainLogs() string { logMu.Lock() defer logMu.Unlock() if len(logEntries) == 0 { return "" } out := strings.Join(logEntries, "\n") logEntries = nil return out } func isWlocHost(host string) bool { host = strings.ToLower(strings.TrimSuffix(host, ".")) if strings.Contains(host, ":") { if h, _, err := net.SplitHostPort(host); err == nil { host = h } } return host == "gs-loc.apple.com" || host == "gs-loc-cn.apple.com" } func newProxy(cert *tls.Certificate) *goproxy.ProxyHttpServer { proxy := goproxy.NewProxyHttpServer() proxy.Verbose = false // Handle non-proxy requests (e.g. Safari browsing directly to 127.0.0.1:8888) proxy.NonproxyHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/cert" { stateMu.Lock() cert := globalCACert stateMu.Unlock() if cert != nil { certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Certificate[0]}) w.Header().Set("Content-Type", "application/x-x509-ca-cert") w.Header().Set("Content-Disposition", "attachment; filename=wloccore-ca.crt") w.Write(certPEM) return } w.WriteHeader(http.StatusServiceUnavailable) w.Write([]byte("CA certificate not yet generated")) return } if r.URL.Path == "/" { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(`
Downloading CA certificate...
`)) return } w.WriteHeader(http.StatusBadGateway) w.Write([]byte("This is a proxy server. Use Safari to visit http://127.0.0.1:8888/proxy.mobileconfig for proxy setup, or http://rendoor.cert for CA certificate.")) }) if cert != nil { mitmAction := &goproxy.ConnectAction{ Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(cert), } proxy.OnRequest().HandleConnectFunc(func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { if isWlocHost(host) { logEvent("CONNECT " + host + " -> MITM") return mitmAction, host } // MITM baidu.com for WiFi proxy detection h := host if h2, _, err := net.SplitHostPort(host); err == nil { h = h2 } if h == "baidu.com" || h == "www.baidu.com" || strings.HasSuffix(h, ".baidu.com") { logEvent("CONNECT " + host + " -> MITM (verify)") return mitmAction, host } logEvent("CONNECT " + host + " -> passthrough") return goproxy.OkConnect, host }) } proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { body := []byte(nil) if req.Body != nil { body, _ = io.ReadAll(req.Body) req.Body.Close() req.Body = io.NopCloser(bytes.NewReader(body)) } logEvent(fmt.Sprintf("proxy request target=%s method=%s path=%s size=%d body_prefix=%s", req.Host, req.Method, req.URL.Path, len(body), hexPrefix(body, 64))) return serveLocalRequests(req, ctx) }) proxy.OnResponse().DoFunc(patchWlocResponse) return proxy } func serveLocalRequests(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { host := strings.ToLower(req.Host) // 代理验证拦截:baidu.com/paopao-verify-* → 返回 token h := host if h2, _, err := net.SplitHostPort(host); err == nil { h = h2 } if (h == "baidu.com" || h == "www.baidu.com") && strings.HasPrefix(req.URL.Path, "/paopao-verify-") { token := strings.TrimPrefix(req.URL.Path, "/paopao-verify-") logEvent("verify request path=" + req.URL.Path + " token=" + token) if checkVerifyToken(token) { resp := goproxy.NewResponse(req, "text/plain", http.StatusOK, token) resp.Header.Set("Cache-Control", "no-store") return req, resp } } if host != "rendoor.cert" && host != "www.rendoor.cert" { return req, nil } stateMu.Lock() cert := globalCACert stateMu.Unlock() if cert == nil { return req, nil } if req.URL.Path == "/cert" { certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Certificate[0]}) resp := goproxy.NewResponse(req, "application/x-x509-ca-cert", http.StatusOK, string(certPEM)) resp.Header.Set("Content-Disposition", `attachment; filename=wloccore-ca.crt`) return req, resp } html := `正在准备 CA 证书,如未弹出请点击 这里。
` resp := goproxy.NewResponse(req, "text/html; charset=utf-8", http.StatusOK, html) return req, resp } func patchWlocResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { if resp == nil || resp.Request == nil { return resp } if !isWlocHost(resp.Request.Host) || resp.Request.URL.Path != "/clls/wloc" || resp.Request.Method != http.MethodPost { return resp } stateMu.Lock() enabled, lat, lon, accuracy := currentEnabled, currentLat, currentLon, currentAccuracy stateMu.Unlock() originalBody := resp.Body body, err := io.ReadAll(originalBody) originalBody.Close() if err != nil { logEvent("wloc response read failed: " + err.Error()) resp.Body = io.NopCloser(bytes.NewReader(body)) return resp } logEvent(fmt.Sprintf("wloc upstream response status=%d size=%d headers=[%s] body_prefix=%s", resp.StatusCode, len(body), summarizeHeaders(resp.Header), hexPrefix(body, 64))) if !enabled { logEvent("wloc upstream response passed through (spoofing disabled)") resp.Body = io.NopCloser(bytes.NewReader(body)) return resp } if resp.StatusCode != http.StatusOK { logEvent(fmt.Sprintf("wloc upstream response passed through (status %d != 200)", resp.StatusCode)) resp.Body = io.NopCloser(bytes.NewReader(body)) return resp } if len(body) == 0 { logEvent("wloc upstream response empty, passed through") resp.Body = io.NopCloser(bytes.NewReader(body)) return resp } patched, stats, err := patchResponseBody(body, wlocCoords{Latitude: lat, Longitude: lon, Accuracy: accuracy}) if err != nil { logEvent("wloc patch skipped: " + err.Error()) resp.Body = io.NopCloser(bytes.NewReader(body)) return resp } if bytes.Equal(patched, body) { logEvent("wloc patch produced identical body, passed through") resp.Body = io.NopCloser(bytes.NewReader(body)) return resp } resp.Body = io.NopCloser(bytes.NewReader(patched)) resp.ContentLength = int64(len(patched)) resp.Header.Del("Content-Encoding") resp.Header.Del("Transfer-Encoding") resp.Header.Set("Content-Length", strconv.Itoa(len(patched))) logEvent(fmt.Sprintf("wloc patched target=%.6f,%.6f accuracy=%d locations=%d wifi=%d cell=%d skipped=%d in=%d out=%d body_prefix=%s", lat, lon, accuracy, stats.Locations, stats.WiFi, stats.Cell, stats.Skipped, len(body), len(patched), hexPrefix(patched, 64))) return resp } func hexPrefix(b []byte, n int) string { if len(b) > n { b = b[:n] } return fmt.Sprintf("%x", b) } func summarizeHeaders(h http.Header) string { if len(h) == 0 { return "" } parts := make([]string, 0, len(h)) for k, v := range h { parts = append(parts, k+"="+strings.Join(v, ",")) } sort.Strings(parts) return strings.Join(parts, "; ") } func generateProxyMobileConfig() string { return `