mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-21 22:30:46 +08:00
release: PaopaoLocationSpoofer v1.0.0
- iOS 虚拟定位工具,基于本地 HTTP 代理 MITM 方案 - MapKit 原生地图体验,支持搜索、收藏、实时定位 - 完整的设置引导流程(证书安装、WiFi 代理配置、环境验证) - 支持 iOS 15+,SwiftUI 构建
This commit is contained in:
+225
@@ -0,0 +1,225 @@
|
||||
package main
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -DGOOS_ios -DNDEBUG
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime/cgo"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
//export wloccore_init
|
||||
func wloccore_init() {}
|
||||
|
||||
//export wloccore_hello
|
||||
func wloccore_hello() {}
|
||||
|
||||
//export wloccore_version
|
||||
func wloccore_version() *C.char {
|
||||
return C.CString("0.1.0")
|
||||
}
|
||||
|
||||
//export wloccore_generateca
|
||||
func wloccore_generateca() (r0, r1 *C.char) {
|
||||
logEvent("generateca started")
|
||||
cert, key, err := generateCA()
|
||||
if err != nil {
|
||||
logEvent("generateca failed: " + err.Error())
|
||||
return nil, nil
|
||||
}
|
||||
logEvent("generateca completed")
|
||||
return C.CString(string(cert)), C.CString(string(key))
|
||||
}
|
||||
|
||||
//export wloccore_startproxy
|
||||
func wloccore_startproxy(certData, keyData *C.char, lat, lon C.double, enabled C.int, accuracy C.int) C.uintptr_t {
|
||||
if certData == nil || keyData == nil {
|
||||
return 0
|
||||
}
|
||||
srv, err := startProxy(
|
||||
[]byte(C.GoString(certData)),
|
||||
[]byte(C.GoString(keyData)),
|
||||
float64(lat),
|
||||
float64(lon),
|
||||
enabled != 0,
|
||||
int(accuracy),
|
||||
)
|
||||
if err != nil {
|
||||
logEvent("startproxy failed: " + err.Error())
|
||||
return 0
|
||||
}
|
||||
return C.uintptr_t(cgo.NewHandle(srv))
|
||||
}
|
||||
|
||||
//export wloccore_stopproxy
|
||||
func wloccore_stopproxy(h C.uintptr_t) C.int {
|
||||
logEvent("stopproxy requested")
|
||||
handle := cgo.Handle(h)
|
||||
srv, ok := handle.Value().(*http.Server)
|
||||
handle.Delete()
|
||||
if !ok {
|
||||
logEvent("stopproxy failed: invalid handle")
|
||||
return 1
|
||||
}
|
||||
if err := stopProxy(srv); err != nil {
|
||||
logEvent("stopproxy failed: " + err.Error())
|
||||
return 2
|
||||
}
|
||||
logEvent("stopproxy completed")
|
||||
return 0
|
||||
}
|
||||
|
||||
//export wloccore_setcoords
|
||||
func wloccore_setcoords(lat, lon C.double, enabled C.int, accuracy C.int) {
|
||||
stateMu.Lock()
|
||||
currentLat = float64(lat)
|
||||
currentLon = float64(lon)
|
||||
currentEnabled = enabled != 0
|
||||
currentAccuracy = int(accuracy)
|
||||
stateMu.Unlock()
|
||||
logEvent("setcoords enabled=" + strconv.FormatBool(enabled != 0) + " lat=" + strconv.FormatFloat(float64(lat), 'f', 6, 64) + " lon=" + strconv.FormatFloat(float64(lon), 'f', 6, 64) + " accuracy=" + strconv.Itoa(int(accuracy)))
|
||||
}
|
||||
|
||||
//export wloccore_getcoords
|
||||
func wloccore_getcoords() (lat, lon C.double, enabled C.int) {
|
||||
stateMu.Lock()
|
||||
defer stateMu.Unlock()
|
||||
lat = C.double(currentLat)
|
||||
lon = C.double(currentLon)
|
||||
enabled = 0
|
||||
if currentEnabled {
|
||||
enabled = 1
|
||||
}
|
||||
return lat, lon, enabled
|
||||
}
|
||||
|
||||
//export wloccore_drainlogs
|
||||
func wloccore_drainlogs() *C.char {
|
||||
s := drainLogs()
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return C.CString(s)
|
||||
}
|
||||
|
||||
//export wloccore_startcertserver
|
||||
func wloccore_startcertserver(certData, keyData *C.char) C.uintptr_t {
|
||||
if certData == nil || keyData == nil {
|
||||
return 0
|
||||
}
|
||||
logEvent("start certificate server requested")
|
||||
server, err := startCertificateServer([]byte(C.GoString(certData)), []byte(C.GoString(keyData)))
|
||||
if err != nil {
|
||||
logEvent("start certificate server failed: " + err.Error())
|
||||
return 0
|
||||
}
|
||||
logEvent("certificate server started http=" + server.DownloadURL() + " probe=" + server.ProbeURL())
|
||||
return C.uintptr_t(cgo.NewHandle(server))
|
||||
}
|
||||
|
||||
func certificateServerForHandle(h C.uintptr_t) (*certificateServer, cgo.Handle, bool) {
|
||||
if h == 0 {
|
||||
return nil, 0, false
|
||||
}
|
||||
handle := cgo.Handle(h)
|
||||
server, ok := handle.Value().(*certificateServer)
|
||||
return server, handle, ok
|
||||
}
|
||||
|
||||
//export wloccore_certserver_httpport
|
||||
func wloccore_certserver_httpport(h C.uintptr_t) C.int {
|
||||
server, _, ok := certificateServerForHandle(h)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return C.int(server.httpLn.Addr().(*net.TCPAddr).Port)
|
||||
}
|
||||
|
||||
//export wloccore_certserver_httpsport
|
||||
func wloccore_certserver_httpsport(h C.uintptr_t) C.int {
|
||||
server, _, ok := certificateServerForHandle(h)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return C.int(server.httpsLn.Addr().(*net.TCPAddr).Port)
|
||||
}
|
||||
|
||||
//export wloccore_certserver_leafsha256
|
||||
func wloccore_certserver_leafsha256(h C.uintptr_t) *C.char {
|
||||
server, _, ok := certificateServerForHandle(h)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return C.CString(server.LeafSHA256())
|
||||
}
|
||||
|
||||
//export wloccore_stopcertserver
|
||||
func wloccore_stopcertserver(h C.uintptr_t) C.int {
|
||||
server, handle, ok := certificateServerForHandle(h)
|
||||
if !ok {
|
||||
return 1
|
||||
}
|
||||
handle.Delete()
|
||||
if err := server.Close(); err != nil {
|
||||
return 2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
//export wloccore_testpatch
|
||||
func wloccore_testpatch(lat, lon C.double, accuracy C.int) *C.char {
|
||||
c := wlocCoords{Latitude: float64(lat), Longitude: float64(lon), Accuracy: int(accuracy)}
|
||||
original := makeTestWlocBody()
|
||||
patched, stats, err := patchWlocBody(original, c)
|
||||
if err != nil {
|
||||
return C.CString("error: " + err.Error())
|
||||
}
|
||||
if stats.Locations == 0 {
|
||||
return C.CString("error: no location entries found")
|
||||
}
|
||||
if len(patched) < 10 {
|
||||
return C.CString("error: patched body too short")
|
||||
}
|
||||
newLen := int(binary.BigEndian.Uint16(patched[8:10]))
|
||||
if newLen <= 0 || 10+newLen > len(patched) {
|
||||
return C.CString("error: invalid patched length")
|
||||
}
|
||||
newPayload := patched[10 : 10+newLen]
|
||||
wantLat := append(writeTag(1, wireVarint), writeVarint(uint64(int64(math.Round(c.Latitude*1e8))))...)
|
||||
wantLon := append(writeTag(2, wireVarint), writeVarint(uint64(int64(math.Round(c.Longitude*1e8))))...)
|
||||
if !bytes.Contains(newPayload, wantLat) {
|
||||
return C.CString("error: patched latitude mismatch")
|
||||
}
|
||||
if !bytes.Contains(newPayload, wantLon) {
|
||||
return C.CString("error: patched longitude mismatch")
|
||||
}
|
||||
return C.CString(fmt.Sprintf("ok: lat=%f lon=%f wifi=%d cell=%d locations=%d", c.Latitude, c.Longitude, stats.WiFi, stats.Cell, stats.Locations))
|
||||
}
|
||||
|
||||
//export wloccore_testrequesthex
|
||||
func wloccore_testrequesthex() *C.char {
|
||||
return C.CString(fmt.Sprintf("%x", makeTestWlocRequest()))
|
||||
}
|
||||
|
||||
//export wloccore_refreshverifytoken
|
||||
func wloccore_refreshverifytoken() *C.char {
|
||||
return C.CString(refreshVerifyToken())
|
||||
}
|
||||
|
||||
//export wloccore_checkverifytoken
|
||||
func wloccore_checkverifytoken(token *C.char) C.int {
|
||||
if checkVerifyToken(C.GoString(token)) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
func deterministicReader() io.Reader {
|
||||
seed := sha256.Sum256([]byte("paopao-location-spoofer-ca-v1"))
|
||||
src := rand.NewSource(int64(binary.BigEndian.Uint64(seed[:8])))
|
||||
return rand.New(src)
|
||||
}
|
||||
|
||||
func generateCA() (certPEM, keyPEM []byte, err error) {
|
||||
rng := deterministicReader()
|
||||
privateKey, err := rsa.GenerateKey(rng, 2048)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"WLOC"},
|
||||
CommonName: "WLOC CA " + time.Now().In(time.FixedZone("CST", 8*3600)).Format("2006.01.02 15:04"),
|
||||
},
|
||||
NotBefore: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
NotAfter: time.Date(2045, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
certDER, err := x509.CreateCertificate(rng, &template, &template, &privateKey.PublicKey, privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
certPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
||||
|
||||
keyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
keyPEM = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})
|
||||
return certPEM, keyPEM, nil
|
||||
}
|
||||
|
||||
func parseCA(certPEM, keyPEM []byte) (*tls.Certificate, error) {
|
||||
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cert, nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGenerateCA(t *testing.T) {
|
||||
certPEM, keyPEM, err := generateCA()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(certPEM) == 0 || len(keyPEM) == 0 {
|
||||
t.Fatal("empty CA output")
|
||||
}
|
||||
block, _ := pem.Decode(certPEM)
|
||||
if block == nil {
|
||||
t.Fatal("invalid cert PEM")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !cert.IsCA {
|
||||
t.Fatal("certificate is not a CA")
|
||||
}
|
||||
if time.Until(cert.NotAfter).Hours() < 360*24 {
|
||||
t.Fatal("CA validity is too short")
|
||||
}
|
||||
if _, err := parseCA(certPEM, keyPEM); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// certificateServer exposes a device-specific CA download before the Packet
|
||||
// Tunnel starts and a TLS endpoint whose successful default validation proves
|
||||
// that iOS has installed and fully trusted that CA.
|
||||
type certificateServer struct {
|
||||
httpServer *http.Server
|
||||
httpsServer *http.Server
|
||||
httpLn net.Listener
|
||||
httpsLn net.Listener
|
||||
leafSHA256 string
|
||||
closeOnce sync.Once
|
||||
closeErr error
|
||||
}
|
||||
|
||||
func startCertificateServer(caCertPEM, caKeyPEM []byte) (*certificateServer, error) {
|
||||
ca, err := parseCA(caCertPEM, caKeyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ca.Leaf.IsCA {
|
||||
return nil, errors.New("certificate server requires a CA certificate")
|
||||
}
|
||||
|
||||
leaf, leafDER, err := issueLoopbackLeaf(ca)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rootDER := ca.Leaf.Raw
|
||||
httpLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpsLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
_ = httpLn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
downloadMux := http.NewServeMux()
|
||||
downloadMux.HandleFunc("/ca.cer", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/x-x509-ca-cert")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="LocationSpoofer-CA.cer"`)
|
||||
_, _ = w.Write(rootDER)
|
||||
})
|
||||
|
||||
probeMux := http.NewServeMux()
|
||||
probeMux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte("ok\n"))
|
||||
})
|
||||
|
||||
server := &certificateServer{
|
||||
httpServer: &http.Server{Handler: downloadMux},
|
||||
httpsServer: &http.Server{Handler: probeMux},
|
||||
httpLn: httpLn,
|
||||
httpsLn: httpsLn,
|
||||
leafSHA256: sha256Base64(leafDER),
|
||||
}
|
||||
go func() {
|
||||
if err := server.httpServer.Serve(httpLn); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
logEvent("certificate download server error: " + err.Error())
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
tlsListener := tls.NewListener(httpsLn, &tls.Config{Certificates: []tls.Certificate{leaf}, MinVersion: tls.VersionTLS12})
|
||||
if err := server.httpsServer.Serve(tlsListener); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
logEvent("certificate probe server error: " + err.Error())
|
||||
}
|
||||
}()
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func issueLoopbackLeaf(ca *tls.Certificate) (tls.Certificate, []byte, error) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, nil, err
|
||||
}
|
||||
serialLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serial, err := rand.Int(rand.Reader, serialLimit)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, nil, err
|
||||
}
|
||||
now := time.Now()
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{CommonName: "Location Spoofer Local Trust Probe"},
|
||||
NotBefore: now.Add(-time.Hour),
|
||||
NotAfter: now.Add(24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageServerAuth,
|
||||
},
|
||||
DNSNames: []string{"localhost"},
|
||||
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
}
|
||||
der, err := x509.CreateCertificate(rand.Reader, template, ca.Leaf, &privateKey.PublicKey, ca.PrivateKey)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, nil, err
|
||||
}
|
||||
keyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, nil, err
|
||||
}
|
||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})
|
||||
leaf, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, nil, err
|
||||
}
|
||||
leaf.Leaf, err = x509.ParseCertificate(der)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, nil, err
|
||||
}
|
||||
return leaf, der, nil
|
||||
}
|
||||
|
||||
func sha256Base64(data []byte) string {
|
||||
sum := sha256.Sum256(data)
|
||||
return base64.StdEncoding.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func (s *certificateServer) DownloadURL() string {
|
||||
return "http://" + s.httpLn.Addr().String() + "/ca.cer"
|
||||
}
|
||||
|
||||
func (s *certificateServer) ProbeURL() string {
|
||||
return "https://" + s.httpsLn.Addr().String() + "/health"
|
||||
}
|
||||
|
||||
func (s *certificateServer) LeafSHA256() string { return s.leafSHA256 }
|
||||
|
||||
func (s *certificateServer) Close() error {
|
||||
s.closeOnce.Do(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
if err := s.httpServer.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
s.closeErr = err
|
||||
}
|
||||
if err := s.httpsServer.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) && s.closeErr == nil {
|
||||
s.closeErr = err
|
||||
}
|
||||
})
|
||||
return s.closeErr
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCertificateServerServesCurrentCAAndTrustedProbe(t *testing.T) {
|
||||
certPEM, keyPEM, err := generateCA()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
server, err := startCertificateServer(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = server.Close() })
|
||||
|
||||
download, err := http.Get(server.DownloadURL())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer download.Body.Close()
|
||||
body, err := io.ReadAll(download.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if download.StatusCode != http.StatusOK {
|
||||
t.Fatalf("download status = %d", download.StatusCode)
|
||||
}
|
||||
if download.Header.Get("Content-Type") != "application/x-x509-ca-cert" {
|
||||
t.Fatalf("unexpected content type: %q", download.Header.Get("Content-Type"))
|
||||
}
|
||||
if !strings.Contains(download.Header.Get("Content-Disposition"), "LocationSpoofer-CA.cer") {
|
||||
t.Fatalf("unexpected content disposition: %q", download.Header.Get("Content-Disposition"))
|
||||
}
|
||||
root, err := x509.ParseCertificate(blockBytes(certPEM))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(body) != string(root.Raw) {
|
||||
t.Fatal("downloaded certificate did not match input CA DER")
|
||||
}
|
||||
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM(certPEM) {
|
||||
t.Fatal("could not add CA to pool")
|
||||
}
|
||||
client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{RootCAs: pool}}}
|
||||
probe, err := client.Get(server.ProbeURL())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer probe.Body.Close()
|
||||
response, err := io.ReadAll(probe.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if probe.StatusCode != http.StatusOK || string(response) != "ok\n" {
|
||||
t.Fatalf("probe response = %d %q", probe.StatusCode, response)
|
||||
}
|
||||
if server.LeafSHA256() == "" {
|
||||
t.Fatal("missing leaf SHA-256 fingerprint")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertificateServerOnlyServesKnownPaths(t *testing.T) {
|
||||
certPEM, keyPEM, err := generateCA()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server, err := startCertificateServer(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = server.Close() })
|
||||
|
||||
response, err := http.Get(strings.TrimSuffix(server.DownloadURL(), "/ca.cer") + "/unknown")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusNotFound {
|
||||
t.Fatalf("status = %d, want 404", response.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func blockBytes(certPEM []byte) []byte {
|
||||
block, _ := pem.Decode(certPEM)
|
||||
if block == nil {
|
||||
return nil
|
||||
}
|
||||
return block.Bytes
|
||||
}
|
||||
|
||||
func TestCertificateServerRejectsDefaultTrustBeforeInstallation(t *testing.T) {
|
||||
certPEM, keyPEM, err := generateCA()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
server, err := startCertificateServer(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = server.Close() })
|
||||
|
||||
response, err := http.Get(server.ProbeURL())
|
||||
if response != nil {
|
||||
response.Body.Close()
|
||||
}
|
||||
if err == nil {
|
||||
t.Fatal("default TLS trust unexpectedly accepted an uninstalled CA")
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
module pp_proxy/wloc/core
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require github.com/elazarl/goproxy v1.8.5
|
||||
|
||||
require (
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
)
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/elazarl/goproxy v1.8.5 h1:33R3Q6geBd2PHmjEI82s3dQWSoBKjTktg4YAFXutIoY=
|
||||
github.com/elazarl/goproxy v1.8.5/go.mod h1:b5xm6W48AUHNpRTCvlnd0YVh+JafCCtsLsJZvvNTz+E=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,3 @@
|
||||
package main
|
||||
|
||||
func main() {}
|
||||
+409
@@ -0,0 +1,409 @@
|
||||
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(`<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=/cert"><title>CA Certificate</title></head><body><p><a href="/cert">Download CA certificate</a></p></body></html>`))
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/coords" {
|
||||
stateMu.Lock()
|
||||
enabled, lat, lon := currentEnabled, currentLat, currentLon
|
||||
stateMu.Unlock()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(fmt.Sprintf(`{"enabled":%t,"lat":%.6f,"lon":%.6f,"accuracy":%d}`, enabled, lat, lon, currentAccuracy)))
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/proxy.mobileconfig" || r.URL.Path == "/proxy.mobileconfig/" {
|
||||
w.Header().Set("Content-Type", "application/x-apple-aspen-config")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=paopao-proxy.mobileconfig")
|
||||
w.Write([]byte(generateProxyMobileConfig()))
|
||||
return
|
||||
}
|
||||
// Serve cert download page for rendoor.cert-like hosts
|
||||
if r.Host == "rendoor.cert" || strings.HasPrefix(r.Host, "rendoor.cert:") {
|
||||
stateMu.Lock()
|
||||
cert := globalCACert
|
||||
stateMu.Unlock()
|
||||
if cert != nil && r.URL.Path == "/cert" {
|
||||
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.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Write([]byte(`<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="2;url=/cert"><title>CA Certificate</title></head><body><p>Downloading CA certificate...</p></body></html>`))
|
||||
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 := `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="refresh" content="2;url=/cert">
|
||||
<title>Preparing Certificate</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>正在准备 CA 证书,如未弹出请点击 <a href="/cert">这里</a>。</p>
|
||||
</body>
|
||||
</html>`
|
||||
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 `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PayloadContent</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>PayloadDescription</key>
|
||||
<string>Configures a global HTTP proxy for location spoofing.</string>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>Paopao Location Proxy</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>com.paopaolabs.location-spoofer.proxy.payload</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.proxy.http.global</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>` + uuidString() + `</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>GlobalHTTPProxy</key>
|
||||
<dict>
|
||||
<key>ProxyServer</key>
|
||||
<string>127.0.0.1</string>
|
||||
<key>ProxyServerPort</key>
|
||||
<integer>8888</integer>
|
||||
<key>ProxyType</key>
|
||||
<string>Manual</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>Paopao Location Proxy</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>com.paopaolabs.location-spoofer.proxy</string>
|
||||
<key>PayloadType</key>
|
||||
<string>Configuration</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>` + uuidString() + `</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</plist>`
|
||||
}
|
||||
|
||||
func uuidString() string {
|
||||
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
|
||||
randomUint32(), randomUint32()&0xFFFF,
|
||||
(randomUint32()|0x4000)&0x4FFF,
|
||||
(randomUint32()|0x8000)&0xBFFF,
|
||||
uint64(randomUint32())<<32|uint64(randomUint32()))
|
||||
}
|
||||
|
||||
func randomUint32() uint32 {
|
||||
b := make([]byte, 4)
|
||||
crand.Read(b)
|
||||
return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])
|
||||
}
|
||||
|
||||
func startProxy(certPEM, keyPEM []byte, lat, lon float64, enabled bool, accuracy int) (*http.Server, error) {
|
||||
cert, err := parseCA(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stateMu.Lock()
|
||||
globalCACert = cert
|
||||
currentLat, currentLon, currentEnabled, currentAccuracy = lat, lon, enabled, accuracy
|
||||
stateMu.Unlock()
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", proxyPort))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
srv := &http.Server{Handler: newProxy(cert)}
|
||||
go func() {
|
||||
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
logEvent("proxy server error: " + err.Error())
|
||||
}
|
||||
}()
|
||||
logEvent("proxy started on 127.0.0.1:8888")
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
func stopProxy(srv *http.Server) error {
|
||||
if srv == nil {
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
err := srv.Shutdown(ctx)
|
||||
stateMu.Lock()
|
||||
globalCACert = nil
|
||||
stateMu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func refreshVerifyToken() string {
|
||||
token := fmt.Sprintf("%04x", uuidString())
|
||||
stateMu.Lock()
|
||||
verifyToken = token
|
||||
stateMu.Unlock()
|
||||
return token
|
||||
}
|
||||
|
||||
func checkVerifyToken(token string) bool {
|
||||
stateMu.Lock()
|
||||
defer stateMu.Unlock()
|
||||
return verifyToken != "" && verifyToken == token
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
wireVarint = 0
|
||||
wireFixed64 = 1
|
||||
wireLengthDelim = 2
|
||||
wireFixed32 = 5
|
||||
)
|
||||
|
||||
type wlocCoords struct {
|
||||
Latitude float64
|
||||
Longitude float64
|
||||
Accuracy int
|
||||
}
|
||||
|
||||
type patchStats struct {
|
||||
WiFi int
|
||||
Cell int
|
||||
Locations int
|
||||
Skipped int
|
||||
}
|
||||
|
||||
type wireField struct {
|
||||
num int
|
||||
wireType int
|
||||
value []byte
|
||||
raw []byte
|
||||
}
|
||||
|
||||
var macPattern = regexp.MustCompile(`^[0-9a-fA-F]{1,2}(:[0-9a-fA-F]{1,2}){5}$`)
|
||||
|
||||
func minInt(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func maxInt(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func cloneBytes(b []byte) []byte {
|
||||
return append([]byte(nil), b...)
|
||||
}
|
||||
|
||||
func readVarint(data []byte) (uint64, int, error) {
|
||||
var v uint64
|
||||
for i := 0; i < len(data); i++ {
|
||||
if i >= 10 {
|
||||
return 0, 0, errors.New("varint too long")
|
||||
}
|
||||
b := data[i]
|
||||
v |= uint64(b&0x7f) << (7 * i)
|
||||
if b&0x80 == 0 {
|
||||
return v, i + 1, nil
|
||||
}
|
||||
}
|
||||
return 0, 0, errors.New("truncated varint")
|
||||
}
|
||||
|
||||
func writeVarint(v uint64) []byte {
|
||||
var out []byte
|
||||
for v >= 0x80 {
|
||||
out = append(out, byte(v)|0x80)
|
||||
v >>= 7
|
||||
}
|
||||
return append(out, byte(v))
|
||||
}
|
||||
|
||||
func writeTag(num, wireType int) []byte {
|
||||
return writeVarint(uint64(num<<3 | wireType))
|
||||
}
|
||||
|
||||
func writeLengthDelimited(num int, value []byte) []byte {
|
||||
var out []byte
|
||||
out = append(out, writeTag(num, wireLengthDelim)...)
|
||||
out = append(out, writeVarint(uint64(len(value)))...)
|
||||
out = append(out, value...)
|
||||
return out
|
||||
}
|
||||
|
||||
func parseFields(data []byte) ([]wireField, error) {
|
||||
var fields []wireField
|
||||
idx := 0
|
||||
for idx < len(data) {
|
||||
start := idx
|
||||
tag, n, err := readVarint(data[idx:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idx += n
|
||||
num := int(tag >> 3)
|
||||
wire := int(tag & 7)
|
||||
if num == 0 {
|
||||
return nil, errors.New("invalid protobuf field 0")
|
||||
}
|
||||
var value []byte
|
||||
switch wire {
|
||||
case wireVarint:
|
||||
_, vn, err := readVarint(data[idx:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
value = cloneBytes(data[idx : idx+vn])
|
||||
idx += vn
|
||||
case wireFixed64:
|
||||
if idx+8 > len(data) {
|
||||
return nil, errors.New("truncated fixed64")
|
||||
}
|
||||
value = cloneBytes(data[idx : idx+8])
|
||||
idx += 8
|
||||
case wireLengthDelim:
|
||||
l, ln, err := readVarint(data[idx:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
idx += ln
|
||||
if l > uint64(len(data)-idx) {
|
||||
return nil, errors.New("truncated length-delimited")
|
||||
}
|
||||
value = cloneBytes(data[idx : idx+int(l)])
|
||||
idx += int(l)
|
||||
case wireFixed32:
|
||||
if idx+4 > len(data) {
|
||||
return nil, errors.New("truncated fixed32")
|
||||
}
|
||||
value = cloneBytes(data[idx : idx+4])
|
||||
idx += 4
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported wire type %d", wire)
|
||||
}
|
||||
fields = append(fields, wireField{
|
||||
num: num,
|
||||
wireType: wire,
|
||||
value: value,
|
||||
raw: cloneBytes(data[start:idx]),
|
||||
})
|
||||
}
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
func patchLocation(loc []byte, c wlocCoords) ([]byte, bool, error) {
|
||||
fields, err := parseFields(loc)
|
||||
if err != nil {
|
||||
return loc, false, err
|
||||
}
|
||||
hasLat, hasLon := false, false
|
||||
for _, f := range fields {
|
||||
if f.num == 1 && f.wireType == wireVarint {
|
||||
hasLat = true
|
||||
}
|
||||
if f.num == 2 && f.wireType == wireVarint {
|
||||
hasLon = true
|
||||
}
|
||||
}
|
||||
if !hasLat || !hasLon {
|
||||
return loc, false, nil
|
||||
}
|
||||
|
||||
lat := int64(math.Round(c.Latitude * 1e8))
|
||||
lon := int64(math.Round(c.Longitude * 1e8))
|
||||
var out []byte
|
||||
changed := false
|
||||
for _, f := range fields {
|
||||
switch {
|
||||
case f.num == 1 && f.wireType == wireVarint:
|
||||
raw := append(writeTag(1, wireVarint), writeVarint(uint64(lat))...)
|
||||
if !bytes.Equal(raw, f.raw) {
|
||||
changed = true
|
||||
}
|
||||
out = append(out, raw...)
|
||||
case f.num == 2 && f.wireType == wireVarint:
|
||||
raw := append(writeTag(2, wireVarint), writeVarint(uint64(lon))...)
|
||||
if !bytes.Equal(raw, f.raw) {
|
||||
changed = true
|
||||
}
|
||||
out = append(out, raw...)
|
||||
case f.num == 3 && f.wireType == wireVarint:
|
||||
raw := append(writeTag(3, wireVarint), writeVarint(uint64(c.Accuracy))...)
|
||||
if !bytes.Equal(raw, f.raw) {
|
||||
changed = true
|
||||
}
|
||||
out = append(out, raw...)
|
||||
default:
|
||||
out = append(out, f.raw...)
|
||||
}
|
||||
}
|
||||
return out, changed, nil
|
||||
}
|
||||
|
||||
func patchWifiDevice(device []byte, c wlocCoords, st *patchStats) ([]byte, bool, error) {
|
||||
fields, err := parseFields(device)
|
||||
if err != nil {
|
||||
return device, false, err
|
||||
}
|
||||
hasMac := false
|
||||
for _, f := range fields {
|
||||
if f.num == 1 && f.wireType == wireLengthDelim && macPattern.Match(f.value) {
|
||||
hasMac = true
|
||||
}
|
||||
}
|
||||
if !hasMac {
|
||||
return device, false, nil
|
||||
}
|
||||
|
||||
var out []byte
|
||||
changed := false
|
||||
for _, f := range fields {
|
||||
if f.num == 2 && f.wireType == wireLengthDelim {
|
||||
newVal, subChanged, err := patchLocation(f.value, c)
|
||||
if err != nil {
|
||||
st.Skipped++
|
||||
out = append(out, f.raw...)
|
||||
continue
|
||||
}
|
||||
if subChanged {
|
||||
changed = true
|
||||
st.Locations++
|
||||
}
|
||||
out = append(out, writeLengthDelimited(2, newVal)...)
|
||||
} else {
|
||||
out = append(out, f.raw...)
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
st.WiFi++
|
||||
}
|
||||
return out, changed, nil
|
||||
}
|
||||
|
||||
func patchCellResponse(cell []byte, c wlocCoords, st *patchStats) ([]byte, bool, error) {
|
||||
fields, err := parseFields(cell)
|
||||
if err != nil {
|
||||
return cell, false, err
|
||||
}
|
||||
|
||||
var out []byte
|
||||
changed := false
|
||||
for _, f := range fields {
|
||||
if f.num == 5 && f.wireType == wireLengthDelim {
|
||||
newVal, subChanged, err := patchLocation(f.value, c)
|
||||
if err != nil {
|
||||
st.Skipped++
|
||||
out = append(out, f.raw...)
|
||||
continue
|
||||
}
|
||||
if subChanged {
|
||||
changed = true
|
||||
st.Locations++
|
||||
}
|
||||
out = append(out, writeLengthDelimited(5, newVal)...)
|
||||
} else {
|
||||
out = append(out, f.raw...)
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
st.Cell++
|
||||
}
|
||||
return out, changed, nil
|
||||
}
|
||||
|
||||
func patchWlocPayload(payload []byte, c wlocCoords, st *patchStats) ([]byte, bool, error) {
|
||||
fields, err := parseFields(payload)
|
||||
if err != nil {
|
||||
return payload, false, err
|
||||
}
|
||||
|
||||
var out []byte
|
||||
changed := false
|
||||
for _, f := range fields {
|
||||
switch {
|
||||
case f.num == 2 && f.wireType == wireLengthDelim:
|
||||
newVal, subChanged, err := patchWifiDevice(f.value, c, st)
|
||||
if err != nil {
|
||||
st.Skipped++
|
||||
out = append(out, f.raw...)
|
||||
continue
|
||||
}
|
||||
if subChanged {
|
||||
changed = true
|
||||
}
|
||||
out = append(out, writeLengthDelimited(2, newVal)...)
|
||||
case (f.num == 22 || f.num == 24) && f.wireType == wireLengthDelim:
|
||||
newVal, subChanged, err := patchCellResponse(f.value, c, st)
|
||||
if err != nil {
|
||||
st.Skipped++
|
||||
out = append(out, f.raw...)
|
||||
continue
|
||||
}
|
||||
if subChanged {
|
||||
changed = true
|
||||
}
|
||||
out = append(out, writeLengthDelimited(f.num, newVal)...)
|
||||
default:
|
||||
out = append(out, f.raw...)
|
||||
}
|
||||
}
|
||||
return out, changed, nil
|
||||
}
|
||||
|
||||
func patchFrame(body []byte, offset int, c wlocCoords, st *patchStats) ([]byte, patchStats, error) {
|
||||
if len(body) < offset+10 {
|
||||
return nil, *st, fmt.Errorf("body too short: %d, base=%d", len(body), offset)
|
||||
}
|
||||
length := int(binary.BigEndian.Uint16(body[offset+8 : offset+10]))
|
||||
if length <= 0 {
|
||||
return nil, *st, errors.New("invalid empty frame length")
|
||||
}
|
||||
if offset+10+length > len(body) {
|
||||
return nil, *st, fmt.Errorf("invalid frame length %d at %d for %d", length, offset, len(body))
|
||||
}
|
||||
|
||||
prefix := cloneBytes(body[:offset+8])
|
||||
payload := cloneBytes(body[offset+10 : offset+10+length])
|
||||
suffix := cloneBytes(body[offset+10+length:])
|
||||
before := *st
|
||||
newPayload, changed, err := patchWlocPayload(payload, c, st)
|
||||
if err != nil || !changed || (int(st.WiFi-before.WiFi)+int(st.Cell-before.Cell)+int(st.Locations-before.Locations)) <= 0 || bytes.Equal(newPayload, payload) {
|
||||
*st = before
|
||||
if err != nil {
|
||||
return nil, *st, err
|
||||
}
|
||||
return nil, *st, errors.New("frame parsed but no patchable wloc payload")
|
||||
}
|
||||
if len(newPayload) > 65535 {
|
||||
*st = before
|
||||
return nil, *st, errors.New("patched payload too large")
|
||||
}
|
||||
|
||||
var lenBytes [2]byte
|
||||
binary.BigEndian.PutUint16(lenBytes[:], uint16(len(newPayload)))
|
||||
out := append(prefix, lenBytes[:]...)
|
||||
out = append(out, newPayload...)
|
||||
out = append(out, suffix...)
|
||||
return out, *st, nil
|
||||
}
|
||||
|
||||
func patchWlocBody(body []byte, c wlocCoords) ([]byte, patchStats, error) {
|
||||
var st patchStats
|
||||
offsets := []int{0, 2, 4, 6, 8, 10, 12, 14, 16}
|
||||
seen := map[int]bool{}
|
||||
for _, o := range offsets {
|
||||
seen[o] = true
|
||||
}
|
||||
limit := minInt(96, maxInt(0, len(body)-10))
|
||||
for i := 0; i <= limit; i++ {
|
||||
if !seen[i] {
|
||||
offsets = append(offsets, i)
|
||||
}
|
||||
}
|
||||
|
||||
for _, offset := range offsets {
|
||||
local := st
|
||||
out, _, err := patchFrame(body, offset, c, &local)
|
||||
if err == nil {
|
||||
return out, local, nil
|
||||
}
|
||||
st = local
|
||||
}
|
||||
|
||||
fallbackLimit := minInt(256, len(body))
|
||||
for i := 0; i <= fallbackLimit; i++ {
|
||||
local := patchStats{}
|
||||
payload := body[i:]
|
||||
newPayload, changed, err := patchWlocPayload(payload, c, &local)
|
||||
if err == nil && changed && !bytes.Equal(newPayload, payload) {
|
||||
out := append(cloneBytes(body[:i]), newPayload...)
|
||||
return out, local, nil
|
||||
}
|
||||
}
|
||||
return nil, st, errors.New("no patchable wloc payload found")
|
||||
}
|
||||
|
||||
func maybeGunzip(body []byte) ([]byte, bool, error) {
|
||||
if len(body) >= 2 && body[0] == 0x1f && body[1] == 0x8b {
|
||||
zr, err := gzip.NewReader(bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
defer zr.Close()
|
||||
out, err := io.ReadAll(zr)
|
||||
return out, true, err
|
||||
}
|
||||
return body, false, nil
|
||||
}
|
||||
|
||||
func patchResponseBody(body []byte, c wlocCoords) ([]byte, patchStats, error) {
|
||||
decompressed, wasGzip, err := maybeGunzip(body)
|
||||
if err != nil {
|
||||
return nil, patchStats{}, err
|
||||
}
|
||||
patched, stats, err := patchWlocBody(decompressed, c)
|
||||
if err != nil {
|
||||
return nil, patchStats{}, err
|
||||
}
|
||||
_ = wasGzip
|
||||
return patched, stats, nil
|
||||
}
|
||||
|
||||
func makeTestWlocBody() []byte {
|
||||
var loc []byte
|
||||
loc = append(loc, writeTag(1, wireVarint)...)
|
||||
loc = append(loc, writeVarint(100)...)
|
||||
loc = append(loc, writeTag(2, wireVarint)...)
|
||||
loc = append(loc, writeVarint(200)...)
|
||||
loc = append(loc, writeTag(3, wireVarint)...)
|
||||
loc = append(loc, writeVarint(25)...)
|
||||
|
||||
mac := []byte("aa:bb:cc:dd:ee:ff")
|
||||
var device []byte
|
||||
device = append(device, writeLengthDelimited(1, mac)...)
|
||||
device = append(device, writeLengthDelimited(2, loc)...)
|
||||
|
||||
payload := writeLengthDelimited(2, device)
|
||||
|
||||
magic := []byte{0, 1, 0, 0, 0, 1, 0, 0}
|
||||
var lenBytes [2]byte
|
||||
binary.BigEndian.PutUint16(lenBytes[:], uint16(len(payload)))
|
||||
var out []byte
|
||||
out = append(out, magic...)
|
||||
out = append(out, lenBytes[:]...)
|
||||
out = append(out, payload...)
|
||||
return out
|
||||
}
|
||||
|
||||
func makeTestWlocRequest() []byte {
|
||||
var out []byte
|
||||
|
||||
// 3 个 Wi-Fi AP(真实 wloc 请求格式)
|
||||
type ap struct {
|
||||
mac string
|
||||
rssi int32
|
||||
channel int32
|
||||
}
|
||||
aps := []ap{
|
||||
{"aa:bb:cc:dd:ee:ff", -45, 6},
|
||||
{"11:22:33:44:55:66", -62, 11},
|
||||
{"77:88:99:00:11:22", -71, 1},
|
||||
}
|
||||
now := uint32(time.Now().Unix())
|
||||
for _, a := range aps {
|
||||
var device []byte
|
||||
device = append(device, writeLengthDelimited(1, []byte(a.mac))...)
|
||||
device = append(device, writeTag(4, wireVarint)...)
|
||||
device = append(device, writeVarint(uint64(int64(a.rssi)))...)
|
||||
device = append(device, writeTag(6, wireVarint)...)
|
||||
device = append(device, writeVarint(uint64(a.channel))...)
|
||||
device = append(device, writeTag(11, wireVarint)...)
|
||||
device = append(device, writeVarint(uint64(now))...)
|
||||
out = append(out, writeLengthDelimited(1, device)...)
|
||||
}
|
||||
|
||||
// 1 个蜂窝基站
|
||||
var cell []byte
|
||||
cell = append(cell, writeTag(1, wireVarint)...)
|
||||
cell = append(cell, writeVarint(1)...) // GSM
|
||||
cell = append(cell, writeTag(2, wireVarint)...)
|
||||
cell = append(cell, writeVarint(460)...) // MCC China
|
||||
cell = append(cell, writeTag(3, wireVarint)...)
|
||||
cell = append(cell, writeVarint(1)...) // MNC
|
||||
cell = append(cell, writeTag(4, wireVarint)...)
|
||||
cell = append(cell, writeVarint(15200)...) // LAC
|
||||
cell = append(cell, writeTag(5, wireVarint)...)
|
||||
cell = append(cell, writeVarint(24680)...) // CellID
|
||||
out = append(out, writeLengthDelimited(5, cell)...)
|
||||
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/binary"
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testLocation(lat, lon int64, accuracy uint64) []byte {
|
||||
var out []byte
|
||||
out = append(out, writeTag(1, wireVarint)...)
|
||||
out = append(out, writeVarint(uint64(lat))...)
|
||||
out = append(out, writeTag(2, wireVarint)...)
|
||||
out = append(out, writeVarint(uint64(lon))...)
|
||||
out = append(out, writeTag(3, wireVarint)...)
|
||||
out = append(out, writeVarint(accuracy)...)
|
||||
return out
|
||||
}
|
||||
|
||||
func testWifiDevice(loc []byte) []byte {
|
||||
mac := []byte("aa:bb:cc:dd:ee:ff")
|
||||
var out []byte
|
||||
out = append(out, writeLengthDelimited(1, mac)...)
|
||||
out = append(out, writeLengthDelimited(2, loc)...)
|
||||
return out
|
||||
}
|
||||
|
||||
func testFrame(payload []byte) []byte {
|
||||
magic := []byte{0, 1, 0, 0, 0, 1, 0, 0}
|
||||
var lenBytes [2]byte
|
||||
binary.BigEndian.PutUint16(lenBytes[:], uint16(len(payload)))
|
||||
var out []byte
|
||||
out = append(out, magic...)
|
||||
out = append(out, lenBytes[:]...)
|
||||
out = append(out, payload...)
|
||||
return out
|
||||
}
|
||||
|
||||
func TestPatchWifiLocation(t *testing.T) {
|
||||
payload := writeLengthDelimited(2, testWifiDevice(testLocation(100, 200, 25)))
|
||||
body := testFrame(payload)
|
||||
c := wlocCoords{Latitude: 31.230416, Longitude: 121.473701, Accuracy: 50}
|
||||
|
||||
patched, stats, err := patchWlocBody(body, c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stats.WiFi != 1 || stats.Locations != 1 {
|
||||
t.Fatalf("unexpected stats: %+v", stats)
|
||||
}
|
||||
if bytes.Equal(patched, body) {
|
||||
t.Fatal("body was not patched")
|
||||
}
|
||||
|
||||
newLen := int(binary.BigEndian.Uint16(patched[8:10]))
|
||||
newPayload := patched[10 : 10+newLen]
|
||||
latBytes := append(writeTag(1, wireVarint), writeVarint(uint64(int64(math.Round(c.Latitude*1e8))))...)
|
||||
if !bytes.Contains(newPayload, latBytes) {
|
||||
t.Fatal("new latitude bytes not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchCellLocation(t *testing.T) {
|
||||
cell := writeLengthDelimited(5, testLocation(300, 400, 25))
|
||||
payload := writeLengthDelimited(22, cell)
|
||||
body := testFrame(payload)
|
||||
c := wlocCoords{Latitude: 22.544577, Longitude: 113.94114, Accuracy: 25}
|
||||
|
||||
patched, stats, err := patchWlocBody(body, c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stats.Cell != 1 || stats.Locations != 1 {
|
||||
t.Fatalf("unexpected stats: %+v", stats)
|
||||
}
|
||||
if bytes.Equal(patched, body) {
|
||||
t.Fatal("body was not patched")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPatchGzip(t *testing.T) {
|
||||
payload := writeLengthDelimited(2, testWifiDevice(testLocation(100, 200, 25)))
|
||||
var buf bytes.Buffer
|
||||
zw := gzip.NewWriter(&buf)
|
||||
if _, err := zw.Write(testFrame(payload)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := zw.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c := wlocCoords{Latitude: 31.230416, Longitude: 121.473701, Accuracy: 50}
|
||||
|
||||
patched, _, err := patchResponseBody(buf.Bytes(), c)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Equal(patched, testFrame(payload)) {
|
||||
t.Fatal("gzip body was not patched")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransparentBodyUnchanged(t *testing.T) {
|
||||
body := []byte{1, 2, 3, 4}
|
||||
_, _, err := patchResponseBody(body, wlocCoords{Latitude: 31.230416, Longitude: 121.473701, Accuracy: 25})
|
||||
if err == nil {
|
||||
t.Fatal("expected non-patchable body to error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user