commit f40ace40583c3a87200c6a8aeba14f40a9d31230 Author: imxyy_soope_ Date: Sun Nov 23 12:12:20 2025 +0800 initial go implementation diff --git a/.direnv/flake-profile b/.direnv/flake-profile new file mode 120000 index 0000000..0c05709 --- /dev/null +++ b/.direnv/flake-profile @@ -0,0 +1 @@ +flake-profile-1-link \ No newline at end of file diff --git a/.direnv/flake-profile-1-link b/.direnv/flake-profile-1-link new file mode 120000 index 0000000..94004d0 --- /dev/null +++ b/.direnv/flake-profile-1-link @@ -0,0 +1 @@ +/nix/store/4rpkss7h2hfizljr8f462kl90if6vn2k-nix-shell-env \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..33a7142 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.env +/*.log diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go new file mode 100644 index 0000000..c1cb948 --- /dev/null +++ b/cmd/proxy/main.go @@ -0,0 +1,177 @@ +package main + +import ( + "bytes" + "compress/gzip" + "context" + "crypto/tls" + "encoding/json" + "flag" + "fmt" + "io" + "log" + "net/http" + "net/http/httputil" + "net/url" + "os" + "strconv" + "strings" + "sync" + "time" + + "git.imxyy.top/imxyy1soope1/MyLinspirer/internal/utils/crypto" + "git.imxyy.top/imxyy1soope1/MyLinspirer/internal/utils/format" +) + +const ( + targetURL = "https://cloud.linspirer.com:883" +) + +var ( + proxy *httputil.ReverseProxy + host string + port string + + key string + iv string + + once sync.Once +) + +func init() { + once.Do(func() { + key = os.Getenv("LINSPIRER_KEY") + iv = os.Getenv("LINSPIRER_IV") + + if key == "" || iv == "" { + log.Fatalf("LINSPIRER_KEY or LINSPIRER_IV is not set") + } + }) +} + +func main() { + flag.StringVar(&host, "a", "", "listening host") + flag.StringVar(&port, "p", "8080", "listening port") + flag.Parse() + + portNum, err := strconv.Atoi(port) + if err != nil { + log.Fatalf("invalid port: %v", err) + } + if portNum < 1 || portNum > 65535 { + log.Fatalf("port out of range (1-65535): %d", portNum) + } + + addr := host + ":" + port + if host == "" { + addr = ":" + port + } + + target, _ := url.Parse(targetURL) + proxy = &httputil.ReverseProxy{ + Rewrite: func(req *httputil.ProxyRequest) { + startTime := time.Now() + recordRequest(req, startTime) + req.SetURL(target) + }, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + ModifyResponse: logResponse, + } + + log.Printf("Proxy started on %s => %s", addr, targetURL) + log.Fatal(http.ListenAndServe(addr, http.HandlerFunc(proxy.ServeHTTP))) +} + +func recordRequest(req *httputil.ProxyRequest, startTime time.Time) { + if req.Out.Body == nil { + return + } + body, _ := io.ReadAll(req.Out.Body) + req.Out.Body = io.NopCloser(bytes.NewBuffer(body)) + + var requestData map[string]any + if err := json.Unmarshal(body, &requestData); err == nil { + if paramsEnc, ok := requestData["params"].(string); ok { + if decrypted, err := crypto.Decrypt(paramsEnc, key, iv); err == nil { + var unmarshaledParams map[string]any + err = json.Unmarshal([]byte(decrypted), &unmarshaledParams) + if err == nil { + requestData["params"] = unmarshaledParams + } else { + requestData["params"] = decrypted + } + } else { + requestData["params"] = fmt.Sprintf("decrypt failed: %v", err) + } + } + } else { + requestData = map[string]any{"request": fmt.Sprintf("JSON parse error: %v", err)} + } + + ctx := context.WithValue(req.Out.Context(), "startTime", startTime) + ctx = context.WithValue(ctx, "decryptedRequest", requestData) + + req.Out = req.Out.WithContext(ctx) +} + +func extractContextValue[T any](ctx context.Context, name string) (val T, ok bool) { + valAny := ctx.Value(name) + if valAny == nil { + log.Printf("[ERROR] %s not found in context", name) + return + } + val, ok = valAny.(T) + if !ok { + log.Printf("[ERROR] invalid %s type: %T", name, valAny) + return + } + return +} + +func logResponse(resp *http.Response) error { + startTime, timeOk := extractContextValue[time.Time](resp.Request.Context(), "startTime") + decryptedRequest, reqOk := extractContextValue[map[string]any](resp.Request.Context(), "decryptedRequest") + if !timeOk || !reqOk { + return nil + } + + body, _ := io.ReadAll(resp.Body) + resp.Body = io.NopCloser(bytes.NewBuffer(body)) + + var response []byte + if resp.Header.Get("Content-Encoding") == "gzip" { + gzReader, err := gzip.NewReader(bytes.NewReader(body)) + if err == nil { + defer gzReader.Close() + if decompressed, err := io.ReadAll(gzReader); err == nil { + response = decompressed + } else { + fmt.Appendf(response, "gzip decompress failed: %v", err) + } + } else { + fmt.Appendf(response, "gzip init failed: %v", err) + } + } else { + response = body + } + + respPlaintext := "N/A" + if decrypted, err := crypto.Decrypt(string(response), key, iv); err == nil { + respPlaintext = format.FormatJSON(decrypted) + } else { + respPlaintext = fmt.Sprintf("decrypt failed: %v", err) + } + + requestJSON, _ := json.MarshalIndent(decryptedRequest, "", " ") + log.Printf("[%s] %s\nRequest:\n%s\nResponse:\n%s\n%s\n", + startTime.Format("2006/01/02 15:04:05"), + decryptedRequest["method"].(string), + format.FormatJSON(string(requestJSON)), + respPlaintext, + strings.Repeat("-", 80), + ) + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ea1b7e0 --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module git.imxyy.top/imxyy1soope1/MyLinspirer + +go 1.24.1 + +require ( + github.com/bytedance/sonic v1.13.2 // indirect + github.com/bytedance/sonic/loader v0.2.4 // indirect + github.com/cloudwego/base64x v0.1.5 // indirect + github.com/klauspost/cpuid/v2 v2.0.9 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..171d971 --- /dev/null +++ b/go.sum @@ -0,0 +1,29 @@ +github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= +github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= +github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= +github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= diff --git a/internal/utils/crypto/decrypt.go b/internal/utils/crypto/decrypt.go new file mode 100644 index 0000000..632beee --- /dev/null +++ b/internal/utils/crypto/decrypt.go @@ -0,0 +1,52 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "errors" + "fmt" +) + +func Decrypt(ciphertextB64, key, iv string) (string, error) { + ciphertext, err := base64.StdEncoding.DecodeString(ciphertextB64) + if err != nil { + return "", fmt.Errorf("base64 decode failed: %v", err) + } + + block, err := aes.NewCipher([]byte(key)) + if err != nil { + return "", fmt.Errorf("AES init failed: %v", err) + } + + if len(iv) != aes.BlockSize { + return "", fmt.Errorf("IV must be %d bytes", aes.BlockSize) + } + + mode := cipher.NewCBCDecrypter(block, []byte(iv)) + plaintext := make([]byte, len(ciphertext)) + mode.CryptBlocks(plaintext, ciphertext) + + unpadded, err := pkcs7Unpad(plaintext) + if err != nil { + return "", fmt.Errorf("PKCS7 unpadding failed: %v", err) + } + + return string(unpadded), nil +} + +func pkcs7Unpad(data []byte) ([]byte, error) { + if len(data) == 0 { + return nil, errors.New("empty data") + } + padSize := int(data[len(data)-1]) + if padSize > len(data) || padSize > aes.BlockSize { + return nil, fmt.Errorf("invalid padding size: %d", padSize) + } + for i := range padSize { + if data[len(data)-1-i] != byte(padSize) { + return nil, errors.New("invalid padding content") + } + } + return data[:len(data)-padSize], nil +} diff --git a/internal/utils/crypto/encrypt.go b/internal/utils/crypto/encrypt.go new file mode 100644 index 0000000..15bc572 --- /dev/null +++ b/internal/utils/crypto/encrypt.go @@ -0,0 +1,41 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "fmt" +) + +func Encrypt(plaintext, key, iv string) (string, error) { + padded := pkcs7Pad([]byte(plaintext)) + + block, err := aes.NewCipher([]byte(key)) + if err != nil { + return "", fmt.Errorf("AES init failed: %v", err) + } + + if len(iv) != aes.BlockSize { + return "", fmt.Errorf("IV must be %d bytes", aes.BlockSize) + } + + mode := cipher.NewCBCEncrypter(block, []byte(iv)) + ciphertext := make([]byte, len(padded)) + + mode.CryptBlocks(ciphertext, padded) + + ciphertextB64 := base64.StdEncoding.EncodeToString(ciphertext) + + return ciphertextB64, nil +} + +func pkcs7Pad(buf []byte) []byte { + bufLen := len(buf) + padLen := aes.BlockSize - bufLen%aes.BlockSize + padded := make([]byte, bufLen+padLen) + copy(padded, buf) + for i := range padLen { + padded[bufLen+i] = byte(padLen) + } + return padded +} diff --git a/internal/utils/format/json.go b/internal/utils/format/json.go new file mode 100644 index 0000000..c67170a --- /dev/null +++ b/internal/utils/format/json.go @@ -0,0 +1,31 @@ +package format + +import ( + "bytes" + "encoding/json" +) + +func FormatJSON(input string) string { + var data any + if err := json.Unmarshal([]byte(input), &data); err != nil { + var out bytes.Buffer + if err := json.Indent(&out, []byte(input), "", " "); err == nil { + return out.String() + } + return input + } + + var out bytes.Buffer + encoder := json.NewEncoder(&out) + encoder.SetEscapeHTML(false) + encoder.SetIndent("", " ") + if err := encoder.Encode(data); err != nil { + return input + } + + formatted := out.String() + if len(formatted) > 0 && formatted[len(formatted)-1] == '\n' { + formatted = formatted[:len(formatted)-1] + } + return formatted +} diff --git a/pkg/types/params.go b/pkg/types/params.go new file mode 100644 index 0000000..0de2ec1 --- /dev/null +++ b/pkg/types/params.go @@ -0,0 +1,5 @@ +package types + +type Params interface { + param() +} diff --git a/pkg/types/request.go b/pkg/types/request.go new file mode 100644 index 0000000..1e379f3 --- /dev/null +++ b/pkg/types/request.go @@ -0,0 +1,10 @@ +package types + +type Request struct { + Version int `json:"!version"` + ClientVersion string `json:"client_version"` + Id int `json:"id"` + JsonRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params Params `json:"params"` +} diff --git a/pkg/types/response.go b/pkg/types/response.go new file mode 100644 index 0000000..bdc4f95 --- /dev/null +++ b/pkg/types/response.go @@ -0,0 +1,7 @@ +package types + +type Response struct { + Code int32 `json:"code"` + Type string `json:"type"` + Data string `json:"data"` +}