initial go implementation

This commit is contained in:
2025-11-23 12:12:20 +08:00
commit f40ace4058
12 changed files with 368 additions and 0 deletions

1
.direnv/flake-profile Symbolic link
View File

@@ -0,0 +1 @@
flake-profile-1-link

View File

@@ -0,0 +1 @@
/nix/store/4rpkss7h2hfizljr8f462kl90if6vn2k-nix-shell-env

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/.env
/*.log

177
cmd/proxy/main.go Normal file
View File

@@ -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
}

12
go.mod Normal file
View File

@@ -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
)

29
go.sum Normal file
View File

@@ -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=

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

5
pkg/types/params.go Normal file
View File

@@ -0,0 +1,5 @@
package types
type Params interface {
param()
}

10
pkg/types/request.go Normal file
View File

@@ -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"`
}

7
pkg/types/response.go Normal file
View File

@@ -0,0 +1,7 @@
package types
type Response struct {
Code int32 `json:"code"`
Type string `json:"type"`
Data string `json:"data"`
}