initial go implementation
This commit is contained in:
1
.direnv/flake-profile
Symbolic link
1
.direnv/flake-profile
Symbolic link
@@ -0,0 +1 @@
|
||||
flake-profile-1-link
|
||||
1
.direnv/flake-profile-1-link
Symbolic link
1
.direnv/flake-profile-1-link
Symbolic link
@@ -0,0 +1 @@
|
||||
/nix/store/4rpkss7h2hfizljr8f462kl90if6vn2k-nix-shell-env
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/.env
|
||||
/*.log
|
||||
177
cmd/proxy/main.go
Normal file
177
cmd/proxy/main.go
Normal 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
12
go.mod
Normal 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
29
go.sum
Normal 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=
|
||||
52
internal/utils/crypto/decrypt.go
Normal file
52
internal/utils/crypto/decrypt.go
Normal 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
|
||||
}
|
||||
41
internal/utils/crypto/encrypt.go
Normal file
41
internal/utils/crypto/encrypt.go
Normal 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
|
||||
}
|
||||
31
internal/utils/format/json.go
Normal file
31
internal/utils/format/json.go
Normal 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
5
pkg/types/params.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package types
|
||||
|
||||
type Params interface {
|
||||
param()
|
||||
}
|
||||
10
pkg/types/request.go
Normal file
10
pkg/types/request.go
Normal 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
7
pkg/types/response.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package types
|
||||
|
||||
type Response struct {
|
||||
Code int32 `json:"code"`
|
||||
Type string `json:"type"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
Reference in New Issue
Block a user