diff --git a/Makefile b/Makefile index e98f875..d03b985 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ TARGET_SDK := android23 TARGET_ARCH := aarch64 # optional: armv7a i686 x86_64 CGO_ENABLED := 1 -GO_SRC := $(shell find . -name '*.go') +GO_SRC := $(shell find . -name '*.go' | grep -v '_test.go$$') NDK_TOOLCHAIN := ~/Library/Android/sdk/ndk/$(NDK_VERSION)/toolchains/llvm/prebuilt/$(BUILD_MACHINE)-$(BUILD_ARCH) CC := $(NDK_TOOLCHAIN)/bin/$(TARGET_ARCH)-linux-$(TARGET_SDK)-clang TEST_OUTPUT = '$(shell cd $(BUILD_PATH) && ./test | head -c 12)' diff --git a/dns.go b/dns.go index dc76d98..cec221a 100644 --- a/dns.go +++ b/dns.go @@ -48,14 +48,28 @@ func (ds *dnsservers) add(m map[string][]string) { func (ds *dnsservers) dial(ctx context.Context) (tlsConn *tls.Conn, err error) { ds.RLock() defer ds.RUnlock() + + if dialer.Timeout != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, dialer.Timeout) + defer cancel() + } + + if !dialer.Deadline.IsZero() { + var cancel context.CancelFunc + ctx, cancel = context.WithDeadline(ctx, dialer.Deadline) + defer cancel() + } + var conn net.Conn for host, addrs := range ds.m { for _, addr := range addrs { if !addr.E { continue } - conn, err = net.Dial("tcp", addr.A) + conn, err = dialer.DialContext(ctx, "tcp", addr.A) if err != nil { + addr.E = false // no need to acquire write lock continue } tlsConn = terasu.Use(tls.Client(conn, &tls.Config{ServerName: host})) @@ -63,6 +77,7 @@ func (ds *dnsservers) dial(ctx context.Context) (tlsConn *tls.Conn, err error) { if err == nil { return } + _ = tlsConn.Close() addr.E = false // no need to acquire write lock } } @@ -129,7 +144,7 @@ var dotv4servers = dnsservers{ var resolver = &net.Resolver{ PreferGo: true, - Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + Dial: func(ctx context.Context, _, _ string) (net.Conn, error) { if canUseIPv6.Get() { return dotv6servers.dial(ctx) } diff --git a/dns_test.go b/dns_test.go index fe0daf9..0f9f10a 100644 --- a/dns_test.go +++ b/dns_test.go @@ -16,7 +16,6 @@ func TestResolver(t *testing.T) { t.Fatal(err) } t.Log(addrs) - t.Fail() } func TestDNS(t *testing.T) { diff --git a/go.mod b/go.mod index adebd80..8bb4842 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,10 @@ module comandy go 1.22.1 require ( + github.com/FloatTech/ttl v0.0.0-20230307105452-d6f7b2b647d1 github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e - github.com/fumiama/terasu v0.0.0-20240414143030-44fae3a81905 + github.com/fumiama/terasu v0.0.0-20240415131749-e65650a52c3c + golang.org/x/net v0.24.0 ) + +require golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index 2a661f4..5d63b53 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,18 @@ +github.com/FloatTech/ttl v0.0.0-20230307105452-d6f7b2b647d1 h1:g4pTnDJUW4VbJ9NvoRfUvdjDrHz/6QhfN/LoIIpICbo= +github.com/FloatTech/ttl v0.0.0-20230307105452-d6f7b2b647d1/go.mod h1:fHZFWGquNXuHttu9dUYoKuNbm3dzLETnIOnm1muSfDs= github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e h1:wR3MXQ3VbUlPKOOUwLOYgh/QaJThBTYtsl673O3lqSA= github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e/go.mod h1:vD7Ra3Q9onRtojoY5sMCLQ7JBgjUsrXDnDKyFxqpf9w= -github.com/fumiama/terasu v0.0.0-20240414143030-44fae3a81905 h1:PHf84+ujLpFGJbfytrwZT6/D7KojmjFm5Itv6te6WUA= -github.com/fumiama/terasu v0.0.0-20240414143030-44fae3a81905/go.mod h1:BFl0X1+rGJf8bLHl/kO+v05ryHrj/R4kyCrK89NvegA= +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/fumiama/terasu v0.0.0-20240415131749-e65650a52c3c h1:RxkHkeanPDrZrEEcUcosgBULmL8UDkSasvwP+jpdIZQ= +github.com/fumiama/terasu v0.0.0-20240415131749-e65650a52c3c/go.mod h1:BFl0X1+rGJf8bLHl/kO+v05ryHrj/R4kyCrK89NvegA= +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.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/http.go b/http.go index 30624cb..8df0090 100644 --- a/http.go +++ b/http.go @@ -5,36 +5,81 @@ import ( "crypto/tls" "encoding/base64" "encoding/json" + "errors" + "fmt" + "io" "net" "net/http" + "reflect" "strings" + "sync" "time" + "github.com/FloatTech/ttl" "github.com/fumiama/terasu" + "golang.org/x/net/http2" ) var dialer = net.Dialer{ Timeout: time.Minute, } -var cli = http.Client{ - Transport: &http.Transport{ - DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - conn, err := dialer.DialContext(ctx, "tcp", addr) +var lookupTable = ttl.NewCache[string, []string](time.Hour) + +type comandyClient http.Client + +var cli = comandyClient(http.Client{ + Transport: &http2.Transport{ + DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { + if dialer.Timeout != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, dialer.Timeout) + defer cancel() + } + + if !dialer.Deadline.IsZero() { + var cancel context.CancelFunc + ctx, cancel = context.WithDeadline(ctx, dialer.Deadline) + defer cancel() + } + + host, port, err := net.SplitHostPort(addr) if err != nil { return nil, err } - host, _, err := net.SplitHostPort(addr) - if err != nil { - return nil, err + addrs := lookupTable.Get(host) + if len(addrs) == 0 { + addrs, err = resolver.LookupHost(ctx, host) + if err != nil { + return nil, err + } + lookupTable.Set(host, addrs) } - return terasu.Use(tls.Client(conn, &tls.Config{ - ServerName: host, - InsecureSkipVerify: true, - })), nil + if len(addr) == 0 { + return nil, errors.New("empty host addr") + } + var tlsConn *tls.Conn + for _, a := range addrs { + if strings.Contains(a, ":") { + a = "[" + a + "]:" + port + } else { + a += ":" + port + } + conn, err := dialer.DialContext(ctx, network, a) + if err != nil { + continue + } + tlsConn = terasu.Use(tls.Client(conn, cfg)) + err = tlsConn.HandshakeContext(ctx) + if err == nil { + break + } + _ = tlsConn.Close() + } + return tlsConn, nil }, }, -} +}) type capsule struct { C int `json:"code,omitempty"` @@ -59,3 +104,77 @@ func (r *capsule) printstrerr(err string) string { _ = json.NewEncoder(&buf).Encode(r) return buf.String() } + +func (cli *comandyClient) request(para string) string { + r := capsule{} + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + defer wg.Done() + _ = canUseIPv6.Get() + }() + err := json.Unmarshal(stringToBytes(para), &r) + if err != nil { + return r.printerr(err) + } + if r.U == "" || !strings.HasPrefix(r.U, "https://") { + return r.printstrerr("invalid url '" + r.U + "'") + } + if r.M != "GET" && r.M != "POST" && r.M != "DELETE" { + return r.printstrerr("invalid method '" + r.U + "'") + } + var body io.Reader + if len(r.D) > 0 { + body = strings.NewReader(r.D) + } + req, err := http.NewRequest(r.M, r.U, body) + if err != nil { + return r.printerr(err) + } + for k, vs := range r.H { + lk := strings.ToLower(k) + if strings.HasPrefix(lk, "x-") { + continue + } + switch x := vs.(type) { + case string: + req.Header.Add(k, x) + case []string: + for _, v := range x { + req.Header.Add(k, v) + } + default: + return r.printstrerr("unsupported H type " + reflect.ValueOf(x).Type().Name()) + } + } + fmt.Println(r.U) + wg.Wait() + resp, err := (*http.Client)(cli).Do(req) + if err != nil { + return r.printerr(err) + } + defer resp.Body.Close() + sb := strings.Builder{} + enc := base64.NewEncoder(base64.StdEncoding, &sb) + _, err = io.Copy(enc, resp.Body) + _ = enc.Close() + if err != nil { + return r.printerr(err) + } + r.C = resp.StatusCode + r.H = make(map[string]any, len(resp.Header)*2) + for k, vs := range resp.Header { + if len(vs) == 1 { + r.H[k] = vs[0] + continue + } + r.H[k] = vs + } + r.D = sb.String() + outbuf := strings.Builder{} + err = json.NewEncoder(&outbuf).Encode(&r) + if err != nil { + return r.printerr(err) + } + return outbuf.String() +} diff --git a/http_test.go b/http_test.go new file mode 100644 index 0000000..1f6ed00 --- /dev/null +++ b/http_test.go @@ -0,0 +1,49 @@ +package main + +import ( + "encoding/json" + "io" + "net/http" + "testing" +) + +func TestClientGet(t *testing.T) { + _ = canUseIPv6.Get() + req, err := http.NewRequest("GET", "https://api.mangacopy.com/api/v3/h5/homeIndex?platform=3", nil) + if err != nil { + t.Fatal(err) + } + resp, err := (*http.Client)(&cli).Do(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + t.Log("[T] response code", resp.StatusCode) + for k, vs := range resp.Header { + for _, v := range vs { + t.Log("[T] response header", k+":", v) + } + } + data, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + t.Log(bytesToString(data)) + t.Fail() +} + +func TestRequest(t *testing.T) { + r := cli.request(`{"code":0,"headers":{"authorization":"Token ","host":"api.mangacopy.com","source":"copyApp","webp":"1","region":"1","version":"2.1.7","platform":"3","user-agent":"COPY/2.1.7"},"method":"GET","url":"https://api.mangacopy.com/api/v3/h5/homeIndex?platform\u003d3"}`) + t.Log(r) + c := capsule{} + err := json.Unmarshal(stringToBytes(r), &c) + if err != nil { + t.Fatal(err) + } + if c.C != http.StatusOK { + t.Fatal("status code", c.C) + } + if len(c.D) == 0 { + t.Fatal("empty data") + } +} diff --git a/main.go b/main.go index 3da8bad..0d27e8b 100644 --- a/main.go +++ b/main.go @@ -3,12 +3,7 @@ package main import "C" import ( - "encoding/base64" "encoding/json" - "io" - "net/http" - "reflect" - "strings" ) func main() {} @@ -45,67 +40,5 @@ func add_dns(para *C.char, is_ipv6 C.int) *C.char { // //export request func request(para *C.char) *C.char { - r := capsule{} - err := json.Unmarshal(stringToBytes(C.GoString(para)), &r) - if err != nil { - return C.CString(r.printerr(err)) - } - if r.U == "" || !strings.HasPrefix(r.U, "https://") { - return C.CString(r.printstrerr("invalid url '" + r.U + "'")) - } - if r.M != "GET" && r.M != "POST" && r.M != "DELETE" { - return C.CString(r.printstrerr("invalid method '" + r.U + "'")) - } - var body io.Reader - if len(r.D) > 0 { - body = strings.NewReader(r.D) - } - req, err := http.NewRequest(r.M, r.U, body) - if err != nil { - return C.CString(r.printerr(err)) - } - for k, vs := range r.H { - lk := strings.ToLower(k) - if strings.HasPrefix(lk, "x-") { - continue - } - switch x := vs.(type) { - case string: - req.Header.Add(k, x) - case []string: - for _, v := range x { - req.Header.Add(k, v) - } - default: - return C.CString(r.printstrerr("unsupported H type " + reflect.ValueOf(x).Type().Name())) - } - } - resp, err := cli.Do(req) - if err != nil { - return C.CString(r.printerr(err)) - } - defer resp.Body.Close() - sb := strings.Builder{} - enc := base64.NewEncoder(base64.StdEncoding, &sb) - _, err = io.CopyN(enc, resp.Body, resp.ContentLength) - _ = enc.Close() - if err != nil { - return C.CString(r.printerr(err)) - } - r.C = resp.StatusCode - r.H = make(map[string]any, len(resp.Header)*2) - for k, vs := range resp.Header { - if len(vs) == 1 { - r.H[k] = vs[0] - continue - } - r.H[k] = vs - } - r.D = sb.String() - outbuf := strings.Builder{} - err = json.NewEncoder(&outbuf).Encode(&r) - if err != nil { - return C.CString(r.printerr(err)) - } - return C.CString(outbuf.String()) + return C.CString(cli.request(C.GoString(para))) } diff --git a/utils.go b/utils.go index edf0da1..d898d1d 100644 --- a/utils.go +++ b/utils.go @@ -14,12 +14,10 @@ type slice struct { cap int } -/* // bytesToString 没有内存开销的转换 func bytesToString(b []byte) string { return *(*string)(unsafe.Pointer(&b)) } -*/ // stringToBytes 没有内存开销的转换 func stringToBytes(s string) (b []byte) {