diff --git a/Makefile b/Makefile index d10c954..3ec4501 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,6 @@ +# Tested under Apple M1. +# Edit it if you use different platform. + PROJECT_NAME := comandy BUILD_PATH := build GOOS := android @@ -11,12 +14,12 @@ CGO_ENABLED := 1 GO_SRC := $(shell find . -name '*.go') NDK_TOOLCHAIN := ~/Library/Android/sdk/ndk/$(NDK_VERSION)/toolchains/llvm/prebuilt/$(BUILD_MACHINE)-$(BUILD_ARCH) CC := $(NDK_TOOLCHAIN)/bin/aarch64-linux-$(TARGET_SDK)-clang -TEST_OUTPUT = '$(shell cd $(BUILD_PATH) && ./test)' -TEST_EXPECTED = '{"code":500,"data":"aW52YWxpZCB1cmwgJyc="}' +TEST_OUTPUT = '$(shell cd $(BUILD_PATH) && ./test | head -c 12)' +TEST_EXPECTED = '{"code":200,' all: shared -shared: $(GO_SRC) dir +shared: $(GO_SRC) dir tidy GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=$(CGO_ENABLED) NDK_TOOLCHAIN=$(NDK_TOOLCHAIN) CC=$(CC) go build -buildmode=c-shared -o $(BUILD_PATH)/lib$(PROJECT_NAME).so $(GO_SRC) test: dir @GOOS=$(BUILD_MACHINE) CC=cc NDK_TOOLCHAIN="" $(MAKE) -e shared @@ -27,7 +30,12 @@ runtest: test else \ echo "test failed, expected:" $(TEST_EXPECTED) "but got:" $(TEST_OUTPUT); \ fi +tidy: + go mod tidy dir: @if [ ! -d "$(BUILD_PATH)" ]; then mkdir $(BUILD_PATH); fi clean: - @if [ -d "$(BUILD_PATH)" ]; then rm -rf $(BUILD_PATH)/lib$(PROJECT_NAME).*; fi + @if [ -d "$(BUILD_PATH)" ]; then \ + rm -rf $(BUILD_PATH)/lib$(PROJECT_NAME).*; \ + rm -rf $(BUILD_PATH)/test; \ + fi diff --git a/build/test.c b/build/test.c index 921fc89..c157b3e 100644 --- a/build/test.c +++ b/build/test.c @@ -3,6 +3,11 @@ #include "libcomandy.h" int main() { - char* msg = request("{}"); - puts(msg); + puts(request("{\"method\":\"GET\"," + "\"url\":\"https://i.pximg.net/img-master/img/2012/04/04/21/24/46/26339586_p0_master1200.jpg\"," + "\"headers\":{" + "\"Referer\":\"https://www.pixiv.net/\"," + "\"User-Agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0\"" + "}" + "}")); } diff --git a/dns.go b/dns.go new file mode 100644 index 0000000..dc76d98 --- /dev/null +++ b/dns.go @@ -0,0 +1,138 @@ +package main + +import ( + "context" + "crypto/tls" + "net" + "sync" + + "github.com/fumiama/terasu" +) + +type dnsstat struct { + A string + E bool +} + +type dnsservers struct { + sync.RWMutex + m map[string][]*dnsstat +} + +// hasrecord no lock, use under lock +func hasrecord(lst []*dnsstat, a string) bool { + for _, addr := range lst { + if addr.A == a { + return true + } + } + return false +} + +func (ds *dnsservers) add(m map[string][]string) { + ds.Lock() + defer ds.Unlock() + addList := map[string][]*dnsstat{} + for host, addrs := range m { + for _, addr := range addrs { + if !hasrecord(ds.m[host], addr) && !hasrecord(addList[host], addr) { + addList[host] = append(addList[host], &dnsstat{addr, true}) + } + } + } + for host, addrs := range addList { + ds.m[host] = append(ds.m[host], addrs...) + } +} + +func (ds *dnsservers) dial(ctx context.Context) (tlsConn *tls.Conn, err error) { + ds.RLock() + defer ds.RUnlock() + 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) + if err != nil { + continue + } + tlsConn = terasu.Use(tls.Client(conn, &tls.Config{ServerName: host})) + err = tlsConn.HandshakeContext(ctx) + if err == nil { + return + } + addr.E = false // no need to acquire write lock + } + } + return +} + +var dotv6servers = dnsservers{ + m: map[string][]*dnsstat{ + "dns.alidns.com": { + {"[2400:3200::1]:853", true}, + {"[2400:3200:baba::1]:853", true}, + }, + "dot.sb": { + {"[2a09::]:853", true}, + {"[2a11::]:853", true}, + }, + "dns.google": { + {"[2001:4860:4860::8888]:853", true}, + {"[2001:4860:4860::8844]:853", true}, + }, + "cloudflare-dns.com": { + {"[2606:4700:4700::1111]:853", true}, + {"[2606:4700:4700::1001]:853", true}, + }, + "dns.umbrella.com": { + {"[2620:0:ccc::2]:853", true}, + {"[2620:0:ccd::2]:853", true}, + }, + "dns10.quad9.net": { + {"[2620:fe::10]:853", true}, + {"[2620:fe::fe:10]:853", true}, + }, + }, +} + +var dotv4servers = dnsservers{ + m: map[string][]*dnsstat{ + "dns.alidns.com": { + {"223.5.5.5:853", true}, + {"223.6.6.6:853", true}, + }, + "dot.sb": { + {"185.222.222.222:853", true}, + {"45.11.45.11:853", true}, + }, + "dns.google": { + {"8.8.8.8:853", true}, + {"8.8.4.4:853", true}, + }, + "cloudflare-dns.com": { + {"1.1.1.1:853", true}, + {"1.0.0.1:853", true}, + }, + "dns.umbrella.com": { + {"208.67.222.222:853", true}, + {"208.67.220.220:853", true}, + }, + "dns10.quad9.net": { + {"9.9.9.10:853", true}, + {"149.112.112.10:853", true}, + }, + }, +} + +var resolver = &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + if canUseIPv6.Get() { + return dotv6servers.dial(ctx) + } + return dotv4servers.dial(ctx) + }, +} diff --git a/dns_test.go b/dns_test.go new file mode 100644 index 0000000..fe0daf9 --- /dev/null +++ b/dns_test.go @@ -0,0 +1,51 @@ +package main + +import ( + "context" + "crypto/tls" + "net" + "testing" + + "github.com/fumiama/terasu" +) + +func TestResolver(t *testing.T) { + t.Log("canUseIPv6:", canUseIPv6.Get()) + addrs, err := resolver.LookupHost(context.TODO(), "dns.google") + if err != nil { + t.Fatal(err) + } + t.Log(addrs) + t.Fail() +} + +func TestDNS(t *testing.T) { + if canUseIPv6.Get() { + dotv6servers.test(t) + } + dotv4servers.test(t) +} + +func (ds *dnsservers) test(t *testing.T) { + ds.RLock() + defer ds.RUnlock() + for host, addrs := range ds.m { + for _, addr := range addrs { + if !addr.E { + continue + } + conn, err := net.Dial("tcp", addr.A) + if err != nil { + continue + } + tlsConn := terasu.Use(tls.Client(conn, &tls.Config{ServerName: host})) + err = tlsConn.Handshake() + _ = tlsConn.Close() + if err == nil { + t.Log("succ:", host, addr.A) + continue + } + t.Fatal("fail:", host, addr.A) + } + } +} diff --git a/go.mod b/go.mod index 1de9e01..adebd80 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module comandy go 1.22.1 -require github.com/fumiama/terasu v0.0.0-20240414143030-44fae3a81905 +require ( + github.com/RomiChan/syncx v0.0.0-20221202055724-5f842c53020e + github.com/fumiama/terasu v0.0.0-20240414143030-44fae3a81905 +) diff --git a/go.sum b/go.sum index f5e7d94..2a661f4 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ +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= diff --git a/http.go b/http.go new file mode 100644 index 0000000..30624cb --- /dev/null +++ b/http.go @@ -0,0 +1,61 @@ +package main + +import ( + "context" + "crypto/tls" + "encoding/base64" + "encoding/json" + "net" + "net/http" + "strings" + "time" + + "github.com/fumiama/terasu" +) + +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) + if err != nil { + return nil, err + } + host, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + return terasu.Use(tls.Client(conn, &tls.Config{ + ServerName: host, + InsecureSkipVerify: true, + })), nil + }, + }, +} + +type capsule struct { + C int `json:"code,omitempty"` + M string `json:"method,omitempty"` + U string `json:"url,omitempty"` + H map[string]any `json:"headers,omitempty"` + D string `json:"data,omitempty"` +} + +func (r *capsule) printerr(err error) string { + buf := strings.Builder{} + r.C = http.StatusInternalServerError + r.D = base64.StdEncoding.EncodeToString(stringToBytes(err.Error())) + _ = json.NewEncoder(&buf).Encode(r) + return buf.String() +} + +func (r *capsule) printstrerr(err string) string { + buf := strings.Builder{} + r.C = http.StatusInternalServerError + r.D = base64.StdEncoding.EncodeToString(stringToBytes(err)) + _ = json.NewEncoder(&buf).Encode(r) + return buf.String() +} diff --git a/ipv6.go b/ipv6.go new file mode 100644 index 0000000..55fb808 --- /dev/null +++ b/ipv6.go @@ -0,0 +1,16 @@ +package main + +import ( + "net/http" + + "github.com/RomiChan/syncx" +) + +var canUseIPv6 = syncx.Lazy[bool]{Init: func() bool { + resp, err := http.Get("http://v6.ipv6-test.com/json/widgetdata.php?callback=?") + if err != nil { + return false + } + _ = resp.Body.Close() + return true +}} diff --git a/main.go b/main.go index 0f47732..3da8bad 100644 --- a/main.go +++ b/main.go @@ -3,69 +3,46 @@ package main import "C" import ( - "context" - "crypto/tls" "encoding/base64" "encoding/json" "io" - "net" "net/http" "reflect" "strings" - "time" - - "github.com/fumiama/terasu" ) func main() {} -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) - if err != nil { - return nil, err - } - host, _, err := net.SplitHostPort(addr) - if err != nil { - return nil, err - } - return terasu.Use(tls.Client(conn, &tls.Config{ - ServerName: host, - InsecureSkipVerify: true, - })), nil - }, - }, -} - -type capsule struct { - C int `json:"code,omitempty"` - M string `json:"method,omitempty"` - U string `json:"url,omitempty"` - H map[string]any `json:"headers,omitempty"` - D string `json:"data,omitempty"` -} - -func (r *capsule) printerr(err error) string { - buf := strings.Builder{} - r.C = http.StatusInternalServerError - r.D = base64.StdEncoding.EncodeToString(stringToBytes(err.Error())) - _ = json.NewEncoder(&buf).Encode(r) - return buf.String() -} - -func (r *capsule) printstrerr(err string) string { - buf := strings.Builder{} - r.C = http.StatusInternalServerError - r.D = base64.StdEncoding.EncodeToString(stringToBytes(err)) - _ = json.NewEncoder(&buf).Encode(r) - return buf.String() +// para: json of map[host string][]addr:port string +// +//export add_dns +func add_dns(para *C.char, is_ipv6 C.int) *C.char { + m := map[string][]string{} + err := json.Unmarshal(stringToBytes(C.GoString(para)), &m) + if err != nil { + return C.CString(err.Error()) + } + if is_ipv6 != 0 { + if !canUseIPv6.Get() { + return C.CString("cannot use ipv6") + } + dotv6servers.add(m) + return nil + } + dotv4servers.add(m) + return nil } +// para: +// +// request("{\"method\":\"GET\"," +// "\"url\":\"https://i.pximg.net/img-master/img/2012/04/04/21/24/46/26339586_p0_master1200.jpg\"," +// "\"headers\":{" +// "\"Referer\":\"https://www.pixiv.net/\"," +// "\"User-Agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0\"" +// "}" +// "}"); +// //export request func request(para *C.char) *C.char { r := capsule{} @@ -73,13 +50,17 @@ func request(para *C.char) *C.char { if err != nil { return C.CString(r.printerr(err)) } - if r.U == "" || !strings.HasPrefix(r.U, "http") { + 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 + "'")) } - req, err := http.NewRequest(r.M, r.U, strings.NewReader(r.D)) + 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)) }