├── test ├── .gitignore ├── config │ ├── sing-box.json │ └── sing-box-quic.json ├── go.mod ├── go.sum └── quic_test.go ├── include_cgo.go ├── naive_client_cgo.go ├── .gitmodules ├── cmd └── build-naive │ ├── main.go │ ├── cmd_download_toolchain.go │ ├── cmd_sync.go │ ├── cmd_env.go │ └── cmd_extract_lib.go ├── naive_client_purego.go ├── loader_purego.go ├── .gitignore ├── internal └── cronet │ ├── api_purego_nonwindows.go │ ├── api_purego_float.go │ ├── api_purego_windows.go │ ├── loader_windows_float.go │ └── api_purego_float_stub.go ├── date_time_test.go ├── upload_data_provider_purego.go ├── url_request_callback_purego.go ├── dns_types.go ├── runnable_purego.go ├── executor_purego.go ├── buffer_callback_purego.go ├── engine_version_test.go ├── go.mod ├── upload_data_provider_cgo.go ├── url_request_callback_cgo.go ├── date_time_purego.go ├── url_request_status_listener_purego.go ├── url_request_status_listener_cgo.go ├── runnable_cgo.go ├── url_request_finished_info_listener_purego.go ├── url_request_finished_info_listener_cgo.go ├── LICENSE ├── http_header_purego.go ├── executor_cgo.go ├── buffer_callback_cgo.go ├── Makefile ├── error_go.go ├── date_time_cgo.go ├── quic_hint_purego.go ├── .golangci.yml ├── url_request_finished_info_purego.go ├── upload_data_sink_purego.go ├── http_header_cgo.go ├── error_purego.go ├── public_key_pins_purego.go ├── url_request_purego.go ├── quic_hint_cgo.go ├── bidirectional_stream_map.go ├── runnable_impl_purego.go ├── url_response_info_purego.go ├── buffer_purego.go ├── buffer_callback_impl_purego.go ├── runnable_impl_cgo.go ├── url_request_status_listener_impl_purego.go ├── executor_impl_cgo.go ├── buffer_callback_impl.go ├── error_mapping_test.go ├── naive_client_fd_unix.go ├── buffer_cgo.go ├── url_request_status_listener_impl_cgo.go ├── executor_impl_purego.go ├── error_codes.go ├── callback_types.go ├── upload_data_sink_cgo.go ├── upload_data_provider_handler.go ├── url_request_finished_info_listener_impl_purego.go ├── net_error.go ├── engine_params_experimental_options.go ├── url_request_finished_info_cgo.go ├── url_request_finished_info_impl_cgo.go ├── public_key_pins_cgo.go ├── error_cgo.go ├── buffer_impl_cgo.go ├── socket_fd_unix_test.go ├── upload_data_provider_impl_purego.go ├── dns_socketpair_unix.go ├── upload_data_sink_impl_cgo.go ├── metrics_purego.go ├── bidirectional_stream_purego.go ├── socket_fd_windows_test.go ├── result.go ├── README.md ├── upload_data_provider_impl_cgo.go ├── url_request_params_purego.go ├── engine_params_purego.go ├── go.sum ├── url_request_callback_impl_purego.go ├── url_request_cgo.go ├── bidirectional_stream_impl_purego.go ├── url_request_impl_cgo.go ├── url_request_callback_impl_purego_32bit.go ├── dialer_test.go ├── engine_impl_cgo.go ├── dns_socketpair_windows_test.go └── dns_socketpair_unix_test.go /test/.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | -------------------------------------------------------------------------------- /include_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #cgo CFLAGS: -I${SRCDIR}/include 6 | import "C" 7 | -------------------------------------------------------------------------------- /naive_client_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | func checkLibrary() error { 6 | return nil 7 | } 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "naiveproxy"] 2 | path = naiveproxy 3 | url = https://github.com/SagerNet/naiveproxy.git 4 | branch = cronet-go 5 | -------------------------------------------------------------------------------- /cmd/build-naive/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "log" 4 | 5 | func main() { 6 | err := mainCommand.Execute() 7 | if err != nil { 8 | log.Fatal(err) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /naive_client_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import "github.com/sagernet/cronet-go/internal/cronet" 6 | 7 | func checkLibrary() error { 8 | return cronet.LoadLibrary("") 9 | } 10 | -------------------------------------------------------------------------------- /loader_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import "github.com/sagernet/cronet-go/internal/cronet" 6 | 7 | func LoadLibrary(path string) error { 8 | return cronet.LoadLibrary(path) 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /build/ 3 | /thinlto-cache/ 4 | .DS_Store 5 | CLAUDE.md 6 | /.claude/ 7 | 8 | # Prebuilt libraries (committed to go branch only) 9 | /build-naive 10 | /lib/ 11 | /include/ 12 | /cgo_*.go 13 | /lib_*_cgo.go 14 | -------------------------------------------------------------------------------- /internal/cronet/api_purego_nonwindows.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego && !windows 2 | 3 | package cronet 4 | 5 | // EngineStartWithParams starts the engine on non-Windows platforms. 6 | func EngineStartWithParams(engine, params uintptr) int32 { 7 | return cronetEngineStartWithParams(engine, params) 8 | } 9 | -------------------------------------------------------------------------------- /date_time_test.go: -------------------------------------------------------------------------------- 1 | package cronet_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/sagernet/cronet-go" 8 | ) 9 | 10 | func TestDateTime(t *testing.T) { 11 | d := cronet.NewDateTime() 12 | m := time.UnixMilli(time.Now().UnixMilli()) 13 | d.SetValue(m) 14 | if d.Value() != m { 15 | t.Fatal("bad time") 16 | } 17 | d.Destroy() 18 | } 19 | -------------------------------------------------------------------------------- /test/config/sing-box.json: -------------------------------------------------------------------------------- 1 | { 2 | "inbounds": [ 3 | { 4 | "type": "naive", 5 | "listen": "::", 6 | "listen_port": 10000, 7 | "users": [ 8 | { 9 | "username": "test", 10 | "password": "test" 11 | } 12 | ], 13 | "tls": { 14 | "enabled": true, 15 | "certificate_path": "/cert.pem", 16 | "key_path": "/key.pem" 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /internal/cronet/api_purego_float.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego && (amd64 || arm64) 2 | 3 | package cronet 4 | 5 | func EngineParamsNetworkThreadPrioritySet(params uintptr, priority float64) { 6 | ensureLoaded() 7 | cronetEngineParamsNetworkThreadPrioritySet(params, priority) 8 | } 9 | 10 | func EngineParamsNetworkThreadPriorityGet(params uintptr) float64 { 11 | ensureLoaded() 12 | return cronetEngineParamsNetworkThreadPriorityGet(params) 13 | } 14 | -------------------------------------------------------------------------------- /test/config/sing-box-quic.json: -------------------------------------------------------------------------------- 1 | { 2 | "inbounds": [ 3 | { 4 | "type": "naive", 5 | "listen": "::", 6 | "listen_port": 10002, 7 | "network": "udp", 8 | "users": [ 9 | { 10 | "username": "test", 11 | "password": "test" 12 | } 13 | ], 14 | "tls": { 15 | "enabled": true, 16 | "certificate_path": "/cert.pem", 17 | "key_path": "/key.pem" 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /upload_data_provider_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "unsafe" 7 | 8 | "github.com/sagernet/cronet-go/internal/cronet" 9 | ) 10 | 11 | func (p UploadDataProvider) SetClientContext(context unsafe.Pointer) { 12 | cronet.UploadDataProviderSetClientContext(p.ptr, uintptr(context)) 13 | } 14 | 15 | func (p UploadDataProvider) ClientContext() unsafe.Pointer { 16 | return unsafe.Pointer(cronet.UploadDataProviderGetClientContext(p.ptr)) 17 | } 18 | -------------------------------------------------------------------------------- /url_request_callback_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "unsafe" 7 | 8 | "github.com/sagernet/cronet-go/internal/cronet" 9 | ) 10 | 11 | func (c URLRequestCallback) SetClientContext(context unsafe.Pointer) { 12 | cronet.UrlRequestCallbackSetClientContext(c.ptr, uintptr(context)) 13 | } 14 | 15 | func (c URLRequestCallback) ClientContext() unsafe.Pointer { 16 | return unsafe.Pointer(cronet.UrlRequestCallbackGetClientContext(c.ptr)) 17 | } 18 | -------------------------------------------------------------------------------- /dns_types.go: -------------------------------------------------------------------------------- 1 | package cronet 2 | 3 | import ( 4 | "context" 5 | 6 | mDNS "github.com/miekg/dns" 7 | ) 8 | 9 | // DNSResolverFunc resolves a DNS request into a DNS response. 10 | // 11 | // The resolver is used by NaiveClient's optional in-process DNS server. The 12 | // returned message should be a response to the request; the implementation 13 | // will normalize the ID and question section as needed. 14 | type DNSResolverFunc func(ctx context.Context, request *mDNS.Msg) (response *mDNS.Msg) 15 | 16 | -------------------------------------------------------------------------------- /runnable_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "unsafe" 7 | 8 | "github.com/sagernet/cronet-go/internal/cronet" 9 | ) 10 | 11 | func (r Runnable) SetClientContext(context unsafe.Pointer) { 12 | cronet.RunnableSetClientContext(r.ptr, uintptr(context)) 13 | } 14 | 15 | func (r Runnable) ClientContext() unsafe.Pointer { 16 | return unsafe.Pointer(cronet.RunnableGetClientContext(r.ptr)) 17 | } 18 | 19 | func (r Runnable) Run() { 20 | cronet.RunnableRun(r.ptr) 21 | } 22 | -------------------------------------------------------------------------------- /internal/cronet/api_purego_windows.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego && windows 2 | 3 | package cronet 4 | 5 | import "runtime" 6 | 7 | // EngineStartWithParams starts the engine on Windows. 8 | // On Windows, we lock the OS thread to ensure stable thread-local storage 9 | // state during Chromium's initialization, which creates threads and message loops. 10 | func EngineStartWithParams(engine, params uintptr) int32 { 11 | runtime.LockOSThread() 12 | defer runtime.UnlockOSThread() 13 | return cronetEngineStartWithParams(engine, params) 14 | } 15 | -------------------------------------------------------------------------------- /executor_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "unsafe" 7 | 8 | "github.com/sagernet/cronet-go/internal/cronet" 9 | ) 10 | 11 | func (e Executor) SetClientContext(context unsafe.Pointer) { 12 | cronet.ExecutorSetClientContext(e.ptr, uintptr(context)) 13 | } 14 | 15 | func (e Executor) ClientContext() unsafe.Pointer { 16 | return unsafe.Pointer(cronet.ExecutorGetClientContext(e.ptr)) 17 | } 18 | 19 | func (e Executor) Execute(runnable Runnable) { 20 | cronet.ExecutorExecute(e.ptr, runnable.ptr) 21 | } 22 | -------------------------------------------------------------------------------- /buffer_callback_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "unsafe" 7 | 8 | "github.com/sagernet/cronet-go/internal/cronet" 9 | ) 10 | 11 | func (c BufferCallback) Destroy() { 12 | c.destroy() 13 | cronet.BufferCallbackDestroy(c.ptr) 14 | } 15 | 16 | func (c BufferCallback) SetClientContext(context unsafe.Pointer) { 17 | cronet.BufferCallbackSetClientContext(c.ptr, uintptr(context)) 18 | } 19 | 20 | func (c BufferCallback) ClientContext() unsafe.Pointer { 21 | return unsafe.Pointer(cronet.BufferCallbackGetClientContext(c.ptr)) 22 | } 23 | -------------------------------------------------------------------------------- /engine_version_test.go: -------------------------------------------------------------------------------- 1 | package cronet_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/sagernet/cronet-go" 8 | ) 9 | 10 | func TestEngineVersion(t *testing.T) { 11 | params := cronet.NewEngineParams() 12 | params.SetUserAgent("test") 13 | engine := cronet.NewEngine() 14 | engine.StartWithParams(params) 15 | defer params.Destroy() 16 | defer engine.Destroy() 17 | defer engine.Shutdown() 18 | 19 | version := engine.Version() 20 | fmt.Printf("Cronet Engine Version: %s\n", version) 21 | if version == "" { 22 | t.Fatal("engine version is empty") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sagernet/cronet-go 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/ebitengine/purego v0.9.1 7 | github.com/miekg/dns v1.1.50 8 | github.com/sagernet/sing v0.7.13 9 | github.com/spf13/cobra v1.4.0 10 | golang.org/x/sys v0.30.0 11 | ) 12 | 13 | require ( 14 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 15 | github.com/spf13/pflag v1.0.5 // indirect 16 | golang.org/x/mod v0.4.2 // indirect 17 | golang.org/x/net v0.35.0 // indirect 18 | golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect 19 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /upload_data_provider_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | 10 | import "unsafe" 11 | 12 | func (p UploadDataProvider) SetClientContext(context unsafe.Pointer) { 13 | C.Cronet_UploadDataProvider_SetClientContext(C.Cronet_UploadDataProviderPtr(unsafe.Pointer(p.ptr)), C.Cronet_ClientContext(context)) 14 | } 15 | 16 | func (p UploadDataProvider) ClientContext() unsafe.Pointer { 17 | return unsafe.Pointer(C.Cronet_UploadDataProvider_GetClientContext(C.Cronet_UploadDataProviderPtr(unsafe.Pointer(p.ptr)))) 18 | } 19 | -------------------------------------------------------------------------------- /url_request_callback_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | 10 | import "unsafe" 11 | 12 | func (c URLRequestCallback) SetClientContext(context unsafe.Pointer) { 13 | C.Cronet_UrlRequestCallback_SetClientContext(C.Cronet_UrlRequestCallbackPtr(unsafe.Pointer(c.ptr)), C.Cronet_ClientContext(context)) 14 | } 15 | 16 | func (c URLRequestCallback) ClientContext() unsafe.Pointer { 17 | return unsafe.Pointer(C.Cronet_UrlRequestCallback_GetClientContext(C.Cronet_UrlRequestCallbackPtr(unsafe.Pointer(c.ptr)))) 18 | } 19 | -------------------------------------------------------------------------------- /date_time_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "time" 7 | 8 | "github.com/sagernet/cronet-go/internal/cronet" 9 | ) 10 | 11 | func NewDateTime() DateTime { 12 | return DateTime{cronet.DateTimeCreate()} 13 | } 14 | 15 | func (d DateTime) Destroy() { 16 | cronet.DateTimeDestroy(d.ptr) 17 | } 18 | 19 | // SetValue sets the number of milliseconds since the UNIX epoch. 20 | func (d DateTime) SetValue(value time.Time) { 21 | cronet.DateTimeValueSet(d.ptr, value.UnixMilli()) 22 | } 23 | 24 | func (d DateTime) Value() time.Time { 25 | return time.UnixMilli(cronet.DateTimeValueGet(d.ptr)) 26 | } 27 | -------------------------------------------------------------------------------- /internal/cronet/loader_windows_float.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego && windows && (amd64 || arm64) 2 | 3 | package cronet 4 | 5 | // registerFloatFuncs registers functions that use float types. 6 | // Only supported on 64-bit platforms due to purego limitations. 7 | func registerFloatFuncs() error { 8 | if err := registerFunc(&cronetEngineParamsNetworkThreadPrioritySet, "Cronet_EngineParams_network_thread_priority_set"); err != nil { 9 | return err 10 | } 11 | if err := registerFunc(&cronetEngineParamsNetworkThreadPriorityGet, "Cronet_EngineParams_network_thread_priority_get"); err != nil { 12 | return err 13 | } 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /url_request_status_listener_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "unsafe" 7 | 8 | "github.com/sagernet/cronet-go/internal/cronet" 9 | ) 10 | 11 | func (l URLRequestStatusListener) Destroy() { 12 | l.destroy() 13 | cronet.UrlRequestStatusListenerDestroy(l.ptr) 14 | } 15 | 16 | func (l URLRequestStatusListener) SetClientContext(context unsafe.Pointer) { 17 | cronet.UrlRequestStatusListenerSetClientContext(l.ptr, uintptr(context)) 18 | } 19 | 20 | func (l URLRequestStatusListener) ClientContext() unsafe.Pointer { 21 | return unsafe.Pointer(cronet.UrlRequestStatusListenerGetClientContext(l.ptr)) 22 | } 23 | -------------------------------------------------------------------------------- /url_request_status_listener_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | 10 | import "unsafe" 11 | 12 | func (l URLRequestStatusListener) SetClientContext(context unsafe.Pointer) { 13 | C.Cronet_UrlRequestStatusListener_SetClientContext(C.Cronet_UrlRequestStatusListenerPtr(unsafe.Pointer(l.ptr)), C.Cronet_ClientContext(context)) 14 | } 15 | 16 | func (l URLRequestStatusListener) ClientContext() unsafe.Pointer { 17 | return unsafe.Pointer(C.Cronet_UrlRequestStatusListener_GetClientContext(C.Cronet_UrlRequestStatusListenerPtr(unsafe.Pointer(l.ptr)))) 18 | } 19 | -------------------------------------------------------------------------------- /runnable_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | 10 | import "unsafe" 11 | 12 | func (r Runnable) Run() { 13 | C.Cronet_Runnable_Run(C.Cronet_RunnablePtr(unsafe.Pointer(r.ptr))) 14 | } 15 | 16 | func (r Runnable) SetClientContext(context unsafe.Pointer) { 17 | C.Cronet_Runnable_SetClientContext(C.Cronet_RunnablePtr(unsafe.Pointer(r.ptr)), C.Cronet_ClientContext(context)) 18 | } 19 | 20 | func (r Runnable) ClientContext() unsafe.Pointer { 21 | return unsafe.Pointer(C.Cronet_Runnable_GetClientContext(C.Cronet_RunnablePtr(unsafe.Pointer(r.ptr)))) 22 | } 23 | -------------------------------------------------------------------------------- /url_request_finished_info_listener_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "unsafe" 7 | 8 | "github.com/sagernet/cronet-go/internal/cronet" 9 | ) 10 | 11 | func (l URLRequestFinishedInfoListener) Destroy() { 12 | l.destroy() 13 | cronet.RequestFinishedInfoListenerDestroy(l.ptr) 14 | } 15 | 16 | func (l URLRequestFinishedInfoListener) SetClientContext(context unsafe.Pointer) { 17 | cronet.RequestFinishedInfoListenerSetClientContext(l.ptr, uintptr(context)) 18 | } 19 | 20 | func (l URLRequestFinishedInfoListener) ClientContext() unsafe.Pointer { 21 | return unsafe.Pointer(cronet.RequestFinishedInfoListenerGetClientContext(l.ptr)) 22 | } 23 | -------------------------------------------------------------------------------- /url_request_finished_info_listener_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | 10 | import "unsafe" 11 | 12 | func (l URLRequestFinishedInfoListener) SetClientContext(context unsafe.Pointer) { 13 | C.Cronet_RequestFinishedInfoListener_SetClientContext(C.Cronet_RequestFinishedInfoListenerPtr(unsafe.Pointer(l.ptr)), C.Cronet_ClientContext(context)) 14 | } 15 | 16 | func (l URLRequestFinishedInfoListener) ClientContext() unsafe.Pointer { 17 | return unsafe.Pointer(C.Cronet_RequestFinishedInfoListener_GetClientContext(C.Cronet_RequestFinishedInfoListenerPtr(unsafe.Pointer(l.ptr)))) 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2022 by nekohasekai 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation, either version 3 of the License, or 6 | (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License 14 | along with this program. If not, see . -------------------------------------------------------------------------------- /internal/cronet/api_purego_float_stub.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego && (386 || arm) 2 | 3 | package cronet 4 | 5 | // EngineParamsNetworkThreadPrioritySet is not supported on 32-bit platforms. 6 | // purego does not support float parameters on 32-bit platforms. 7 | func EngineParamsNetworkThreadPrioritySet(params uintptr, priority float64) { 8 | panic("cronet: NetworkThreadPriority not supported on 32-bit platforms") 9 | } 10 | 11 | // EngineParamsNetworkThreadPriorityGet is not supported on 32-bit platforms. 12 | // purego does not support float parameters on 32-bit platforms. 13 | func EngineParamsNetworkThreadPriorityGet(params uintptr) float64 { 14 | panic("cronet: NetworkThreadPriority not supported on 32-bit platforms") 15 | } 16 | -------------------------------------------------------------------------------- /http_header_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "github.com/sagernet/cronet-go/internal/cronet" 7 | ) 8 | 9 | func NewHTTPHeader() HTTPHeader { 10 | return HTTPHeader{cronet.HttpHeaderCreate()} 11 | } 12 | 13 | func (h HTTPHeader) Destroy() { 14 | cronet.HttpHeaderDestroy(h.ptr) 15 | } 16 | 17 | func (h HTTPHeader) SetName(name string) { 18 | cronet.HttpHeaderNameSet(h.ptr, name) 19 | } 20 | 21 | func (h HTTPHeader) Name() string { 22 | return cronet.HttpHeaderNameGet(h.ptr) 23 | } 24 | 25 | func (h HTTPHeader) SetValue(value string) { 26 | cronet.HttpHeaderValueSet(h.ptr, value) 27 | } 28 | 29 | func (h HTTPHeader) Value() string { 30 | return cronet.HttpHeaderValueGet(h.ptr) 31 | } 32 | -------------------------------------------------------------------------------- /executor_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | 10 | import "unsafe" 11 | 12 | func (e Executor) Execute(command Runnable) { 13 | C.Cronet_Executor_Execute(C.Cronet_ExecutorPtr(unsafe.Pointer(e.ptr)), C.Cronet_RunnablePtr(unsafe.Pointer(command.ptr))) 14 | } 15 | 16 | func (e Executor) SetClientContext(context unsafe.Pointer) { 17 | C.Cronet_Executor_SetClientContext(C.Cronet_ExecutorPtr(unsafe.Pointer(e.ptr)), C.Cronet_ClientContext(context)) 18 | } 19 | 20 | func (e Executor) ClientContext() unsafe.Pointer { 21 | return unsafe.Pointer(C.Cronet_Executor_GetClientContext(C.Cronet_ExecutorPtr(unsafe.Pointer(e.ptr)))) 22 | } 23 | -------------------------------------------------------------------------------- /buffer_callback_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | 10 | import "unsafe" 11 | 12 | func (c BufferCallback) Destroy() { 13 | c.destroy() 14 | C.Cronet_BufferCallback_Destroy(C.Cronet_BufferCallbackPtr(unsafe.Pointer(c.ptr))) 15 | } 16 | 17 | func (c BufferCallback) SetClientContext(context unsafe.Pointer) { 18 | C.Cronet_BufferCallback_SetClientContext(C.Cronet_BufferCallbackPtr(unsafe.Pointer(c.ptr)), C.Cronet_ClientContext(context)) 19 | } 20 | 21 | func (c BufferCallback) ClientContext() unsafe.Pointer { 22 | return unsafe.Pointer(C.Cronet_BufferCallback_GetClientContext(C.Cronet_BufferCallbackPtr(unsafe.Pointer(c.ptr)))) 23 | } 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TARGET ?= 2 | TARGET_FLAG = $(if $(TARGET),--target=$(TARGET),) 3 | 4 | build: 5 | go run -v ./cmd/build-naive build $(TARGET_FLAG) 6 | go run -v ./cmd/build-naive package --local $(TARGET_FLAG) 7 | 8 | test: make 9 | go test -v . 10 | 11 | fmt: 12 | @find . -name '*.go' -not -path './naiveproxy/*' -exec gofumpt -l -w {} + 13 | @find . -name '*.go' -not -path './naiveproxy/*' -exec gofmt -s -w {} + 14 | @gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" $$(find . -name '*.go' -not -path './naiveproxy/*') 15 | 16 | fmt_install: 17 | go install -v mvdan.cc/gofumpt@v0.8.0 18 | go install -v github.com/daixiang0/gci@latest 19 | 20 | lint: 21 | golangci-lint run 22 | 23 | lint_install: 24 | go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0 25 | -------------------------------------------------------------------------------- /error_go.go: -------------------------------------------------------------------------------- 1 | package cronet 2 | 3 | type ErrorGo struct { 4 | ErrorCode ErrorCode 5 | Message string 6 | InternalErrorCode int 7 | Retryable bool 8 | QuicDetailedErrorCode int 9 | } 10 | 11 | func (e *ErrorGo) Error() string { 12 | return e.Message 13 | } 14 | 15 | func (e *ErrorGo) Timeout() bool { 16 | return e.ErrorCode == ErrorCodeErrorConnectionTimedOut 17 | } 18 | 19 | func (e *ErrorGo) Temporary() bool { 20 | return e.Retryable 21 | } 22 | 23 | func ErrorFromError(error Error) *ErrorGo { 24 | return &ErrorGo{ 25 | ErrorCode: error.ErrorCode(), 26 | Message: error.Message(), 27 | InternalErrorCode: error.InternalErrorCode(), 28 | Retryable: error.Retryable(), 29 | QuicDetailedErrorCode: error.QuicDetailedErrorCode(), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /date_time_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | 10 | import ( 11 | "time" 12 | "unsafe" 13 | ) 14 | 15 | func NewDateTime() DateTime { 16 | return DateTime{uintptr(unsafe.Pointer(C.Cronet_DateTime_Create()))} 17 | } 18 | 19 | func (t DateTime) Destroy() { 20 | C.Cronet_DateTime_Destroy(C.Cronet_DateTimePtr(unsafe.Pointer(t.ptr))) 21 | } 22 | 23 | // SetValue 24 | // Number of milliseconds since the UNIX epoch. 25 | func (t DateTime) SetValue(value time.Time) { 26 | C.Cronet_DateTime_value_set(C.Cronet_DateTimePtr(unsafe.Pointer(t.ptr)), C.int64_t(value.UnixMilli())) 27 | } 28 | 29 | func (t DateTime) Value() time.Time { 30 | return time.UnixMilli(int64(C.Cronet_DateTime_value_get(C.Cronet_DateTimePtr(unsafe.Pointer(t.ptr))))) 31 | } 32 | -------------------------------------------------------------------------------- /quic_hint_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "github.com/sagernet/cronet-go/internal/cronet" 7 | ) 8 | 9 | func NewQuicHint() QuicHint { 10 | return QuicHint{cronet.QuicHintCreate()} 11 | } 12 | 13 | func (q QuicHint) Destroy() { 14 | cronet.QuicHintDestroy(q.ptr) 15 | } 16 | 17 | func (q QuicHint) SetHost(host string) { 18 | cronet.QuicHintHostSet(q.ptr, host) 19 | } 20 | 21 | func (q QuicHint) Host() string { 22 | return cronet.QuicHintHostGet(q.ptr) 23 | } 24 | 25 | func (q QuicHint) SetPort(port int32) { 26 | cronet.QuicHintPortSet(q.ptr, port) 27 | } 28 | 29 | func (q QuicHint) Port() int32 { 30 | return cronet.QuicHintPortGet(q.ptr) 31 | } 32 | 33 | func (q QuicHint) SetAlternatePort(port int32) { 34 | cronet.QuicHintAlternatePortSet(q.ptr, port) 35 | } 36 | 37 | func (q QuicHint) AlternatePort() int32 { 38 | return cronet.QuicHintAlternatePortGet(q.ptr) 39 | } 40 | -------------------------------------------------------------------------------- /cmd/build-naive/cmd_download_toolchain.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var commandDownloadToolchain = &cobra.Command{ 10 | Use: "download-toolchain", 11 | Short: "Download clang and sysroot without building", 12 | Run: func(cmd *cobra.Command, args []string) { 13 | targets := parseTargets() 14 | downloadToolchain(targets) 15 | }, 16 | } 17 | 18 | func init() { 19 | mainCommand.AddCommand(commandDownloadToolchain) 20 | } 21 | 22 | func downloadToolchain(targets []Target) { 23 | log.Printf("Downloading toolchain for %d target(s)", len(targets)) 24 | 25 | for _, t := range targets { 26 | if t.Libc == "musl" { 27 | log.Printf("Downloading toolchain for %s/%s (musl)...", t.GOOS, t.ARCH) 28 | } else { 29 | log.Printf("Downloading toolchain for %s/%s...", t.GOOS, t.ARCH) 30 | } 31 | runGetClang(t) 32 | } 33 | 34 | log.Print("Toolchain download complete!") 35 | } 36 | -------------------------------------------------------------------------------- /test/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sagernet/cronet-go/test 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/miekg/dns v1.1.68 7 | github.com/sagernet/cronet-go v0.0.0 8 | github.com/sagernet/sing v0.7.13 9 | github.com/stretchr/testify v1.11.1 10 | go.uber.org/goleak v1.3.0 11 | golang.org/x/crypto v0.38.0 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/ebitengine/purego v0.9.1 // indirect 17 | github.com/kr/pretty v0.3.1 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | github.com/rogpeppe/go-internal v1.14.1 // indirect 20 | golang.org/x/mod v0.24.0 // indirect 21 | golang.org/x/net v0.40.0 // indirect 22 | golang.org/x/sync v0.14.0 // indirect 23 | golang.org/x/sys v0.39.0 // indirect 24 | golang.org/x/tools v0.33.0 // indirect 25 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 26 | gopkg.in/yaml.v3 v3.0.1 // indirect 27 | ) 28 | 29 | replace github.com/sagernet/cronet-go => ../ 30 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | go: "1.24" 4 | linters: 5 | default: none 6 | enable: 7 | - govet 8 | - ineffassign 9 | - paralleltest 10 | - staticcheck 11 | settings: 12 | staticcheck: 13 | checks: 14 | - all 15 | - -S1000 16 | - -S1008 17 | - -S1017 18 | - -ST1001 19 | - -ST1003 20 | - -QF1001 21 | - -QF1003 22 | - -QF1008 23 | exclusions: 24 | generated: lax 25 | presets: 26 | - comments 27 | - common-false-positives 28 | - legacy 29 | - std-error-handling 30 | paths: 31 | - naiveproxy$ 32 | formatters: 33 | enable: 34 | - gci 35 | - gofumpt 36 | settings: 37 | gci: 38 | sections: 39 | - standard 40 | - prefix(github.com/sagernet/) 41 | - default 42 | custom-order: true 43 | exclusions: 44 | generated: lax 45 | paths: 46 | - naiveproxy$ 47 | - third_party$ 48 | - builtin$ 49 | - examples$ 50 | -------------------------------------------------------------------------------- /url_request_finished_info_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "unsafe" 7 | 8 | "github.com/sagernet/cronet-go/internal/cronet" 9 | ) 10 | 11 | func NewURLRequestFinishedInfo() RequestFinishedInfo { 12 | return RequestFinishedInfo{cronet.RequestFinishedInfoCreate()} 13 | } 14 | 15 | func (i RequestFinishedInfo) Destroy() { 16 | cronet.RequestFinishedInfoDestroy(i.ptr) 17 | } 18 | 19 | func (i RequestFinishedInfo) Metrics() Metrics { 20 | return Metrics{cronet.RequestFinishedInfoMetricsGet(i.ptr)} 21 | } 22 | 23 | func (i RequestFinishedInfo) AnnotationSize() int { 24 | return int(cronet.RequestFinishedInfoAnnotationsSize(i.ptr)) 25 | } 26 | 27 | func (i RequestFinishedInfo) AnnotationAt(index int) unsafe.Pointer { 28 | return unsafe.Pointer(cronet.RequestFinishedInfoAnnotationsAt(i.ptr, uint32(index))) 29 | } 30 | 31 | func (i RequestFinishedInfo) FinishedReason() URLRequestFinishedInfoFinishedReason { 32 | return URLRequestFinishedInfoFinishedReason(cronet.RequestFinishedInfoFinishedReasonGet(i.ptr)) 33 | } 34 | -------------------------------------------------------------------------------- /upload_data_sink_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "unsafe" 7 | 8 | "github.com/sagernet/cronet-go/internal/cronet" 9 | ) 10 | 11 | func (s UploadDataSink) Destroy() { 12 | cronet.UploadDataSinkDestroy(s.ptr) 13 | } 14 | 15 | func (s UploadDataSink) SetClientContext(context unsafe.Pointer) { 16 | cronet.UploadDataSinkSetClientContext(s.ptr, uintptr(context)) 17 | } 18 | 19 | func (s UploadDataSink) ClientContext() unsafe.Pointer { 20 | return unsafe.Pointer(cronet.UploadDataSinkGetClientContext(s.ptr)) 21 | } 22 | 23 | func (s UploadDataSink) OnReadSucceeded(bytesRead int64, finalChunk bool) { 24 | cronet.UploadDataSinkOnReadSucceeded(s.ptr, uint64(bytesRead), finalChunk) 25 | } 26 | 27 | func (s UploadDataSink) OnReadError(message string) { 28 | cronet.UploadDataSinkOnReadError(s.ptr, message) 29 | } 30 | 31 | func (s UploadDataSink) OnRewindSucceeded() { 32 | cronet.UploadDataSinkOnRewindSucceeded(s.ptr) 33 | } 34 | 35 | func (s UploadDataSink) OnRewindError(message string) { 36 | cronet.UploadDataSinkOnRewindError(s.ptr, message) 37 | } 38 | -------------------------------------------------------------------------------- /http_header_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | import "unsafe" 10 | 11 | func NewHTTPHeader() HTTPHeader { 12 | return HTTPHeader{uintptr(unsafe.Pointer(C.Cronet_HttpHeader_Create()))} 13 | } 14 | 15 | func (h HTTPHeader) Destroy() { 16 | C.Cronet_HttpHeader_Destroy(C.Cronet_HttpHeaderPtr(unsafe.Pointer(h.ptr))) 17 | } 18 | 19 | // SetName sets header name 20 | func (h HTTPHeader) SetName(name string) { 21 | cName := C.CString(name) 22 | C.Cronet_HttpHeader_name_set(C.Cronet_HttpHeaderPtr(unsafe.Pointer(h.ptr)), cName) 23 | C.free(unsafe.Pointer(cName)) 24 | } 25 | 26 | func (h HTTPHeader) Name() string { 27 | return C.GoString(C.Cronet_HttpHeader_name_get(C.Cronet_HttpHeaderPtr(unsafe.Pointer(h.ptr)))) 28 | } 29 | 30 | // SetValue sts header value 31 | func (h HTTPHeader) SetValue(value string) { 32 | cValue := C.CString(value) 33 | C.Cronet_HttpHeader_value_set(C.Cronet_HttpHeaderPtr(unsafe.Pointer(h.ptr)), cValue) 34 | C.free(unsafe.Pointer(cValue)) 35 | } 36 | 37 | func (h HTTPHeader) Value() string { 38 | return C.GoString(C.Cronet_HttpHeader_value_get(C.Cronet_HttpHeaderPtr(unsafe.Pointer(h.ptr)))) 39 | } 40 | -------------------------------------------------------------------------------- /error_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "github.com/sagernet/cronet-go/internal/cronet" 7 | ) 8 | 9 | func NewError() Error { 10 | return Error{cronet.ErrorCreate()} 11 | } 12 | 13 | func (e Error) Destroy() { 14 | cronet.ErrorDestroy(e.ptr) 15 | } 16 | 17 | func (e Error) ErrorCode() ErrorCode { 18 | return ErrorCode(cronet.ErrorErrorCodeGet(e.ptr)) 19 | } 20 | 21 | func (e Error) Message() string { 22 | return cronet.ErrorMessageGet(e.ptr) 23 | } 24 | 25 | func (e Error) InternalErrorCode() int { 26 | return int(cronet.ErrorInternalErrorCodeGet(e.ptr)) 27 | } 28 | 29 | func (e Error) Retryable() bool { 30 | return cronet.ErrorImmediatelyRetryableGet(e.ptr) 31 | } 32 | 33 | func (e Error) QuicDetailedErrorCode() int { 34 | return int(cronet.ErrorQuicDetailedErrorCodeGet(e.ptr)) 35 | } 36 | 37 | func (e Error) SetErrorCode(code ErrorCode) { 38 | cronet.ErrorErrorCodeSet(e.ptr, int32(code)) 39 | } 40 | 41 | func (e Error) SetMessage(message string) { 42 | cronet.ErrorMessageSet(e.ptr, message) 43 | } 44 | 45 | func (e Error) SetInternalErrorCode(code int32) { 46 | cronet.ErrorInternalErrorCodeSet(e.ptr, code) 47 | } 48 | 49 | func (e Error) SetRetryable(retryable bool) { 50 | cronet.ErrorImmediatelyRetryableSet(e.ptr, retryable) 51 | } 52 | 53 | func (e Error) SetQuicDetailedErrorCode(code int32) { 54 | cronet.ErrorQuicDetailedErrorCodeSet(e.ptr, code) 55 | } 56 | -------------------------------------------------------------------------------- /public_key_pins_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "github.com/sagernet/cronet-go/internal/cronet" 7 | ) 8 | 9 | func NewPublicKeyPins() PublicKeyPins { 10 | return PublicKeyPins{cronet.PublicKeyPinsCreate()} 11 | } 12 | 13 | func (p PublicKeyPins) Destroy() { 14 | cronet.PublicKeyPinsDestroy(p.ptr) 15 | } 16 | 17 | func (p PublicKeyPins) SetHost(host string) { 18 | cronet.PublicKeyPinsHostSet(p.ptr, host) 19 | } 20 | 21 | func (p PublicKeyPins) Host() string { 22 | return cronet.PublicKeyPinsHostGet(p.ptr) 23 | } 24 | 25 | func (p PublicKeyPins) AddPinSHA256(pin string) { 26 | cronet.PublicKeyPinsPinsSha256Add(p.ptr, pin) 27 | } 28 | 29 | func (p PublicKeyPins) PinSHA256Size() int { 30 | return int(cronet.PublicKeyPinsPinsSha256Size(p.ptr)) 31 | } 32 | 33 | func (p PublicKeyPins) PinSHA256At(index int) string { 34 | return cronet.PublicKeyPinsPinsSha256At(p.ptr, uint32(index)) 35 | } 36 | 37 | func (p PublicKeyPins) SetIncludeSubdomains(include bool) { 38 | cronet.PublicKeyPinsIncludeSubdomainsSet(p.ptr, include) 39 | } 40 | 41 | func (p PublicKeyPins) IncludeSubdomains() bool { 42 | return cronet.PublicKeyPinsIncludeSubdomainsGet(p.ptr) 43 | } 44 | 45 | func (p PublicKeyPins) SetExpirationDate(date int64) { 46 | cronet.PublicKeyPinsExpirationDateSet(p.ptr, date) 47 | } 48 | 49 | func (p PublicKeyPins) ExpirationDate() int64 { 50 | return cronet.PublicKeyPinsExpirationDateGet(p.ptr) 51 | } 52 | -------------------------------------------------------------------------------- /url_request_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "unsafe" 7 | 8 | "github.com/sagernet/cronet-go/internal/cronet" 9 | ) 10 | 11 | func NewURLRequest() URLRequest { 12 | return URLRequest{cronet.UrlRequestCreate()} 13 | } 14 | 15 | func (r URLRequest) Destroy() { 16 | cronet.UrlRequestDestroy(r.ptr) 17 | } 18 | 19 | func (r URLRequest) SetClientContext(context unsafe.Pointer) { 20 | cronet.UrlRequestSetClientContext(r.ptr, uintptr(context)) 21 | } 22 | 23 | func (r URLRequest) ClientContext() unsafe.Pointer { 24 | return unsafe.Pointer(cronet.UrlRequestGetClientContext(r.ptr)) 25 | } 26 | 27 | func (r URLRequest) InitWithParams(engine Engine, url string, params URLRequestParams, callback URLRequestCallback, executor Executor) Result { 28 | return Result(cronet.UrlRequestInitWithParams(r.ptr, engine.ptr, url, params.ptr, callback.ptr, executor.ptr)) 29 | } 30 | 31 | func (r URLRequest) Start() Result { 32 | return Result(cronet.UrlRequestStart(r.ptr)) 33 | } 34 | 35 | func (r URLRequest) FollowRedirect() Result { 36 | return Result(cronet.UrlRequestFollowRedirect(r.ptr)) 37 | } 38 | 39 | func (r URLRequest) Read(buffer Buffer) Result { 40 | return Result(cronet.UrlRequestRead(r.ptr, buffer.ptr)) 41 | } 42 | 43 | func (r URLRequest) Cancel() { 44 | cronet.UrlRequestCancel(r.ptr) 45 | } 46 | 47 | func (r URLRequest) IsDone() bool { 48 | return cronet.UrlRequestIsDone(r.ptr) 49 | } 50 | 51 | func (r URLRequest) GetStatus(listener URLRequestStatusListener) { 52 | cronet.UrlRequestGetStatus(r.ptr, listener.ptr) 53 | } 54 | -------------------------------------------------------------------------------- /quic_hint_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | 10 | import ( 11 | "unsafe" 12 | ) 13 | 14 | func NewQuicHint() QuicHint { 15 | return QuicHint{uintptr(unsafe.Pointer(C.Cronet_QuicHint_Create()))} 16 | } 17 | 18 | func (h QuicHint) Destroy() { 19 | C.Cronet_QuicHint_Destroy(C.Cronet_QuicHintPtr(unsafe.Pointer(h.ptr))) 20 | } 21 | 22 | // SetHost set name of the host that supports QUIC. 23 | func (h QuicHint) SetHost(host string) { 24 | cHost := C.CString(host) 25 | C.Cronet_QuicHint_host_set(C.Cronet_QuicHintPtr(unsafe.Pointer(h.ptr)), cHost) 26 | C.free(unsafe.Pointer(cHost)) 27 | } 28 | 29 | func (h QuicHint) Host() string { 30 | return C.GoString(C.Cronet_QuicHint_host_get(C.Cronet_QuicHintPtr(unsafe.Pointer(h.ptr)))) 31 | } 32 | 33 | // SetPort set port of the server that supports QUIC. 34 | func (h QuicHint) SetPort(port int32) { 35 | C.Cronet_QuicHint_port_set(C.Cronet_QuicHintPtr(unsafe.Pointer(h.ptr)), C.int32_t(port)) 36 | } 37 | 38 | func (h QuicHint) Port() int32 { 39 | return int32(C.Cronet_QuicHint_port_get(C.Cronet_QuicHintPtr(unsafe.Pointer(h.ptr)))) 40 | } 41 | 42 | // SetAlternatePort set alternate port to use for QUIC 43 | func (h QuicHint) SetAlternatePort(port int32) { 44 | C.Cronet_QuicHint_alternate_port_set(C.Cronet_QuicHintPtr(unsafe.Pointer(h.ptr)), C.int32_t(port)) 45 | } 46 | 47 | func (h QuicHint) AlternatePort() int32 { 48 | return int32(C.Cronet_QuicHint_alternate_port_get(C.Cronet_QuicHintPtr(unsafe.Pointer(h.ptr)))) 49 | } 50 | -------------------------------------------------------------------------------- /bidirectional_stream_map.go: -------------------------------------------------------------------------------- 1 | package cronet 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | ) 7 | 8 | // bidirectionalStreamEntry holds a callback and its destroyed state. 9 | // The destroyed flag is used to handle async destruction - callbacks 10 | // may be invoked after Destroy() returns due to Cronet's async API. 11 | type bidirectionalStreamEntry struct { 12 | callback BidirectionalStreamCallback 13 | destroyed atomic.Bool 14 | } 15 | 16 | var ( 17 | bidirectionalStreamAccess sync.RWMutex 18 | bidirectionalStreamMap map[uintptr]*bidirectionalStreamEntry 19 | ) 20 | 21 | func init() { 22 | bidirectionalStreamMap = make(map[uintptr]*bidirectionalStreamEntry) 23 | } 24 | 25 | // instanceOfBidirectionalStreamCallback returns the callback for the given stream pointer. 26 | // Returns nil if the stream was destroyed or not found. Callers should silently 27 | // return on nil as post-destroy callbacks are expected async API behavior. 28 | func instanceOfBidirectionalStreamCallback(ptr uintptr) BidirectionalStreamCallback { 29 | bidirectionalStreamAccess.RLock() 30 | defer bidirectionalStreamAccess.RUnlock() 31 | entry := bidirectionalStreamMap[ptr] 32 | if entry == nil || entry.destroyed.Load() { 33 | return nil 34 | } 35 | return entry.callback 36 | } 37 | 38 | // cleanupBidirectionalStream removes the stream entry from the map. 39 | // Should be called from terminal callbacks (OnSucceeded/OnFailed/OnCanceled). 40 | func cleanupBidirectionalStream(ptr uintptr) { 41 | bidirectionalStreamAccess.Lock() 42 | delete(bidirectionalStreamMap, ptr) 43 | bidirectionalStreamAccess.Unlock() 44 | } 45 | -------------------------------------------------------------------------------- /runnable_impl_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "sync" 7 | "sync/atomic" 8 | 9 | "github.com/sagernet/cronet-go/internal/cronet" 10 | 11 | "github.com/ebitengine/purego" 12 | ) 13 | 14 | type runnableEntry struct { 15 | runFunc RunnableRunFunc 16 | destroyed atomic.Bool 17 | } 18 | 19 | var ( 20 | runnableAccess sync.RWMutex 21 | runnableMap map[uintptr]*runnableEntry 22 | runnableCallbackFn uintptr 23 | ) 24 | 25 | func init() { 26 | runnableMap = make(map[uintptr]*runnableEntry) 27 | runnableCallbackFn = purego.NewCallback(runnableRunCallback) 28 | } 29 | 30 | func runnableRunCallback(self uintptr) uintptr { 31 | runnableAccess.RLock() 32 | entry := runnableMap[self] 33 | runnableAccess.RUnlock() 34 | if entry == nil || entry.destroyed.Load() { 35 | return 0 // Post-destroy callback, silently ignore 36 | } 37 | entry.runFunc(Runnable{self}) 38 | // Run is one-shot - safe to cleanup 39 | runnableAccess.Lock() 40 | delete(runnableMap, self) 41 | runnableAccess.Unlock() 42 | return 0 43 | } 44 | 45 | // NewRunnable creates a new Runnable with the given run function. 46 | func NewRunnable(runFunc RunnableRunFunc) Runnable { 47 | if runFunc == nil { 48 | panic("nil runnable run function") 49 | } 50 | ptr := cronet.RunnableCreateWith(runnableCallbackFn) 51 | runnableAccess.Lock() 52 | runnableMap[ptr] = &runnableEntry{runFunc: runFunc} 53 | runnableAccess.Unlock() 54 | return Runnable{ptr} 55 | } 56 | 57 | func (r Runnable) Destroy() { 58 | runnableAccess.RLock() 59 | entry := runnableMap[r.ptr] 60 | runnableAccess.RUnlock() 61 | if entry != nil { 62 | entry.destroyed.Store(true) 63 | } 64 | cronet.RunnableDestroy(r.ptr) 65 | } 66 | -------------------------------------------------------------------------------- /url_response_info_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "github.com/sagernet/cronet-go/internal/cronet" 7 | ) 8 | 9 | func NewURLResponseInfo() URLResponseInfo { 10 | return URLResponseInfo{cronet.UrlResponseInfoCreate()} 11 | } 12 | 13 | func (i URLResponseInfo) Destroy() { 14 | cronet.UrlResponseInfoDestroy(i.ptr) 15 | } 16 | 17 | func (i URLResponseInfo) URL() string { 18 | return cronet.UrlResponseInfoUrlGet(i.ptr) 19 | } 20 | 21 | func (i URLResponseInfo) URLChainSize() int { 22 | return int(cronet.UrlResponseInfoUrlChainSize(i.ptr)) 23 | } 24 | 25 | func (i URLResponseInfo) URLChainAt(index int) string { 26 | return cronet.UrlResponseInfoUrlChainAt(i.ptr, uint32(index)) 27 | } 28 | 29 | func (i URLResponseInfo) StatusCode() int { 30 | return int(cronet.UrlResponseInfoHttpStatusCodeGet(i.ptr)) 31 | } 32 | 33 | func (i URLResponseInfo) StatusText() string { 34 | return cronet.UrlResponseInfoHttpStatusTextGet(i.ptr) 35 | } 36 | 37 | func (i URLResponseInfo) HeaderSize() int { 38 | return int(cronet.UrlResponseInfoAllHeadersListSize(i.ptr)) 39 | } 40 | 41 | func (i URLResponseInfo) HeaderAt(index int) HTTPHeader { 42 | return HTTPHeader{cronet.UrlResponseInfoAllHeadersListAt(i.ptr, uint32(index))} 43 | } 44 | 45 | func (i URLResponseInfo) Cached() bool { 46 | return cronet.UrlResponseInfoWasCachedGet(i.ptr) 47 | } 48 | 49 | func (i URLResponseInfo) NegotiatedProtocol() string { 50 | return cronet.UrlResponseInfoNegotiatedProtocolGet(i.ptr) 51 | } 52 | 53 | func (i URLResponseInfo) ProxyServer() string { 54 | return cronet.UrlResponseInfoProxyServerGet(i.ptr) 55 | } 56 | 57 | func (i URLResponseInfo) ReceivedByteCount() int64 { 58 | return cronet.UrlResponseInfoReceivedByteCountGet(i.ptr) 59 | } 60 | -------------------------------------------------------------------------------- /buffer_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "unsafe" 7 | 8 | "github.com/sagernet/cronet-go/internal/cronet" 9 | ) 10 | 11 | func NewBuffer() Buffer { 12 | return Buffer{cronet.BufferCreate()} 13 | } 14 | 15 | func (b Buffer) Destroy() { 16 | cronet.BufferDestroy(b.ptr) 17 | } 18 | 19 | func (b Buffer) SetClientContext(context unsafe.Pointer) { 20 | cronet.BufferSetClientContext(b.ptr, uintptr(context)) 21 | } 22 | 23 | func (b Buffer) ClientContext() unsafe.Pointer { 24 | return unsafe.Pointer(cronet.BufferGetClientContext(b.ptr)) 25 | } 26 | 27 | // InitWithDataAndCallback initializes Buffer with raw buffer |data| allocated by the app. 28 | // The |callback| is invoked when buffer is destroyed. 29 | func (b Buffer) InitWithDataAndCallback(data []byte, callback BufferCallback) { 30 | if len(data) == 0 { 31 | cronet.BufferInitWithDataAndCallback(b.ptr, 0, 0, callback.ptr) 32 | return 33 | } 34 | cronet.BufferInitWithDataAndCallback(b.ptr, uintptr(unsafe.Pointer(&data[0])), uint64(len(data)), callback.ptr) 35 | } 36 | 37 | // InitWithAlloc initializes Buffer by allocating buffer of |size|. 38 | // The content of allocated data is not initialized. 39 | func (b Buffer) InitWithAlloc(size int64) { 40 | cronet.BufferInitWithAlloc(b.ptr, uint64(size)) 41 | } 42 | 43 | // Size returns size of data owned by this buffer. 44 | func (b Buffer) Size() int64 { 45 | return int64(cronet.BufferGetSize(b.ptr)) 46 | } 47 | 48 | // Data returns raw pointer to |data| owned by this buffer. 49 | func (b Buffer) Data() unsafe.Pointer { 50 | return unsafe.Pointer(cronet.BufferGetData(b.ptr)) 51 | } 52 | 53 | func (b Buffer) DataSlice() []byte { 54 | size := b.Size() 55 | if size == 0 { 56 | return nil 57 | } 58 | return unsafe.Slice((*byte)(b.Data()), size) 59 | } 60 | -------------------------------------------------------------------------------- /buffer_callback_impl_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "sync" 7 | "sync/atomic" 8 | 9 | "github.com/sagernet/cronet-go/internal/cronet" 10 | 11 | "github.com/ebitengine/purego" 12 | ) 13 | 14 | type bufferCallbackEntry struct { 15 | callback BufferCallbackFunc 16 | destroyed atomic.Bool 17 | } 18 | 19 | var ( 20 | bufferCallbackAccess sync.RWMutex 21 | bufferCallbackMap map[uintptr]*bufferCallbackEntry 22 | bufferCallbackOnDestroy uintptr 23 | ) 24 | 25 | func init() { 26 | bufferCallbackMap = make(map[uintptr]*bufferCallbackEntry) 27 | bufferCallbackOnDestroy = purego.NewCallback(onBufferDestroyCallback) 28 | } 29 | 30 | func onBufferDestroyCallback(self, buffer uintptr) uintptr { 31 | bufferCallbackAccess.RLock() 32 | entry := bufferCallbackMap[self] 33 | bufferCallbackAccess.RUnlock() 34 | if entry == nil || entry.destroyed.Load() { 35 | return 0 // Post-destroy callback, silently ignore 36 | } 37 | if entry.callback != nil { 38 | entry.callback(BufferCallback{self}, Buffer{buffer}) 39 | } 40 | // OnDestroy is the cleanup signal - safe to delete 41 | bufferCallbackAccess.Lock() 42 | delete(bufferCallbackMap, self) 43 | bufferCallbackAccess.Unlock() 44 | return 0 45 | } 46 | 47 | func NewBufferCallback(callbackFunc BufferCallbackFunc) BufferCallback { 48 | ptr := cronet.BufferCallbackCreateWith(bufferCallbackOnDestroy) 49 | if callbackFunc != nil { 50 | bufferCallbackAccess.Lock() 51 | bufferCallbackMap[ptr] = &bufferCallbackEntry{callback: callbackFunc} 52 | bufferCallbackAccess.Unlock() 53 | } 54 | return BufferCallback{ptr} 55 | } 56 | 57 | func (c BufferCallback) destroy() { 58 | bufferCallbackAccess.RLock() 59 | entry := bufferCallbackMap[c.ptr] 60 | bufferCallbackAccess.RUnlock() 61 | if entry != nil { 62 | entry.destroyed.Store(true) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /runnable_impl_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | // extern CRONET_EXPORT void cronetRunnableRun(Cronet_RunnablePtr self); 9 | import "C" 10 | 11 | import ( 12 | "sync" 13 | "sync/atomic" 14 | "unsafe" 15 | ) 16 | 17 | type runnableEntry struct { 18 | runFunc RunnableRunFunc 19 | destroyed atomic.Bool 20 | } 21 | 22 | var ( 23 | runnableAccess sync.RWMutex 24 | runnableMap map[uintptr]*runnableEntry 25 | ) 26 | 27 | func init() { 28 | runnableMap = make(map[uintptr]*runnableEntry) 29 | } 30 | 31 | // NewRunnable creates a new Runnable with the given run function. 32 | func NewRunnable(runFunc RunnableRunFunc) Runnable { 33 | if runFunc == nil { 34 | panic("nil runnable run function") 35 | } 36 | ptr := C.Cronet_Runnable_CreateWith((*[0]byte)(C.cronetRunnableRun)) 37 | ptrVal := uintptr(unsafe.Pointer(ptr)) 38 | runnableAccess.Lock() 39 | runnableMap[ptrVal] = &runnableEntry{runFunc: runFunc} 40 | runnableAccess.Unlock() 41 | return Runnable{ptrVal} 42 | } 43 | 44 | func (r Runnable) Destroy() { 45 | runnableAccess.RLock() 46 | entry := runnableMap[r.ptr] 47 | runnableAccess.RUnlock() 48 | if entry != nil { 49 | entry.destroyed.Store(true) 50 | } 51 | C.Cronet_Runnable_Destroy(C.Cronet_RunnablePtr(unsafe.Pointer(r.ptr))) 52 | } 53 | 54 | //export cronetRunnableRun 55 | func cronetRunnableRun(self C.Cronet_RunnablePtr) { 56 | ptr := uintptr(unsafe.Pointer(self)) 57 | runnableAccess.RLock() 58 | entry := runnableMap[ptr] 59 | runnableAccess.RUnlock() 60 | if entry == nil || entry.destroyed.Load() { 61 | return // Post-destroy callback, silently ignore 62 | } 63 | entry.runFunc(Runnable{ptr}) 64 | // Run is one-shot - safe to cleanup 65 | runnableAccess.Lock() 66 | delete(runnableMap, ptr) 67 | runnableAccess.Unlock() 68 | } 69 | -------------------------------------------------------------------------------- /url_request_status_listener_impl_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "sync" 7 | 8 | "github.com/sagernet/cronet-go/internal/cronet" 9 | 10 | "github.com/ebitengine/purego" 11 | ) 12 | 13 | var ( 14 | urlRequestStatusListenerAccess sync.RWMutex 15 | urlRequestStatusListenerMap map[uintptr]URLRequestStatusListenerOnStatusFunc 16 | urlRequestStatusListenerOnStatusCallback uintptr 17 | ) 18 | 19 | func init() { 20 | urlRequestStatusListenerMap = make(map[uintptr]URLRequestStatusListenerOnStatusFunc) 21 | urlRequestStatusListenerOnStatusCallback = purego.NewCallback(cronetURLRequestStatusListenerOnStatus) 22 | } 23 | 24 | func cronetURLRequestStatusListenerOnStatus(self uintptr, status int32) uintptr { 25 | urlRequestStatusListenerAccess.Lock() 26 | listener := urlRequestStatusListenerMap[self] 27 | delete(urlRequestStatusListenerMap, self) // One-shot callback 28 | urlRequestStatusListenerAccess.Unlock() 29 | if listener == nil { 30 | return 0 // Race with Destroy() or already invoked - silently return 31 | } 32 | listener(URLRequestStatusListener{self}, URLRequestStatusListenerStatus(status)) 33 | return 0 34 | } 35 | 36 | func NewURLRequestStatusListener(onStatusFunc URLRequestStatusListenerOnStatusFunc) URLRequestStatusListener { 37 | if onStatusFunc == nil { 38 | panic("nil url request status listener function") 39 | } 40 | ptr := cronet.UrlRequestStatusListenerCreateWith(urlRequestStatusListenerOnStatusCallback) 41 | urlRequestStatusListenerAccess.Lock() 42 | urlRequestStatusListenerMap[ptr] = onStatusFunc 43 | urlRequestStatusListenerAccess.Unlock() 44 | return URLRequestStatusListener{ptr} 45 | } 46 | 47 | func (l URLRequestStatusListener) destroy() { 48 | urlRequestStatusListenerAccess.Lock() 49 | delete(urlRequestStatusListenerMap, l.ptr) 50 | urlRequestStatusListenerAccess.Unlock() 51 | } 52 | -------------------------------------------------------------------------------- /executor_impl_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | // extern CRONET_EXPORT void cronetExecutorExecute(Cronet_ExecutorPtr self,Cronet_RunnablePtr command); 9 | import "C" 10 | 11 | import ( 12 | "sync" 13 | "sync/atomic" 14 | "unsafe" 15 | ) 16 | 17 | type executorEntry struct { 18 | executeFunc ExecutorExecuteFunc 19 | destroyed atomic.Bool 20 | } 21 | 22 | var ( 23 | executorAccess sync.RWMutex 24 | executors map[uintptr]*executorEntry 25 | ) 26 | 27 | func init() { 28 | executors = make(map[uintptr]*executorEntry) 29 | } 30 | 31 | func NewExecutor(executeFunc ExecutorExecuteFunc) Executor { 32 | if executeFunc == nil { 33 | panic("nil executor execute function") 34 | } 35 | ptr := C.Cronet_Executor_CreateWith((*[0]byte)(C.cronetExecutorExecute)) 36 | ptrVal := uintptr(unsafe.Pointer(ptr)) 37 | executorAccess.Lock() 38 | executors[ptrVal] = &executorEntry{executeFunc: executeFunc} 39 | executorAccess.Unlock() 40 | return Executor{ptrVal} 41 | } 42 | 43 | func (e Executor) Destroy() { 44 | executorAccess.Lock() 45 | entry := executors[e.ptr] 46 | if entry != nil { 47 | entry.destroyed.Store(true) 48 | } 49 | executorAccess.Unlock() 50 | C.Cronet_Executor_Destroy(C.Cronet_ExecutorPtr(unsafe.Pointer(e.ptr))) 51 | // Cleanup after C destroy 52 | executorAccess.Lock() 53 | delete(executors, e.ptr) 54 | executorAccess.Unlock() 55 | } 56 | 57 | //export cronetExecutorExecute 58 | func cronetExecutorExecute(self C.Cronet_ExecutorPtr, command C.Cronet_RunnablePtr) { 59 | executorAccess.RLock() 60 | entry := executors[uintptr(unsafe.Pointer(self))] 61 | executorAccess.RUnlock() 62 | if entry == nil || entry.destroyed.Load() { 63 | return // Post-destroy callback, silently ignore 64 | } 65 | entry.executeFunc(Executor{uintptr(unsafe.Pointer(self))}, Runnable{uintptr(unsafe.Pointer(command))}) 66 | } 67 | -------------------------------------------------------------------------------- /buffer_callback_impl.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | // extern CRONET_EXPORT void cronetBufferCallbackOnDestroy(Cronet_BufferCallbackPtr self,Cronet_BufferPtr buffer); 9 | import "C" 10 | 11 | import ( 12 | "sync" 13 | "sync/atomic" 14 | "unsafe" 15 | ) 16 | 17 | type bufferCallbackEntry struct { 18 | callback BufferCallbackFunc 19 | destroyed atomic.Bool 20 | } 21 | 22 | var ( 23 | bufferCallbackAccess sync.RWMutex 24 | bufferCallbackMap map[uintptr]*bufferCallbackEntry 25 | ) 26 | 27 | func init() { 28 | bufferCallbackMap = make(map[uintptr]*bufferCallbackEntry) 29 | } 30 | 31 | func NewBufferCallback(callbackFunc BufferCallbackFunc) BufferCallback { 32 | ptr := C.Cronet_BufferCallback_CreateWith((*[0]byte)(C.cronetBufferCallbackOnDestroy)) 33 | ptrVal := uintptr(unsafe.Pointer(ptr)) 34 | if callbackFunc != nil { 35 | bufferCallbackAccess.Lock() 36 | bufferCallbackMap[ptrVal] = &bufferCallbackEntry{callback: callbackFunc} 37 | bufferCallbackAccess.Unlock() 38 | } 39 | return BufferCallback{ptrVal} 40 | } 41 | 42 | func (c BufferCallback) destroy() { 43 | bufferCallbackAccess.RLock() 44 | entry := bufferCallbackMap[c.ptr] 45 | bufferCallbackAccess.RUnlock() 46 | if entry != nil { 47 | entry.destroyed.Store(true) 48 | } 49 | } 50 | 51 | //export cronetBufferCallbackOnDestroy 52 | func cronetBufferCallbackOnDestroy(self C.Cronet_BufferCallbackPtr, buffer C.Cronet_BufferPtr) { 53 | ptrInt := uintptr(unsafe.Pointer(self)) 54 | bufferCallbackAccess.RLock() 55 | entry := bufferCallbackMap[ptrInt] 56 | bufferCallbackAccess.RUnlock() 57 | if entry == nil || entry.destroyed.Load() { 58 | return // Post-destroy callback, silently ignore 59 | } 60 | if entry.callback != nil { 61 | entry.callback(BufferCallback{ptrInt}, Buffer{uintptr(unsafe.Pointer(buffer))}) 62 | } 63 | // OnDestroy is the cleanup signal - safe to delete 64 | bufferCallbackAccess.Lock() 65 | delete(bufferCallbackMap, ptrInt) 66 | bufferCallbackAccess.Unlock() 67 | } 68 | -------------------------------------------------------------------------------- /error_mapping_test.go: -------------------------------------------------------------------------------- 1 | package cronet 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "syscall" 7 | "testing" 8 | ) 9 | 10 | func TestMapDialErrorToNetError(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | err error 14 | expected int 15 | }{ 16 | {"nil error", nil, 0}, 17 | {"connection refused syscall", &os.SyscallError{Syscall: "connect", Err: syscall.ECONNREFUSED}, -102}, 18 | {"timeout syscall", &os.SyscallError{Syscall: "connect", Err: syscall.ETIMEDOUT}, -118}, 19 | {"network unreachable syscall", &os.SyscallError{Syscall: "connect", Err: syscall.ENETUNREACH}, -109}, 20 | {"host unreachable syscall", &os.SyscallError{Syscall: "connect", Err: syscall.EHOSTUNREACH}, -109}, 21 | {"connection reset syscall", &os.SyscallError{Syscall: "read", Err: syscall.ECONNRESET}, -101}, 22 | {"connection aborted syscall", &os.SyscallError{Syscall: "read", Err: syscall.ECONNABORTED}, -103}, 23 | {"connection refused string", errors.New("connection refused"), -102}, 24 | {"timeout string", errors.New("i/o timeout"), -118}, 25 | {"connection timed out string", errors.New("connection timed out"), -118}, 26 | {"network unreachable string", errors.New("network is unreachable"), -109}, 27 | {"no route string", errors.New("no route to host"), -109}, 28 | {"connection reset string", errors.New("connection reset by peer"), -101}, 29 | {"unknown error", errors.New("some unknown error"), -104}, 30 | } 31 | 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | result := mapDialErrorToNetError(tt.err) 35 | if result != tt.expected { 36 | t.Errorf("mapDialErrorToNetError(%v) = %d, want %d", tt.err, result, tt.expected) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func TestMapDialErrorToNetError_WrappedError(t *testing.T) { 43 | // Test error wrapping 44 | baseErr := syscall.ECONNREFUSED 45 | wrappedErr := &os.SyscallError{Syscall: "connect", Err: baseErr} 46 | 47 | result := mapDialErrorToNetError(wrappedErr) 48 | if result != -102 { 49 | t.Errorf("wrapped ECONNREFUSED: got %d, want -102", result) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /naive_client_fd_unix.go: -------------------------------------------------------------------------------- 1 | //go:build unix 2 | 3 | package cronet 4 | 5 | import ( 6 | "net" 7 | "os" 8 | "syscall" 9 | 10 | E "github.com/sagernet/sing/common/exceptions" 11 | ) 12 | 13 | // dupSocketFD extracts and duplicates the socket file descriptor from a syscall.Conn. 14 | // Returns a new independent FD; the caller should close both the returned FD (after use) 15 | // and the original connection (immediately after this call). 16 | func dupSocketFD(syscallConn syscall.Conn) (int, error) { 17 | rawConn, err := syscallConn.SyscallConn() 18 | if err != nil { 19 | return -1, E.Cause(err, "get syscall conn") 20 | } 21 | var fd int 22 | var controlError error 23 | err = rawConn.Control(func(fdPtr uintptr) { 24 | // Duplicate the file descriptor so we can transfer ownership 25 | newFD, dupError := syscall.Dup(int(fdPtr)) 26 | if dupError != nil { 27 | controlError = E.Cause(dupError, "dup socket fd") 28 | return 29 | } 30 | syscall.CloseOnExec(newFD) 31 | fd = newFD 32 | }) 33 | if err != nil { 34 | return -1, E.Cause(err, "control raw conn") 35 | } 36 | if controlError != nil { 37 | return -1, controlError 38 | } 39 | return fd, nil 40 | } 41 | 42 | // createSocketPair creates a bidirectional socket pair for the proxy fallback. 43 | // Returns the cronet-side FD and a net.Conn for the proxy side. 44 | // The caller is responsible for closing both the FD and the connection. 45 | func createSocketPair() (cronetFD int, proxyConn net.Conn, err error) { 46 | fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) 47 | if err != nil { 48 | return -1, nil, E.Cause(err, "create socketpair") 49 | } 50 | 51 | syscall.CloseOnExec(fds[0]) 52 | 53 | // fds[0] goes to cronet, fds[1] wraps as net.Conn for proxy. 54 | // FileConn duplicates the fd, so close the original. 55 | file := os.NewFile(uintptr(fds[1]), "cronet-socketpair") 56 | conn, err := net.FileConn(file) 57 | _ = file.Close() 58 | if err != nil { 59 | syscall.Close(fds[0]) 60 | return -1, nil, E.Cause(err, "create net conn from socketpair") 61 | } 62 | return fds[0], conn, nil 63 | } 64 | -------------------------------------------------------------------------------- /buffer_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | 10 | import ( 11 | "unsafe" 12 | ) 13 | 14 | func NewBuffer() Buffer { 15 | return Buffer{uintptr(unsafe.Pointer(C.Cronet_Buffer_Create()))} 16 | } 17 | 18 | func (b Buffer) Destroy() { 19 | C.Cronet_Buffer_Destroy(C.Cronet_BufferPtr(unsafe.Pointer(b.ptr))) 20 | } 21 | 22 | // InitWithDataAndCallback initialize Buffer with raw buffer |data| of |size| allocated by the app. 23 | // The |callback| is invoked when buffer is destroyed. 24 | func (b Buffer) InitWithDataAndCallback(data []byte, callback BufferCallback) { 25 | C.Cronet_Buffer_InitWithDataAndCallback(C.Cronet_BufferPtr(unsafe.Pointer(b.ptr)), C.Cronet_RawDataPtr(unsafe.Pointer(&data[0])), C.uint64_t(len(data)), C.Cronet_BufferCallbackPtr(unsafe.Pointer(callback.ptr))) 26 | } 27 | 28 | // InitWithAlloc initialize Buffer by allocating buffer of |size|. 29 | // The content of allocated data is not initialized. 30 | func (b Buffer) InitWithAlloc(size int64) { 31 | C.Cronet_Buffer_InitWithAlloc(C.Cronet_BufferPtr(unsafe.Pointer(b.ptr)), C.uint64_t(size)) 32 | } 33 | 34 | // Size return size of data owned by this buffer. 35 | func (b Buffer) Size() int64 { 36 | return int64(C.Cronet_Buffer_GetSize(C.Cronet_BufferPtr(unsafe.Pointer(b.ptr)))) 37 | } 38 | 39 | // Data return raw pointer to |data| owned by this buffer. 40 | func (b Buffer) Data() unsafe.Pointer { 41 | return unsafe.Pointer(C.Cronet_Buffer_GetData(C.Cronet_BufferPtr(unsafe.Pointer(b.ptr)))) 42 | } 43 | 44 | func (b Buffer) DataSlice() []byte { 45 | size := b.Size() 46 | if size == 0 { 47 | return nil 48 | } 49 | return unsafe.Slice((*byte)(b.Data()), size) 50 | } 51 | 52 | func (b Buffer) SetClientContext(context unsafe.Pointer) { 53 | C.Cronet_Buffer_SetClientContext(C.Cronet_BufferPtr(unsafe.Pointer(b.ptr)), C.Cronet_ClientContext(context)) 54 | } 55 | 56 | func (b Buffer) ClientContext() unsafe.Pointer { 57 | return unsafe.Pointer(C.Cronet_Buffer_GetClientContext(C.Cronet_BufferPtr(unsafe.Pointer(b.ptr)))) 58 | } 59 | -------------------------------------------------------------------------------- /url_request_status_listener_impl_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | // extern CRONET_EXPORT void cronetURLRequestStatusListenerOnStatus(Cronet_UrlRequestStatusListenerPtr self, Cronet_UrlRequestStatusListener_Status status); 9 | import "C" 10 | 11 | import ( 12 | "sync" 13 | "unsafe" 14 | ) 15 | 16 | func NewURLRequestStatusListener(onStatusFunc URLRequestStatusListenerOnStatusFunc) URLRequestStatusListener { 17 | if onStatusFunc == nil { 18 | panic("nil url request status listener function") 19 | } 20 | ptr := C.Cronet_UrlRequestStatusListener_CreateWith((*[0]byte)(C.cronetURLRequestStatusListenerOnStatus)) 21 | ptrVal := uintptr(unsafe.Pointer(ptr)) 22 | urlRequestStatusListenerAccess.Lock() 23 | urlRequestStatusListenerMap[ptrVal] = onStatusFunc 24 | urlRequestStatusListenerAccess.Unlock() 25 | return URLRequestStatusListener{ptrVal} 26 | } 27 | 28 | func (l URLRequestStatusListener) Destroy() { 29 | C.Cronet_UrlRequestStatusListener_Destroy(C.Cronet_UrlRequestStatusListenerPtr(unsafe.Pointer(l.ptr))) 30 | urlRequestStatusListenerAccess.Lock() 31 | delete(urlRequestStatusListenerMap, l.ptr) 32 | urlRequestStatusListenerAccess.Unlock() 33 | } 34 | 35 | var ( 36 | urlRequestStatusListenerAccess sync.RWMutex 37 | urlRequestStatusListenerMap map[uintptr]URLRequestStatusListenerOnStatusFunc 38 | ) 39 | 40 | func init() { 41 | urlRequestStatusListenerMap = make(map[uintptr]URLRequestStatusListenerOnStatusFunc) 42 | } 43 | 44 | //export cronetURLRequestStatusListenerOnStatus 45 | func cronetURLRequestStatusListenerOnStatus(self C.Cronet_UrlRequestStatusListenerPtr, status C.Cronet_UrlRequestStatusListener_Status) { 46 | ptr := uintptr(unsafe.Pointer(self)) 47 | urlRequestStatusListenerAccess.Lock() 48 | listener := urlRequestStatusListenerMap[ptr] 49 | delete(urlRequestStatusListenerMap, ptr) 50 | urlRequestStatusListenerAccess.Unlock() 51 | if listener == nil { 52 | return // Race with Destroy() or already invoked - silently return 53 | } 54 | listener(URLRequestStatusListener{ptr}, URLRequestStatusListenerStatus(status)) 55 | } 56 | -------------------------------------------------------------------------------- /executor_impl_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | // Design Philosophy: Fail-Fast 6 | // 7 | // This file intentionally does NOT use panic recovery in callbacks. 8 | // If a callback handler panics, the process should crash immediately. 9 | // Using recover() would mask programming errors and make debugging harder. 10 | // Errors should be visible and cause immediate failure, not be silently swallowed. 11 | // 12 | // Note: Post-destroy callbacks silently return because they are expected 13 | // async API behavior, not programming errors. 14 | 15 | import ( 16 | "sync" 17 | "sync/atomic" 18 | 19 | "github.com/sagernet/cronet-go/internal/cronet" 20 | 21 | "github.com/ebitengine/purego" 22 | ) 23 | 24 | type executorEntry struct { 25 | executeFunc ExecutorExecuteFunc 26 | destroyed atomic.Bool 27 | } 28 | 29 | var ( 30 | executorAccess sync.RWMutex 31 | executors map[uintptr]*executorEntry 32 | executorCallbackFn uintptr 33 | ) 34 | 35 | func init() { 36 | executors = make(map[uintptr]*executorEntry) 37 | executorCallbackFn = purego.NewCallback(executorExecuteCallback) 38 | } 39 | 40 | func executorExecuteCallback(self, command uintptr) uintptr { 41 | executorAccess.RLock() 42 | entry := executors[self] 43 | executorAccess.RUnlock() 44 | if entry == nil || entry.destroyed.Load() { 45 | return 0 // Post-destroy callback, silently ignore 46 | } 47 | entry.executeFunc(Executor{self}, Runnable{command}) 48 | return 0 49 | } 50 | 51 | func NewExecutor(executeFunc ExecutorExecuteFunc) Executor { 52 | if executeFunc == nil { 53 | panic("nil executor execute function") 54 | } 55 | ptr := cronet.ExecutorCreateWith(executorCallbackFn) 56 | executorAccess.Lock() 57 | executors[ptr] = &executorEntry{executeFunc: executeFunc} 58 | executorAccess.Unlock() 59 | return Executor{ptr} 60 | } 61 | 62 | func (e Executor) Destroy() { 63 | executorAccess.Lock() 64 | entry := executors[e.ptr] 65 | if entry != nil { 66 | entry.destroyed.Store(true) 67 | } 68 | executorAccess.Unlock() 69 | cronet.ExecutorDestroy(e.ptr) 70 | // Cleanup after C destroy 71 | executorAccess.Lock() 72 | delete(executors, e.ptr) 73 | executorAccess.Unlock() 74 | } 75 | -------------------------------------------------------------------------------- /error_codes.go: -------------------------------------------------------------------------------- 1 | package cronet 2 | 3 | // ErrorCode represents the error code returned by cronet. 4 | type ErrorCode int 5 | 6 | const ( 7 | // ErrorCodeErrorCallback indicating the error returned by app callback. 8 | ErrorCodeErrorCallback ErrorCode = 0 9 | 10 | // ErrorCodeErrorHostnameNotResolved indicating the host being sent the request could not be resolved to an IP address. 11 | ErrorCodeErrorHostnameNotResolved ErrorCode = 1 12 | 13 | // ErrorCodeErrorInternetDisconnected indicating the device was not connected to any network. 14 | ErrorCodeErrorInternetDisconnected ErrorCode = 2 15 | 16 | // ErrorCodeErrorNetworkChanged indicating that as the request was processed the network configuration changed. 17 | ErrorCodeErrorNetworkChanged ErrorCode = 3 18 | 19 | // ErrorCodeErrorTimedOut indicating a timeout expired. Timeouts expiring while attempting to connect will 20 | // be reported as the more specific ErrorCodeErrorConnectionTimedOut. 21 | ErrorCodeErrorTimedOut ErrorCode = 4 22 | 23 | // ErrorCodeErrorConnectionClosed indicating the connection was closed unexpectedly. 24 | ErrorCodeErrorConnectionClosed ErrorCode = 5 25 | 26 | // ErrorCodeErrorConnectionTimedOut indicating the connection attempt timed out. 27 | ErrorCodeErrorConnectionTimedOut ErrorCode = 6 28 | 29 | // ErrorCodeErrorConnectionRefused indicating the connection attempt was refused. 30 | ErrorCodeErrorConnectionRefused ErrorCode = 7 31 | 32 | // ErrorCodeErrorConnectionReset indicating the connection was unexpectedly reset. 33 | ErrorCodeErrorConnectionReset ErrorCode = 8 34 | 35 | // ErrorCodeErrorAddressUnreachable indicating the IP address being contacted is unreachable, 36 | // meaning there is no route to the specified host or network. 37 | ErrorCodeErrorAddressUnreachable ErrorCode = 9 38 | 39 | // ErrorCodeErrorQuicProtocolFailed indicating an error related to the QUIC protocol. 40 | // When Error.ErrorCode() is this code, see Error.QuicDetailedErrorCode() for more information. 41 | ErrorCodeErrorQuicProtocolFailed ErrorCode = 10 42 | 43 | // ErrorCodeErrorOther indicating another type of error was encountered. 44 | // Error.InternalErrorCode() can be consulted to get a more specific cause. 45 | ErrorCodeErrorOther ErrorCode = 11 46 | ) 47 | -------------------------------------------------------------------------------- /callback_types.go: -------------------------------------------------------------------------------- 1 | package cronet 2 | 3 | // ExecutorExecuteFunc takes ownership of |command| and runs it synchronously or asynchronously. 4 | // Destroys the |command| after execution, or if executor is shutting down. 5 | type ExecutorExecuteFunc func(executor Executor, command Runnable) 6 | 7 | // RunnableRunFunc is the function type for Runnable.Run callback. 8 | type RunnableRunFunc func(self Runnable) 9 | 10 | // BufferCallbackFunc is called when the Buffer is destroyed. 11 | type BufferCallbackFunc func(callback BufferCallback, buffer Buffer) 12 | 13 | // URLRequestStatusListenerOnStatusFunc is called with the status of a URL request. 14 | type URLRequestStatusListenerOnStatusFunc func(self URLRequestStatusListener, status URLRequestStatusListenerStatus) 15 | 16 | // URLRequestFinishedInfoListenerOnRequestFinishedFunc is called when a request finishes. 17 | type URLRequestFinishedInfoListenerOnRequestFinishedFunc func(listener URLRequestFinishedInfoListener, requestInfo RequestFinishedInfo, responseInfo URLResponseInfo, error Error) 18 | 19 | // URLRequestCallbackHandler handles callbacks from URLRequest. 20 | type URLRequestCallbackHandler interface { 21 | // OnRedirectReceived is invoked whenever a redirect is encountered. 22 | OnRedirectReceived(self URLRequestCallback, request URLRequest, info URLResponseInfo, newLocationUrl string) 23 | 24 | // OnResponseStarted is invoked when the final set of headers, after all redirects, is received. 25 | OnResponseStarted(self URLRequestCallback, request URLRequest, info URLResponseInfo) 26 | 27 | // OnReadCompleted is invoked whenever part of the response body has been read. 28 | OnReadCompleted(self URLRequestCallback, request URLRequest, info URLResponseInfo, buffer Buffer, bytesRead int64) 29 | 30 | // OnSucceeded is invoked when request is completed successfully. 31 | OnSucceeded(self URLRequestCallback, request URLRequest, info URLResponseInfo) 32 | 33 | // OnFailed is invoked if request failed for any reason after URLRequest.Start(). 34 | OnFailed(self URLRequestCallback, request URLRequest, info URLResponseInfo, error Error) 35 | 36 | // OnCanceled is invoked if request was canceled via URLRequest.Cancel(). 37 | OnCanceled(self URLRequestCallback, request URLRequest, info URLResponseInfo) 38 | } 39 | -------------------------------------------------------------------------------- /upload_data_sink_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | import "unsafe" 10 | 11 | // OnReadSucceeded 12 | // 13 | // Called by UploadDataProviderHandler when a read succeeds. 14 | // 15 | // @param bytesRead number of bytes read into buffer passed to UploadDataProviderHandler.Read(). 16 | // @param finalChunk For chunked uploads, |true| if this is the final 17 | // 18 | // read. It must be |false| for non-chunked uploads. 19 | func (s UploadDataSink) OnReadSucceeded(bytesRead int64, finalChunk bool) { 20 | C.Cronet_UploadDataSink_OnReadSucceeded(C.Cronet_UploadDataSinkPtr(unsafe.Pointer(s.ptr)), C.uint64_t(bytesRead), C.bool(finalChunk)) 21 | } 22 | 23 | // OnReadError 24 | // Called by UploadDataProviderHandler when a read fails. 25 | // @param message to pass on to URLRequestCallbackHandler.OnFailed(). 26 | func (s UploadDataSink) OnReadError(message string) { 27 | cMessage := C.CString(message) 28 | C.Cronet_UploadDataSink_OnReadError(C.Cronet_UploadDataSinkPtr(unsafe.Pointer(s.ptr)), cMessage) 29 | C.free(unsafe.Pointer(cMessage)) 30 | } 31 | 32 | // OnRewindSucceeded 33 | // Called by UploadDataProviderHandler when a rewind succeeds. 34 | func (s UploadDataSink) OnRewindSucceeded() { 35 | C.Cronet_UploadDataSink_OnRewindSucceeded(C.Cronet_UploadDataSinkPtr(unsafe.Pointer(s.ptr))) 36 | } 37 | 38 | // OnRewindError 39 | // Called by UploadDataProviderHandler when a rewind fails, or if rewinding 40 | // uploads is not supported. 41 | // * @param message to pass on to URLRequestCallbackHandler.OnFailed(). 42 | func (s UploadDataSink) OnRewindError(message string) { 43 | cMessage := C.CString(message) 44 | C.Cronet_UploadDataSink_OnRewindError(C.Cronet_UploadDataSinkPtr(unsafe.Pointer(s.ptr)), cMessage) 45 | C.free(unsafe.Pointer(cMessage)) 46 | } 47 | 48 | func (s UploadDataSink) SetClientContext(context unsafe.Pointer) { 49 | C.Cronet_UploadDataSink_SetClientContext(C.Cronet_UploadDataSinkPtr(unsafe.Pointer(s.ptr)), C.Cronet_ClientContext(context)) 50 | } 51 | 52 | func (s UploadDataSink) ClientContext() unsafe.Pointer { 53 | return unsafe.Pointer(C.Cronet_UploadDataSink_GetClientContext(C.Cronet_UploadDataSinkPtr(unsafe.Pointer(s.ptr)))) 54 | } 55 | -------------------------------------------------------------------------------- /upload_data_provider_handler.go: -------------------------------------------------------------------------------- 1 | package cronet 2 | 3 | // UploadDataProviderHandler is the interface that must be implemented to provide upload data. 4 | type UploadDataProviderHandler interface { 5 | // Length 6 | // If this is a non-chunked upload, returns the length of the upload. Must 7 | // always return -1 if this is a chunked upload. 8 | Length(self UploadDataProvider) int64 9 | 10 | // Read 11 | // Reads upload data into |buffer|. Each call of this method must be followed be a 12 | // single call, either synchronous or asynchronous, to 13 | // UploadDataSink.OnReadSucceeded() on success 14 | // or UploadDataSink.OnReadError() on failure. Neither read nor rewind 15 | // will be called until one of those methods or the other is called. Even if 16 | // the associated UrlRequest is canceled, one or the other must 17 | // still be called before resources can be safely freed. 18 | // 19 | // @param sink The object to notify when the read has completed, 20 | // successfully or otherwise. 21 | // @param buffer The buffer to copy the read bytes into. 22 | Read(self UploadDataProvider, sink UploadDataSink, buffer Buffer) 23 | 24 | // Rewind 25 | // Rewinds upload data. Each call must be followed be a single 26 | // call, either synchronous or asynchronous, to 27 | // UploadDataSink.OnRewindSucceeded() on success or 28 | // UploadDataSink.OnRewindError() on failure. Neither read nor rewind 29 | // will be called until one of those methods or the other is called. 30 | // Even if the associated UrlRequest is canceled, one or the other 31 | // must still be called before resources can be safely freed. 32 | // 33 | // If rewinding is not supported, this should call 34 | // UploadDataSink.OnRewindError(). Note that rewinding is required to 35 | // follow redirects that preserve the upload body, and for retrying when the 36 | // server times out stale sockets. 37 | // 38 | // @param sink The object to notify when the rewind operation has 39 | // completed, successfully or otherwise. 40 | Rewind(self UploadDataProvider, sink UploadDataSink) 41 | 42 | // Close 43 | // Called when this UploadDataProvider is no longer needed by a request, so that resources 44 | // (like a file) can be explicitly released. 45 | Close(self UploadDataProvider) 46 | } 47 | -------------------------------------------------------------------------------- /url_request_finished_info_listener_impl_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "sync" 7 | "sync/atomic" 8 | 9 | "github.com/sagernet/cronet-go/internal/cronet" 10 | 11 | "github.com/ebitengine/purego" 12 | ) 13 | 14 | type urlRequestFinishedInfoListenerEntry struct { 15 | handler URLRequestFinishedInfoListenerOnRequestFinishedFunc 16 | destroyed atomic.Bool 17 | } 18 | 19 | var ( 20 | urlRequestFinishedInfoListenerAccess sync.RWMutex 21 | urlRequestFinishedInfoListenerMap map[uintptr]*urlRequestFinishedInfoListenerEntry 22 | urlRequestFinishedInfoListenerOnRequestFinishedCallback uintptr 23 | ) 24 | 25 | func init() { 26 | urlRequestFinishedInfoListenerMap = make(map[uintptr]*urlRequestFinishedInfoListenerEntry) 27 | urlRequestFinishedInfoListenerOnRequestFinishedCallback = purego.NewCallback(cronetURLRequestFinishedInfoListenerOnRequestFinished) 28 | } 29 | 30 | func cronetURLRequestFinishedInfoListenerOnRequestFinished(self, requestInfo, responseInfo, errorPtr uintptr) uintptr { 31 | urlRequestFinishedInfoListenerAccess.RLock() 32 | entry := urlRequestFinishedInfoListenerMap[self] 33 | urlRequestFinishedInfoListenerAccess.RUnlock() 34 | if entry == nil || entry.destroyed.Load() { 35 | return 0 // Post-destroy callback, silently ignore 36 | } 37 | entry.handler( 38 | URLRequestFinishedInfoListener{self}, 39 | RequestFinishedInfo{requestInfo}, 40 | URLResponseInfo{responseInfo}, 41 | Error{errorPtr}, 42 | ) 43 | return 0 44 | } 45 | 46 | func NewURLRequestFinishedInfoListener(finishedFunc URLRequestFinishedInfoListenerOnRequestFinishedFunc) URLRequestFinishedInfoListener { 47 | if finishedFunc == nil { 48 | panic("nil url request finished info listener function") 49 | } 50 | ptr := cronet.RequestFinishedInfoListenerCreateWith(urlRequestFinishedInfoListenerOnRequestFinishedCallback) 51 | urlRequestFinishedInfoListenerAccess.Lock() 52 | urlRequestFinishedInfoListenerMap[ptr] = &urlRequestFinishedInfoListenerEntry{handler: finishedFunc} 53 | urlRequestFinishedInfoListenerAccess.Unlock() 54 | return URLRequestFinishedInfoListener{ptr} 55 | } 56 | 57 | func (l URLRequestFinishedInfoListener) destroy() { 58 | urlRequestFinishedInfoListenerAccess.Lock() 59 | entry := urlRequestFinishedInfoListenerMap[l.ptr] 60 | if entry != nil { 61 | entry.destroyed.Store(true) 62 | } 63 | urlRequestFinishedInfoListenerAccess.Unlock() 64 | cronet.RequestFinishedInfoListenerDestroy(l.ptr) 65 | // Cleanup after C destroy 66 | urlRequestFinishedInfoListenerAccess.Lock() 67 | delete(urlRequestFinishedInfoListenerMap, l.ptr) 68 | urlRequestFinishedInfoListenerAccess.Unlock() 69 | } 70 | -------------------------------------------------------------------------------- /net_error.go: -------------------------------------------------------------------------------- 1 | package cronet 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "strconv" 7 | "syscall" 8 | ) 9 | 10 | //go:generate go run ./cmd/generate-net-errors 11 | 12 | // NetError represents a Chromium network error code. 13 | // Error codes are negative integers defined in Chromium's net/base/net_error_list.h. 14 | type NetError int 15 | 16 | // Error implements the error interface with a Go-style lowercase message. 17 | func (e NetError) Error() string { 18 | if info, ok := netErrorInfo[e]; ok { 19 | return info.message 20 | } 21 | return "network error " + strconv.Itoa(int(e)) 22 | } 23 | 24 | // Name returns the Chromium error name (e.g., "ERR_CONNECTION_REFUSED"). 25 | func (e NetError) Name() string { 26 | if info, ok := netErrorInfo[e]; ok { 27 | return info.name 28 | } 29 | return "ERR_UNKNOWN_" + strconv.Itoa(int(-e)) 30 | } 31 | 32 | // Description returns the full description from Chromium source. 33 | func (e NetError) Description() string { 34 | if info, ok := netErrorInfo[e]; ok { 35 | return info.description 36 | } 37 | return "" 38 | } 39 | 40 | // Code returns the raw error code as an integer. 41 | func (e NetError) Code() int { 42 | return int(e) 43 | } 44 | 45 | // Is implements errors.Is() support for comparing NetError with Go standard errors. 46 | func (e NetError) Is(target error) bool { 47 | if t, ok := target.(NetError); ok { 48 | return e == t 49 | } 50 | 51 | switch target { 52 | case net.ErrClosed: 53 | return e == NetErrorConnectionClosed || 54 | e == NetErrorSocketNotConnected || 55 | e == NetErrorConnectionReset || 56 | e == NetErrorConnectionAborted 57 | 58 | case os.ErrDeadlineExceeded: 59 | return e == NetErrorTimedOut || 60 | e == NetErrorConnectionTimedOut 61 | 62 | case syscall.ECONNREFUSED: 63 | return e == NetErrorConnectionRefused 64 | 65 | case syscall.ECONNRESET: 66 | return e == NetErrorConnectionReset 67 | 68 | case syscall.ECONNABORTED: 69 | return e == NetErrorConnectionAborted 70 | 71 | case syscall.ETIMEDOUT: 72 | return e == NetErrorConnectionTimedOut 73 | 74 | case syscall.ENETUNREACH, syscall.EHOSTUNREACH: 75 | return e == NetErrorAddressUnreachable 76 | 77 | case syscall.ENOTCONN: 78 | return e == NetErrorSocketNotConnected 79 | } 80 | 81 | return false 82 | } 83 | 84 | // Timeout returns true if this error represents a timeout. 85 | // This implements the net.Error interface. 86 | func (e NetError) Timeout() bool { 87 | return e == NetErrorTimedOut || e == NetErrorConnectionTimedOut 88 | } 89 | 90 | // Temporary returns false. Chromium errors are not considered temporary. 91 | // This implements the net.Error interface. 92 | func (e NetError) Temporary() bool { 93 | return false 94 | } 95 | -------------------------------------------------------------------------------- /engine_params_experimental_options.go: -------------------------------------------------------------------------------- 1 | package cronet 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | ) 7 | 8 | func (p EngineParams) SetExperimentalOption(key string, value any) error { 9 | options := strings.TrimSpace(p.ExperimentalOptions()) 10 | 11 | experimentalOptions := make(map[string]any) 12 | if options != "" { 13 | if err := json.Unmarshal([]byte(options), &experimentalOptions); err != nil { 14 | return err 15 | } 16 | } 17 | 18 | if value == nil { 19 | delete(experimentalOptions, key) 20 | } else { 21 | experimentalOptions[key] = value 22 | } 23 | 24 | encoded, err := json.Marshal(experimentalOptions) 25 | if err != nil { 26 | return err 27 | } 28 | p.SetExperimentalOptions(string(encoded)) 29 | return nil 30 | } 31 | 32 | func (p EngineParams) SetAsyncDNS(enable bool) error { 33 | if !enable { 34 | return p.SetExperimentalOption("AsyncDNS", nil) 35 | } 36 | return p.SetExperimentalOption("AsyncDNS", map[string]any{ 37 | "enable": true, 38 | }) 39 | } 40 | 41 | // SetDNSServerOverride configures Cronet's built-in DNS client to exclusively use the 42 | // provided nameserver addresses. 43 | // 44 | // The nameserver entries must be IP literals, in "ip:port" form (IPv6 in "[ip]:port" 45 | // form). Passing an empty slice disables the override. 46 | func (p EngineParams) SetDNSServerOverride(nameservers []string) error { 47 | if len(nameservers) == 0 { 48 | return p.SetExperimentalOption("DnsServerOverride", nil) 49 | } 50 | return p.SetExperimentalOption("DnsServerOverride", map[string]any{ 51 | "nameservers": nameservers, 52 | }) 53 | } 54 | 55 | // SetHostResolverRules sets rules to override DNS resolution. 56 | // Format: "MAP hostname ip" or "MAP *.example.com ip" or "EXCLUDE hostname". 57 | // Multiple rules can be separated by commas: "MAP foo 1.2.3.4, MAP bar 5.6.7.8". 58 | // See net/dns/mapped_host_resolver.h for full format. 59 | func (p EngineParams) SetHostResolverRules(rules string) error { 60 | if rules == "" { 61 | return p.SetExperimentalOption("HostResolverRules", nil) 62 | } 63 | return p.SetExperimentalOption("HostResolverRules", map[string]any{ 64 | "host_resolver_rules": rules, 65 | }) 66 | } 67 | 68 | // SetUseDnsHttpsSvcb enables or disables DNS HTTPS SVCB record lookups. 69 | // When enabled, Chromium will query DNS for HTTPS records (type 65) which can 70 | // contain ECH (Encrypted Client Hello) configurations and ALPN hints. 71 | // This is required for ECH support. 72 | func (p EngineParams) SetUseDnsHttpsSvcb(enable bool) error { 73 | if !enable { 74 | return p.SetExperimentalOption("UseDnsHttpsSvcb", nil) 75 | } 76 | return p.SetExperimentalOption("UseDnsHttpsSvcb", map[string]any{ 77 | "enable": true, 78 | }) 79 | } 80 | -------------------------------------------------------------------------------- /url_request_finished_info_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | 10 | import "unsafe" 11 | 12 | // Note: RequestFinishedInfo is used in types.go, but the C type uses different naming. 13 | // The Go type is defined in types.go as RequestFinishedInfo (without URL prefix for internal consistency). 14 | 15 | func NewURLRequestFinishedInfo() RequestFinishedInfo { 16 | return RequestFinishedInfo{uintptr(unsafe.Pointer(C.Cronet_RequestFinishedInfo_Create()))} 17 | } 18 | 19 | func (i RequestFinishedInfo) Destroy() { 20 | C.Cronet_RequestFinishedInfo_Destroy(C.Cronet_RequestFinishedInfoPtr(unsafe.Pointer(i.ptr))) 21 | } 22 | 23 | // Metrics 24 | // Metrics collected for this request. 25 | func (i RequestFinishedInfo) Metrics() Metrics { 26 | return Metrics{uintptr(unsafe.Pointer(C.Cronet_RequestFinishedInfo_metrics_get(C.Cronet_RequestFinishedInfoPtr(unsafe.Pointer(i.ptr)))))} 27 | } 28 | 29 | // AnnotationSize 30 | // The objects that the caller has supplied when initiating the request, 31 | // using URLRequestParams.AddAnnotation 32 | // 33 | // Annotations can be used to associate a RequestFinishedInfo with 34 | // the original request or type of request. 35 | func (i RequestFinishedInfo) AnnotationSize() int { 36 | return int(C.Cronet_RequestFinishedInfo_annotations_size(C.Cronet_RequestFinishedInfoPtr(unsafe.Pointer(i.ptr)))) 37 | } 38 | 39 | func (i RequestFinishedInfo) AnnotationAt(index int) unsafe.Pointer { 40 | return unsafe.Pointer(C.Cronet_RequestFinishedInfo_annotations_at(C.Cronet_RequestFinishedInfoPtr(unsafe.Pointer(i.ptr)), C.uint32_t(index))) 41 | } 42 | 43 | // FinishedReason 44 | // Returns the reason why the request finished. 45 | func (i RequestFinishedInfo) FinishedReason() URLRequestFinishedInfoFinishedReason { 46 | return URLRequestFinishedInfoFinishedReason(C.Cronet_RequestFinishedInfo_finished_reason_get(C.Cronet_RequestFinishedInfoPtr(unsafe.Pointer(i.ptr)))) 47 | } 48 | 49 | func (i RequestFinishedInfo) SetMetrics(metrics Metrics) { 50 | C.Cronet_RequestFinishedInfo_metrics_set(C.Cronet_RequestFinishedInfoPtr(unsafe.Pointer(i.ptr)), C.Cronet_MetricsPtr(unsafe.Pointer(metrics.ptr))) 51 | } 52 | 53 | func (i RequestFinishedInfo) AddAnnotation(annotation unsafe.Pointer) { 54 | C.Cronet_RequestFinishedInfo_annotations_add(C.Cronet_RequestFinishedInfoPtr(unsafe.Pointer(i.ptr)), C.Cronet_RawDataPtr(annotation)) 55 | } 56 | 57 | func (i RequestFinishedInfo) ClearAnnotations() { 58 | C.Cronet_RequestFinishedInfo_annotations_clear(C.Cronet_RequestFinishedInfoPtr(unsafe.Pointer(i.ptr))) 59 | } 60 | 61 | func (i RequestFinishedInfo) SetFinishedReason(reason URLRequestFinishedInfoFinishedReason) { 62 | C.Cronet_RequestFinishedInfo_finished_reason_set(C.Cronet_RequestFinishedInfoPtr(unsafe.Pointer(i.ptr)), C.Cronet_RequestFinishedInfo_FINISHED_REASON(reason)) 63 | } 64 | -------------------------------------------------------------------------------- /url_request_finished_info_impl_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | // extern CRONET_EXPORT void cronetURLRequestFinishedInfoListenerOnRequestFinished(Cronet_RequestFinishedInfoListenerPtr self, Cronet_RequestFinishedInfoPtr request_info, Cronet_UrlResponseInfoPtr response_info, Cronet_ErrorPtr error); 9 | import "C" 10 | 11 | import ( 12 | "sync" 13 | "sync/atomic" 14 | "unsafe" 15 | ) 16 | 17 | type urlRequestFinishedInfoListenerEntry struct { 18 | handler URLRequestFinishedInfoListenerOnRequestFinishedFunc 19 | destroyed atomic.Bool 20 | } 21 | 22 | var ( 23 | urlRequestFinishedInfoListenerAccess sync.RWMutex 24 | urlRequestFinishedInfoListenerMap map[uintptr]*urlRequestFinishedInfoListenerEntry 25 | ) 26 | 27 | func init() { 28 | urlRequestFinishedInfoListenerMap = make(map[uintptr]*urlRequestFinishedInfoListenerEntry) 29 | } 30 | 31 | func NewURLRequestFinishedInfoListener(finishedFunc URLRequestFinishedInfoListenerOnRequestFinishedFunc) URLRequestFinishedInfoListener { 32 | if finishedFunc == nil { 33 | panic("nil url request finished info listener function") 34 | } 35 | ptr := C.Cronet_RequestFinishedInfoListener_CreateWith((*[0]byte)(C.cronetURLRequestFinishedInfoListenerOnRequestFinished)) 36 | ptrVal := uintptr(unsafe.Pointer(ptr)) 37 | urlRequestFinishedInfoListenerAccess.Lock() 38 | urlRequestFinishedInfoListenerMap[ptrVal] = &urlRequestFinishedInfoListenerEntry{handler: finishedFunc} 39 | urlRequestFinishedInfoListenerAccess.Unlock() 40 | return URLRequestFinishedInfoListener{ptrVal} 41 | } 42 | 43 | func (l URLRequestFinishedInfoListener) Destroy() { 44 | urlRequestFinishedInfoListenerAccess.Lock() 45 | entry := urlRequestFinishedInfoListenerMap[l.ptr] 46 | if entry != nil { 47 | entry.destroyed.Store(true) 48 | } 49 | urlRequestFinishedInfoListenerAccess.Unlock() 50 | C.Cronet_RequestFinishedInfoListener_Destroy(C.Cronet_RequestFinishedInfoListenerPtr(unsafe.Pointer(l.ptr))) 51 | // Cleanup after C destroy 52 | urlRequestFinishedInfoListenerAccess.Lock() 53 | delete(urlRequestFinishedInfoListenerMap, l.ptr) 54 | urlRequestFinishedInfoListenerAccess.Unlock() 55 | } 56 | 57 | //export cronetURLRequestFinishedInfoListenerOnRequestFinished 58 | func cronetURLRequestFinishedInfoListenerOnRequestFinished(self C.Cronet_RequestFinishedInfoListenerPtr, requestInfo C.Cronet_RequestFinishedInfoPtr, responseInfo C.Cronet_UrlResponseInfoPtr, error C.Cronet_ErrorPtr) { 59 | ptr := uintptr(unsafe.Pointer(self)) 60 | urlRequestFinishedInfoListenerAccess.RLock() 61 | entry := urlRequestFinishedInfoListenerMap[ptr] 62 | urlRequestFinishedInfoListenerAccess.RUnlock() 63 | if entry == nil || entry.destroyed.Load() { 64 | return // Post-destroy callback, silently ignore 65 | } 66 | entry.handler(URLRequestFinishedInfoListener{ptr}, RequestFinishedInfo{uintptr(unsafe.Pointer(requestInfo))}, URLResponseInfo{uintptr(unsafe.Pointer(responseInfo))}, Error{uintptr(unsafe.Pointer(error))}) 67 | } 68 | -------------------------------------------------------------------------------- /public_key_pins_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | 10 | import ( 11 | "unsafe" 12 | ) 13 | 14 | func NewPublicKeyPins() PublicKeyPins { 15 | return PublicKeyPins{uintptr(unsafe.Pointer(C.Cronet_PublicKeyPins_Create()))} 16 | } 17 | 18 | func (p PublicKeyPins) Destroy() { 19 | C.Cronet_PublicKeyPins_Destroy(C.Cronet_PublicKeyPinsPtr(unsafe.Pointer(p.ptr))) 20 | } 21 | 22 | // SetHost set name of the host to which the public keys should be pinned. A host that 23 | // consists only of digits and the dot character is treated as invalid. 24 | func (p PublicKeyPins) SetHost(host string) { 25 | cHost := C.CString(host) 26 | C.Cronet_PublicKeyPins_host_set(C.Cronet_PublicKeyPinsPtr(unsafe.Pointer(p.ptr)), cHost) 27 | C.free(unsafe.Pointer(cHost)) 28 | } 29 | 30 | func (p PublicKeyPins) Host() string { 31 | return C.GoString(C.Cronet_PublicKeyPins_host_get(C.Cronet_PublicKeyPinsPtr(unsafe.Pointer(p.ptr)))) 32 | } 33 | 34 | // AddPinnedSHA256 add pins. each pin is the SHA-256 cryptographic 35 | // hash (in the form of "sha256/") of the DER-encoded ASN.1 36 | // representation of the Subject Public Key Info (SPKI) of the host's X.509 certificate. 37 | // Although, the method does not mandate the presence of the backup pin 38 | // that can be used if the control of the primary private key has been 39 | // lost, it is highly recommended to supply one. 40 | func (p PublicKeyPins) AddPinnedSHA256(hash string) { 41 | cHash := C.CString(hash) 42 | C.Cronet_PublicKeyPins_pins_sha256_add(C.Cronet_PublicKeyPinsPtr(unsafe.Pointer(p.ptr)), cHash) 43 | C.free(unsafe.Pointer(cHash)) 44 | } 45 | 46 | func (p PublicKeyPins) PinnedSHA256Size() int { 47 | return int(C.Cronet_PublicKeyPins_pins_sha256_size(C.Cronet_PublicKeyPinsPtr(unsafe.Pointer(p.ptr)))) 48 | } 49 | 50 | func (p PublicKeyPins) PinnedSHA256At(index int) string { 51 | return C.GoString(C.Cronet_PublicKeyPins_pins_sha256_at(C.Cronet_PublicKeyPinsPtr(unsafe.Pointer(p.ptr)), C.uint32_t(index))) 52 | } 53 | 54 | func (p PublicKeyPins) ClearPinnedSHA256() { 55 | C.Cronet_PublicKeyPins_pins_sha256_clear(C.Cronet_PublicKeyPinsPtr(unsafe.Pointer(p.ptr))) 56 | } 57 | 58 | // SetIncludeSubdomains set whether the pinning policy should be applied to subdomains of |host|. 59 | func (p PublicKeyPins) SetIncludeSubdomains(includeSubdomains bool) { 60 | C.Cronet_PublicKeyPins_include_subdomains_set(C.Cronet_PublicKeyPinsPtr(unsafe.Pointer(p.ptr)), C.bool(includeSubdomains)) 61 | } 62 | 63 | func (p PublicKeyPins) IncludeSubdomains() bool { 64 | return bool(C.Cronet_PublicKeyPins_include_subdomains_get(C.Cronet_PublicKeyPinsPtr(unsafe.Pointer(p.ptr)))) 65 | } 66 | 67 | // SetExpirationDate set the expiration date for the pins in milliseconds since epoch (as in java.util.Date). 68 | func (p PublicKeyPins) SetExpirationDate(date int64) { 69 | C.Cronet_PublicKeyPins_expiration_date_set(C.Cronet_PublicKeyPinsPtr(unsafe.Pointer(p.ptr)), C.int64_t(date)) 70 | } 71 | 72 | func (p PublicKeyPins) ExpirationDate() int64 { 73 | return int64(C.Cronet_PublicKeyPins_expiration_date_get(C.Cronet_PublicKeyPinsPtr(unsafe.Pointer(p.ptr)))) 74 | } 75 | -------------------------------------------------------------------------------- /error_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | 10 | import "unsafe" 11 | 12 | func NewError() Error { 13 | return Error{uintptr(unsafe.Pointer(C.Cronet_Error_Create()))} 14 | } 15 | 16 | func (e Error) Destroy() { 17 | C.Cronet_Error_Destroy(C.Cronet_ErrorPtr(unsafe.Pointer(e.ptr))) 18 | } 19 | 20 | // ErrorCode return the error code, one of ErrorCode values. 21 | func (e Error) ErrorCode() ErrorCode { 22 | return ErrorCode(C.Cronet_Error_error_code_get(C.Cronet_ErrorPtr(unsafe.Pointer(e.ptr)))) 23 | } 24 | 25 | // Message explaining the error. 26 | func (e Error) Message() string { 27 | return C.GoString(C.Cronet_Error_message_get(C.Cronet_ErrorPtr(unsafe.Pointer(e.ptr)))) 28 | } 29 | 30 | // InternalErrorCode is the cronet internal error code. This may provide more specific error 31 | // diagnosis than ErrorCode(), but the constant values may change over time. 32 | // See 33 | // here 34 | // for the latest list of values. 35 | func (e Error) InternalErrorCode() int { 36 | return int(C.Cronet_Error_internal_error_code_get(C.Cronet_ErrorPtr(unsafe.Pointer(e.ptr)))) 37 | } 38 | 39 | // Retryable |true| if retrying this request right away might succeed, |false| 40 | // otherwise. For example, is |true| when ErrorCode() is ErrorCodeErrorNetworkChanged 41 | // because trying the request might succeed using the new 42 | // network configuration, but |false| when ErrorCode() is 43 | // ErrorCodeErrorInternetDisconnected because retrying the request right away will 44 | // encounter the same failure (instead retrying should be delayed until device regains 45 | // network connectivity). 46 | func (e Error) Retryable() bool { 47 | return bool(C.Cronet_Error_immediately_retryable_get(C.Cronet_ErrorPtr(unsafe.Pointer(e.ptr)))) 48 | } 49 | 50 | // QuicDetailedErrorCode contains detailed QUIC error code from 51 | // 52 | // QuicErrorCode when the ErrorCode() code is ErrorCodeErrorQuicProtocolFailed. 53 | func (e Error) QuicDetailedErrorCode() int { 54 | return int(C.Cronet_Error_quic_detailed_error_code_get(C.Cronet_ErrorPtr(unsafe.Pointer(e.ptr)))) 55 | } 56 | 57 | func (e Error) SetErrorCode(code ErrorCode) { 58 | C.Cronet_Error_error_code_set(C.Cronet_ErrorPtr(unsafe.Pointer(e.ptr)), C.Cronet_Error_ERROR_CODE(code)) 59 | } 60 | 61 | func (e Error) SetMessage(message string) { 62 | cMessage := C.CString(message) 63 | C.Cronet_Error_message_set(C.Cronet_ErrorPtr(unsafe.Pointer(e.ptr)), cMessage) 64 | C.free(unsafe.Pointer(cMessage)) 65 | } 66 | 67 | func (e Error) SetInternalErrorCode(code int32) { 68 | C.Cronet_Error_internal_error_code_set(C.Cronet_ErrorPtr(unsafe.Pointer(e.ptr)), C.int32_t(code)) 69 | } 70 | 71 | func (e Error) SetRetryable(retryable bool) { 72 | C.Cronet_Error_immediately_retryable_set(C.Cronet_ErrorPtr(unsafe.Pointer(e.ptr)), C.bool(retryable)) 73 | } 74 | 75 | func (e Error) SetQuicDetailedErrorCode(code int32) { 76 | C.Cronet_Error_quic_detailed_error_code_set(C.Cronet_ErrorPtr(unsafe.Pointer(e.ptr)), C.int32_t(code)) 77 | } 78 | -------------------------------------------------------------------------------- /buffer_impl_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | // extern CRONET_EXPORT void cronetBufferInitWithDataAndCallback(Cronet_BufferPtr self, Cronet_RawDataPtr data, uint64_t size, Cronet_BufferCallbackPtr callback); 9 | // extern CRONET_EXPORT void cronetBufferInitWithAlloc(Cronet_BufferPtr self, uint64_t size); 10 | // extern CRONET_EXPORT uint64_t cronetBufferGetSize(Cronet_BufferPtr self); 11 | // extern CRONET_EXPORT Cronet_RawDataPtr cronetBufferGetData(Cronet_BufferPtr self); 12 | import "C" 13 | 14 | import ( 15 | "sync" 16 | "unsafe" 17 | ) 18 | 19 | // BufferHandler is an interface for custom Buffer implementations (for testing/mocking). 20 | type BufferHandler interface { 21 | InitWithDataAndCallback(self Buffer, data unsafe.Pointer, size uint64, callback BufferCallback) 22 | InitWithAlloc(self Buffer, size uint64) 23 | GetSize(self Buffer) uint64 24 | GetData(self Buffer) unsafe.Pointer 25 | } 26 | 27 | // NewBufferWith creates a new Buffer with custom handler (for testing/mocking). 28 | func NewBufferWith(handler BufferHandler) Buffer { 29 | ptr := C.Cronet_Buffer_CreateWith( 30 | (*[0]byte)(C.cronetBufferInitWithDataAndCallback), 31 | (*[0]byte)(C.cronetBufferInitWithAlloc), 32 | (*[0]byte)(C.cronetBufferGetSize), 33 | (*[0]byte)(C.cronetBufferGetData), 34 | ) 35 | ptrVal := uintptr(unsafe.Pointer(ptr)) 36 | bufferHandlerAccess.Lock() 37 | bufferHandlerMap[ptrVal] = handler 38 | bufferHandlerAccess.Unlock() 39 | return Buffer{ptrVal} 40 | } 41 | 42 | var ( 43 | bufferHandlerAccess sync.RWMutex 44 | bufferHandlerMap map[uintptr]BufferHandler 45 | ) 46 | 47 | func init() { 48 | bufferHandlerMap = make(map[uintptr]BufferHandler) 49 | } 50 | 51 | func instanceOfBufferHandler(self C.Cronet_BufferPtr) BufferHandler { 52 | bufferHandlerAccess.RLock() 53 | defer bufferHandlerAccess.RUnlock() 54 | return bufferHandlerMap[uintptr(unsafe.Pointer(self))] 55 | } 56 | 57 | //export cronetBufferInitWithDataAndCallback 58 | func cronetBufferInitWithDataAndCallback(self C.Cronet_BufferPtr, data C.Cronet_RawDataPtr, size C.uint64_t, callback C.Cronet_BufferCallbackPtr) { 59 | handler := instanceOfBufferHandler(self) 60 | if handler != nil { 61 | handler.InitWithDataAndCallback(Buffer{uintptr(unsafe.Pointer(self))}, unsafe.Pointer(data), uint64(size), BufferCallback{uintptr(unsafe.Pointer(callback))}) 62 | } 63 | } 64 | 65 | //export cronetBufferInitWithAlloc 66 | func cronetBufferInitWithAlloc(self C.Cronet_BufferPtr, size C.uint64_t) { 67 | handler := instanceOfBufferHandler(self) 68 | if handler != nil { 69 | handler.InitWithAlloc(Buffer{uintptr(unsafe.Pointer(self))}, uint64(size)) 70 | } 71 | } 72 | 73 | //export cronetBufferGetSize 74 | func cronetBufferGetSize(self C.Cronet_BufferPtr) C.uint64_t { 75 | handler := instanceOfBufferHandler(self) 76 | if handler != nil { 77 | return C.uint64_t(handler.GetSize(Buffer{uintptr(unsafe.Pointer(self))})) 78 | } 79 | return 0 80 | } 81 | 82 | //export cronetBufferGetData 83 | func cronetBufferGetData(self C.Cronet_BufferPtr) C.Cronet_RawDataPtr { 84 | handler := instanceOfBufferHandler(self) 85 | if handler != nil { 86 | return C.Cronet_RawDataPtr(handler.GetData(Buffer{uintptr(unsafe.Pointer(self))})) 87 | } 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /cmd/build-naive/cmd_sync.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var commandSync = &cobra.Command{ 16 | Use: "sync", 17 | Short: "Download Chromium cronet components", 18 | Run: func(cmd *cobra.Command, args []string) { 19 | sync() 20 | }, 21 | } 22 | 23 | func init() { 24 | mainCommand.AddCommand(commandSync) 25 | } 26 | 27 | func sync() { 28 | log.Print("Syncing Chromium cronet components...") 29 | 30 | versionFile := filepath.Join(naiveRoot, "CHROMIUM_VERSION") 31 | versionData, err := os.ReadFile(versionFile) 32 | if err != nil { 33 | log.Fatalf("failed to read CHROMIUM_VERSION: %v", err) 34 | } 35 | version := strings.TrimSpace(string(versionData)) 36 | log.Printf("Chromium version: %s", version) 37 | 38 | cronetDirectory := filepath.Join(srcRoot, "components", "cronet") 39 | if _, err := os.Stat(cronetDirectory); err == nil { 40 | status := runCommandOutput(naiveRoot, "git", "status", "--porcelain", "src/components/cronet") 41 | if strings.TrimSpace(status) == "" { 42 | log.Print("Components already up to date") 43 | return 44 | } 45 | } 46 | 47 | components := []string{"cronet", "grpc_support", "prefs"} 48 | 49 | for _, name := range components { 50 | log.Printf("Downloading %s...", name) 51 | 52 | url := fmt.Sprintf( 53 | "https://chromium.googlesource.com/chromium/src/+archive/refs/tags/%s/components/%s.tar.gz", 54 | version, name) 55 | 56 | destinationDirectory := filepath.Join(srcRoot, "components", name) 57 | 58 | os.RemoveAll(destinationDirectory) 59 | err := os.MkdirAll(destinationDirectory, 0o755) 60 | if err != nil { 61 | log.Fatalf("failed to create directory %s: %v", destinationDirectory, err) 62 | } 63 | 64 | err = downloadAndExtract(url, destinationDirectory) 65 | if err != nil { 66 | log.Fatalf("failed to download %s: %v", name, err) 67 | } 68 | 69 | log.Printf("Downloaded %s", name) 70 | } 71 | 72 | log.Print("Creating git commit...") 73 | runCommand(naiveRoot, "git", "add", 74 | "src/components/cronet", 75 | "src/components/grpc_support", 76 | "src/components/prefs") 77 | 78 | commitMessage := fmt.Sprintf(`Add Chromium cronet components (v%s) 79 | 80 | Downloaded from Chromium source: 81 | - components/cronet/ 82 | - components/grpc_support/ 83 | - components/prefs/ 84 | 85 | Use 'go run ./cmd/build-naive sync' to re-download.`, version) 86 | 87 | runCommand(naiveRoot, "git", "commit", "-m", commitMessage) 88 | 89 | log.Print("Sync complete!") 90 | } 91 | 92 | func downloadAndExtract(url, destinationDirectory string) error { 93 | response, err := http.Get(url) 94 | if err != nil { 95 | return fmt.Errorf("HTTP request failed: %w", err) 96 | } 97 | defer response.Body.Close() 98 | 99 | if response.StatusCode != http.StatusOK { 100 | return fmt.Errorf("HTTP %d: %s", response.StatusCode, response.Status) 101 | } 102 | 103 | // Use tar command (simpler than using archive/tar with gzip) 104 | command := exec.Command("tar", "-xzf", "-", "-C", destinationDirectory) 105 | command.Stdin = response.Body 106 | command.Stdout = os.Stdout 107 | command.Stderr = os.Stderr 108 | 109 | err = command.Run() 110 | if err != nil { 111 | return fmt.Errorf("tar extraction failed: %w", err) 112 | } 113 | 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /socket_fd_unix_test.go: -------------------------------------------------------------------------------- 1 | //go:build unix 2 | 3 | package cronet 4 | 5 | import ( 6 | "io" 7 | "net" 8 | "syscall" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestDupSocketFD(t *testing.T) { 14 | // Create a local TCP server 15 | listener, err := net.Listen("tcp", "127.0.0.1:0") 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | defer listener.Close() 20 | 21 | serverDone := make(chan struct{}) 22 | go func() { 23 | defer close(serverDone) 24 | conn, err := listener.Accept() 25 | if err != nil { 26 | return 27 | } 28 | defer conn.Close() 29 | io.Copy(conn, conn) // echo server 30 | }() 31 | 32 | // Dial the server 33 | conn, err := net.Dial("tcp", listener.Addr().String()) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | // Extract and duplicate the FD 39 | tcpConn := conn.(*net.TCPConn) 40 | fd, err := dupSocketFD(tcpConn) 41 | if err != nil { 42 | conn.Close() 43 | t.Fatal(err) 44 | } 45 | 46 | // Close the original connection 47 | conn.Close() 48 | 49 | // Verify the duplicated FD is valid and usable 50 | if fd < 0 { 51 | t.Fatal("invalid fd returned") 52 | } 53 | 54 | // Clean up 55 | syscall.Close(fd) 56 | } 57 | 58 | func TestDupSocketFD_InvalidConn(t *testing.T) { 59 | // Test with a connection that doesn't support syscall.Conn 60 | // This would require a mock - skip for now as it's hard to create 61 | // a net.Conn that doesn't implement syscall.Conn 62 | t.Skip("requires mock connection") 63 | } 64 | 65 | func TestCreateSocketPair(t *testing.T) { 66 | fd, conn, err := createSocketPair() 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | defer syscall.Close(fd) 71 | defer conn.Close() 72 | 73 | if fd < 0 { 74 | t.Fatal("invalid fd returned") 75 | } 76 | 77 | // Test bidirectional communication: fd -> conn 78 | testData := []byte("hello from fd side") 79 | done := make(chan error, 1) 80 | go func() { 81 | _, err := syscall.Write(fd, testData) 82 | done <- err 83 | }() 84 | 85 | buf := make([]byte, len(testData)) 86 | conn.SetReadDeadline(time.Now().Add(5 * time.Second)) 87 | _, err = io.ReadFull(conn, buf) 88 | if err != nil { 89 | t.Fatalf("failed to read from conn: %v", err) 90 | } 91 | if string(buf) != string(testData) { 92 | t.Errorf("data mismatch: got %q, want %q", buf, testData) 93 | } 94 | 95 | if err := <-done; err != nil { 96 | t.Fatalf("failed to write to fd: %v", err) 97 | } 98 | 99 | // Test bidirectional communication: conn -> fd 100 | testData2 := []byte("hello from conn side") 101 | go func() { 102 | conn.Write(testData2) 103 | }() 104 | 105 | buf2 := make([]byte, len(testData2)) 106 | n, err := syscall.Read(fd, buf2) 107 | if err != nil { 108 | t.Fatalf("failed to read from fd: %v", err) 109 | } 110 | if n != len(testData2) || string(buf2[:n]) != string(testData2) { 111 | t.Errorf("data mismatch: got %q, want %q", buf2[:n], testData2) 112 | } 113 | } 114 | 115 | func TestCreateSocketPair_MultipleCreation(t *testing.T) { 116 | // Test that we can create multiple socket pairs 117 | pairs := make([]struct { 118 | fd int 119 | conn net.Conn 120 | }, 5) 121 | 122 | for i := range pairs { 123 | fd, conn, err := createSocketPair() 124 | if err != nil { 125 | t.Fatalf("failed to create socket pair %d: %v", i, err) 126 | } 127 | pairs[i].fd = fd 128 | pairs[i].conn = conn 129 | } 130 | 131 | // Clean up 132 | for _, pair := range pairs { 133 | syscall.Close(pair.fd) 134 | pair.conn.Close() 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /upload_data_provider_impl_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "sync" 7 | "sync/atomic" 8 | 9 | "github.com/sagernet/cronet-go/internal/cronet" 10 | 11 | "github.com/ebitengine/purego" 12 | ) 13 | 14 | type uploadDataProviderEntry struct { 15 | handler UploadDataProviderHandler 16 | destroyed atomic.Bool 17 | } 18 | 19 | var ( 20 | uploadDataAccess sync.RWMutex 21 | uploadDataProviderMap map[uintptr]*uploadDataProviderEntry 22 | 23 | uploadDataProviderGetLength uintptr 24 | uploadDataProviderRead uintptr 25 | uploadDataProviderRewind uintptr 26 | uploadDataProviderClose uintptr 27 | ) 28 | 29 | func init() { 30 | uploadDataProviderMap = make(map[uintptr]*uploadDataProviderEntry) 31 | 32 | uploadDataProviderGetLength = purego.NewCallback(onGetLengthCallback) 33 | uploadDataProviderRead = purego.NewCallback(onReadCallback) 34 | uploadDataProviderRewind = purego.NewCallback(onRewindCallback) 35 | uploadDataProviderClose = purego.NewCallback(onCloseCallback) 36 | } 37 | 38 | func instanceOfUploadDataProvider(self uintptr) UploadDataProviderHandler { 39 | uploadDataAccess.RLock() 40 | defer uploadDataAccess.RUnlock() 41 | entry := uploadDataProviderMap[self] 42 | if entry == nil || entry.destroyed.Load() { 43 | return nil 44 | } 45 | return entry.handler 46 | } 47 | 48 | func onGetLengthCallback(self uintptr) uintptr { 49 | handler := instanceOfUploadDataProvider(self) 50 | if handler == nil { 51 | return 0 // Post-destroy callback, return 0 52 | } 53 | return uintptr(handler.Length(UploadDataProvider{self})) 54 | } 55 | 56 | func onReadCallback(self, sink, buffer uintptr) uintptr { 57 | handler := instanceOfUploadDataProvider(self) 58 | if handler == nil { 59 | return 0 // Post-destroy callback, silently ignore 60 | } 61 | handler.Read( 62 | UploadDataProvider{self}, 63 | UploadDataSink{sink}, 64 | Buffer{buffer}, 65 | ) 66 | return 0 67 | } 68 | 69 | func onRewindCallback(self, sink uintptr) uintptr { 70 | handler := instanceOfUploadDataProvider(self) 71 | if handler == nil { 72 | return 0 // Post-destroy callback, silently ignore 73 | } 74 | handler.Rewind( 75 | UploadDataProvider{self}, 76 | UploadDataSink{sink}, 77 | ) 78 | return 0 79 | } 80 | 81 | func onCloseCallback(self uintptr) uintptr { 82 | handler := instanceOfUploadDataProvider(self) 83 | if handler == nil { 84 | return 0 // Post-destroy callback, silently ignore 85 | } 86 | handler.Close(UploadDataProvider{self}) 87 | // Close is terminal callback - safe to cleanup 88 | uploadDataAccess.Lock() 89 | delete(uploadDataProviderMap, self) 90 | uploadDataAccess.Unlock() 91 | return 0 92 | } 93 | 94 | func NewUploadDataProvider(handler UploadDataProviderHandler) UploadDataProvider { 95 | if handler == nil { 96 | panic("nil upload data provider handler") 97 | } 98 | ptr := cronet.UploadDataProviderCreateWith( 99 | uploadDataProviderGetLength, 100 | uploadDataProviderRead, 101 | uploadDataProviderRewind, 102 | uploadDataProviderClose, 103 | ) 104 | uploadDataAccess.Lock() 105 | uploadDataProviderMap[ptr] = &uploadDataProviderEntry{handler: handler} 106 | uploadDataAccess.Unlock() 107 | return UploadDataProvider{ptr} 108 | } 109 | 110 | func (p UploadDataProvider) Destroy() { 111 | uploadDataAccess.RLock() 112 | entry := uploadDataProviderMap[p.ptr] 113 | uploadDataAccess.RUnlock() 114 | if entry != nil { 115 | entry.destroyed.Store(true) 116 | } 117 | cronet.UploadDataProviderDestroy(p.ptr) 118 | } 119 | -------------------------------------------------------------------------------- /cmd/build-naive/cmd_env.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var ( 13 | commandEnv = &cobra.Command{ 14 | Use: "env", 15 | Short: "Output environment variables for building downstream projects", 16 | Run: func(cmd *cobra.Command, args []string) { 17 | targets := parseTargets() 18 | if len(targets) != 1 { 19 | log.Fatal("env requires exactly one target") 20 | } 21 | printEnv(targets[0]) 22 | }, 23 | } 24 | envExport bool 25 | ) 26 | 27 | func init() { 28 | mainCommand.AddCommand(commandEnv) 29 | commandEnv.Flags().BoolVar(&envExport, "export", false, "Prefix output with 'export' for use with eval") 30 | } 31 | 32 | func getClangTarget(t Target) string { 33 | if t.Libc == "musl" { 34 | switch t.CPU { 35 | case "x64": 36 | return "x86_64-openwrt-linux-musl" 37 | case "arm64": 38 | return "aarch64-openwrt-linux-musl" 39 | case "x86": 40 | return "i486-openwrt-linux-musl" 41 | case "arm": 42 | return "arm-openwrt-linux-musleabi" 43 | } 44 | } 45 | switch t.CPU { 46 | case "x64": 47 | return "x86_64-linux-gnu" 48 | case "arm64": 49 | return "aarch64-linux-gnu" 50 | case "x86": 51 | return "i686-linux-gnu" 52 | case "arm": 53 | return "arm-linux-gnueabihf" 54 | } 55 | return "" 56 | } 57 | 58 | func getSysrootPath(t Target) string { 59 | if t.Libc == "musl" { 60 | config := getOpenwrtConfig(t) 61 | return filepath.Join(srcRoot, "out/sysroot-build/openwrt", config.release, config.arch) 62 | } 63 | sysrootArch := map[string]string{ 64 | "x64": "amd64", 65 | "arm64": "arm64", 66 | "x86": "i386", 67 | "arm": "armhf", 68 | }[t.CPU] 69 | return filepath.Join(srcRoot, "out/sysroot-build/bullseye", "bullseye_"+sysrootArch+"_staging") 70 | } 71 | 72 | func printEnv(t Target) { 73 | if t.GOOS == "windows" { 74 | log.Fatal("env command is not supported for Windows (use purego mode with embedded DLL)") 75 | } 76 | 77 | prefix := "" 78 | if envExport { 79 | prefix = "export " 80 | } 81 | 82 | // CGO_LDFLAGS: Only output toolchain flags that cannot be in #cgo LDFLAGS. 83 | // Library paths and system libs are in the generated lib_*_cgo.go files. 84 | if t.GOOS == "linux" { 85 | var ldFlags []string 86 | ldFlags = append(ldFlags, "-fuse-ld=lld") 87 | if t.ARCH == "386" || t.ARCH == "arm" { 88 | ldFlags = append(ldFlags, "-no-pie") 89 | } 90 | fmt.Printf("%sCGO_LDFLAGS=%s\n", prefix, shellQuote(strings.Join(ldFlags, " "), envExport)) 91 | } 92 | // Darwin/iOS: No CGO_LDFLAGS needed, all flags are in the generated cgo files 93 | 94 | // Linux-specific: CC, CXX for cross-compilation, QEMU_LD_PREFIX for running binaries 95 | if t.GOOS == "linux" { 96 | clangPath := filepath.Join(srcRoot, "third_party/llvm-build/Release+Asserts/bin/clang") 97 | clangTarget := getClangTarget(t) 98 | sysroot := getSysrootPath(t) 99 | 100 | cc := fmt.Sprintf("%s --target=%s --sysroot=%s", clangPath, clangTarget, sysroot) 101 | cxx := fmt.Sprintf("%s++ --target=%s --sysroot=%s", clangPath, clangTarget, sysroot) 102 | 103 | fmt.Printf("%sCC=%s\n", prefix, shellQuote(cc, envExport)) 104 | fmt.Printf("%sCXX=%s\n", prefix, shellQuote(cxx, envExport)) 105 | fmt.Printf("%sQEMU_LD_PREFIX=%s\n", prefix, sysroot) 106 | } 107 | } 108 | 109 | func shellQuote(s string, quote bool) string { 110 | if quote && strings.ContainsAny(s, " \t\n\"'\\$") { 111 | return "\"" + strings.ReplaceAll(s, "\"", "\\\"") + "\"" 112 | } 113 | return s 114 | } 115 | -------------------------------------------------------------------------------- /dns_socketpair_unix.go: -------------------------------------------------------------------------------- 1 | //go:build unix 2 | 3 | package cronet 4 | 5 | import ( 6 | "net" 7 | "os" 8 | "syscall" 9 | 10 | E "github.com/sagernet/sing/common/exceptions" 11 | ) 12 | 13 | func createPacketSocketPair(forceUDPLoopback bool) (cronetFD int, proxyConn net.PacketConn, err error) { 14 | if forceUDPLoopback { 15 | return createUDPLoopbackPair() 16 | } 17 | 18 | fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_DGRAM, 0) 19 | if err != nil { 20 | return -1, nil, E.Cause(err, "create dgram socketpair") 21 | } 22 | 23 | syscall.CloseOnExec(fds[0]) 24 | 25 | file := os.NewFile(uintptr(fds[1]), "cronet-dgram-socketpair") 26 | conn, err := net.FilePacketConn(file) 27 | _ = file.Close() 28 | if err != nil { 29 | syscall.Close(fds[0]) 30 | return -1, nil, E.Cause(err, "create packet conn from socketpair") 31 | } 32 | 33 | return fds[0], conn, nil 34 | } 35 | 36 | func createUDPLoopbackPair() (cronetFD int, proxyConn net.PacketConn, err error) { 37 | // Create two UDP sockets and connect them to each other. 38 | // Both sockets must be connected for bidirectional communication. 39 | 40 | // Step 1: Create proxyConn socket (unconnected initially to get a port) 41 | proxyAddress, err := net.ResolveUDPAddr("udp", "127.0.0.1:0") 42 | if err != nil { 43 | return -1, nil, err 44 | } 45 | proxyUDPConn, err := net.ListenUDP("udp", proxyAddress) 46 | if err != nil { 47 | return -1, nil, err 48 | } 49 | proxyLocalAddr := proxyUDPConn.LocalAddr().(*net.UDPAddr) 50 | 51 | // Step 2: Create cronetConn socket connected to proxyConn 52 | cronetAddress, err := net.ResolveUDPAddr("udp", "127.0.0.1:0") 53 | if err != nil { 54 | proxyUDPConn.Close() 55 | return -1, nil, err 56 | } 57 | cronetConn, err := net.DialUDP("udp", cronetAddress, proxyLocalAddr) 58 | if err != nil { 59 | proxyUDPConn.Close() 60 | return -1, nil, err 61 | } 62 | cronetLocalAddr := cronetConn.LocalAddr().(*net.UDPAddr) 63 | 64 | // Step 3: Connect proxyConn to cronetConn's address using syscall 65 | proxyRawConn, err := proxyUDPConn.SyscallConn() 66 | if err != nil { 67 | cronetConn.Close() 68 | proxyUDPConn.Close() 69 | return -1, nil, err 70 | } 71 | 72 | var connectErr error 73 | err = proxyRawConn.Control(func(fd uintptr) { 74 | sockaddr := &syscall.SockaddrInet4{Port: cronetLocalAddr.Port} 75 | copy(sockaddr.Addr[:], cronetLocalAddr.IP.To4()) 76 | connectErr = syscall.Connect(int(fd), sockaddr) 77 | }) 78 | if err != nil { 79 | cronetConn.Close() 80 | proxyUDPConn.Close() 81 | return -1, nil, err 82 | } 83 | if connectErr != nil { 84 | cronetConn.Close() 85 | proxyUDPConn.Close() 86 | return -1, nil, connectErr 87 | } 88 | 89 | // Step 4: Duplicate cronetConn's fd for Chromium 90 | cronetRawConn, err := cronetConn.SyscallConn() 91 | if err != nil { 92 | cronetConn.Close() 93 | proxyUDPConn.Close() 94 | return -1, nil, err 95 | } 96 | 97 | var cronetFDValue int 98 | var dupErr error 99 | err = cronetRawConn.Control(func(fd uintptr) { 100 | dupFD, controlErr := syscall.Dup(int(fd)) 101 | if controlErr != nil { 102 | dupErr = controlErr 103 | return 104 | } 105 | syscall.CloseOnExec(dupFD) 106 | cronetFDValue = dupFD 107 | }) 108 | if err != nil { 109 | cronetConn.Close() 110 | proxyUDPConn.Close() 111 | return -1, nil, err 112 | } 113 | if dupErr != nil { 114 | cronetConn.Close() 115 | proxyUDPConn.Close() 116 | return -1, nil, dupErr 117 | } 118 | 119 | cronetConn.Close() 120 | 121 | return cronetFDValue, proxyUDPConn, nil 122 | } 123 | 124 | -------------------------------------------------------------------------------- /upload_data_sink_impl_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | // extern CRONET_EXPORT void cronetUploadDataSinkOnReadSucceeded(Cronet_UploadDataSinkPtr self, uint64_t bytes_read, bool final_chunk); 9 | // extern CRONET_EXPORT void cronetUploadDataSinkOnReadError(Cronet_UploadDataSinkPtr self, Cronet_String error_message); 10 | // extern CRONET_EXPORT void cronetUploadDataSinkOnRewindSucceeded(Cronet_UploadDataSinkPtr self); 11 | // extern CRONET_EXPORT void cronetUploadDataSinkOnRewindError(Cronet_UploadDataSinkPtr self, Cronet_String error_message); 12 | import "C" 13 | 14 | import ( 15 | "sync" 16 | "unsafe" 17 | ) 18 | 19 | // UploadDataSinkHandler is an interface for custom UploadDataSink implementations (for testing/mocking). 20 | type UploadDataSinkHandler interface { 21 | OnReadSucceeded(self UploadDataSink, bytesRead uint64, finalChunk bool) 22 | OnReadError(self UploadDataSink, errorMessage string) 23 | OnRewindSucceeded(self UploadDataSink) 24 | OnRewindError(self UploadDataSink, errorMessage string) 25 | } 26 | 27 | // NewUploadDataSinkWith creates a new UploadDataSink with custom handler (for testing/mocking). 28 | func NewUploadDataSinkWith(handler UploadDataSinkHandler) UploadDataSink { 29 | ptr := C.Cronet_UploadDataSink_CreateWith( 30 | (*[0]byte)(C.cronetUploadDataSinkOnReadSucceeded), 31 | (*[0]byte)(C.cronetUploadDataSinkOnReadError), 32 | (*[0]byte)(C.cronetUploadDataSinkOnRewindSucceeded), 33 | (*[0]byte)(C.cronetUploadDataSinkOnRewindError), 34 | ) 35 | ptrVal := uintptr(unsafe.Pointer(ptr)) 36 | uploadDataSinkHandlerAccess.Lock() 37 | uploadDataSinkHandlerMap[ptrVal] = handler 38 | uploadDataSinkHandlerAccess.Unlock() 39 | return UploadDataSink{ptrVal} 40 | } 41 | 42 | var ( 43 | uploadDataSinkHandlerAccess sync.RWMutex 44 | uploadDataSinkHandlerMap map[uintptr]UploadDataSinkHandler 45 | ) 46 | 47 | func init() { 48 | uploadDataSinkHandlerMap = make(map[uintptr]UploadDataSinkHandler) 49 | } 50 | 51 | func instanceOfUploadDataSinkHandler(self C.Cronet_UploadDataSinkPtr) UploadDataSinkHandler { 52 | uploadDataSinkHandlerAccess.RLock() 53 | defer uploadDataSinkHandlerAccess.RUnlock() 54 | return uploadDataSinkHandlerMap[uintptr(unsafe.Pointer(self))] 55 | } 56 | 57 | //export cronetUploadDataSinkOnReadSucceeded 58 | func cronetUploadDataSinkOnReadSucceeded(self C.Cronet_UploadDataSinkPtr, bytesRead C.uint64_t, finalChunk C.bool) { 59 | handler := instanceOfUploadDataSinkHandler(self) 60 | if handler != nil { 61 | handler.OnReadSucceeded(UploadDataSink{uintptr(unsafe.Pointer(self))}, uint64(bytesRead), bool(finalChunk)) 62 | } 63 | } 64 | 65 | //export cronetUploadDataSinkOnReadError 66 | func cronetUploadDataSinkOnReadError(self C.Cronet_UploadDataSinkPtr, errorMessage C.Cronet_String) { 67 | handler := instanceOfUploadDataSinkHandler(self) 68 | if handler != nil { 69 | handler.OnReadError(UploadDataSink{uintptr(unsafe.Pointer(self))}, C.GoString(errorMessage)) 70 | } 71 | } 72 | 73 | //export cronetUploadDataSinkOnRewindSucceeded 74 | func cronetUploadDataSinkOnRewindSucceeded(self C.Cronet_UploadDataSinkPtr) { 75 | handler := instanceOfUploadDataSinkHandler(self) 76 | if handler != nil { 77 | handler.OnRewindSucceeded(UploadDataSink{uintptr(unsafe.Pointer(self))}) 78 | } 79 | } 80 | 81 | //export cronetUploadDataSinkOnRewindError 82 | func cronetUploadDataSinkOnRewindError(self C.Cronet_UploadDataSinkPtr, errorMessage C.Cronet_String) { 83 | handler := instanceOfUploadDataSinkHandler(self) 84 | if handler != nil { 85 | handler.OnRewindError(UploadDataSink{uintptr(unsafe.Pointer(self))}, C.GoString(errorMessage)) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= 5 | github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 6 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 7 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 8 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 9 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 10 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 12 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 13 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 14 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 15 | github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= 16 | github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= 17 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 21 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 22 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 23 | github.com/sagernet/sing v0.7.13 h1:XNYgd8e3cxMULs/LLJspdn/deHrnPWyrrglNHeCUAYM= 24 | github.com/sagernet/sing v0.7.13/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= 25 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 26 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 27 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 28 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 29 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 30 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 31 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 32 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 33 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 34 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 35 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 36 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 37 | golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= 38 | golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 39 | golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= 40 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 42 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 43 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 44 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 45 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 46 | -------------------------------------------------------------------------------- /metrics_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "github.com/sagernet/cronet-go/internal/cronet" 7 | ) 8 | 9 | func NewMetrics() Metrics { 10 | return Metrics{cronet.MetricsCreate()} 11 | } 12 | 13 | func (m Metrics) Destroy() { 14 | cronet.MetricsDestroy(m.ptr) 15 | } 16 | 17 | func (m Metrics) RequestStart() DateTime { 18 | return DateTime{cronet.MetricsRequestStartGet(m.ptr)} 19 | } 20 | 21 | func (m Metrics) DNSStart() DateTime { 22 | return DateTime{cronet.MetricsDnsStartGet(m.ptr)} 23 | } 24 | 25 | func (m Metrics) DNSEnd() DateTime { 26 | return DateTime{cronet.MetricsDnsEndGet(m.ptr)} 27 | } 28 | 29 | func (m Metrics) ConnectStart() DateTime { 30 | return DateTime{cronet.MetricsConnectStartGet(m.ptr)} 31 | } 32 | 33 | func (m Metrics) ConnectEnd() DateTime { 34 | return DateTime{cronet.MetricsConnectEndGet(m.ptr)} 35 | } 36 | 37 | func (m Metrics) SSLStart() DateTime { 38 | return DateTime{cronet.MetricsSslStartGet(m.ptr)} 39 | } 40 | 41 | func (m Metrics) SSLEnd() DateTime { 42 | return DateTime{cronet.MetricsSslEndGet(m.ptr)} 43 | } 44 | 45 | func (m Metrics) SendingStart() DateTime { 46 | return DateTime{cronet.MetricsSendingStartGet(m.ptr)} 47 | } 48 | 49 | func (m Metrics) SendingEnd() DateTime { 50 | return DateTime{cronet.MetricsSendingEndGet(m.ptr)} 51 | } 52 | 53 | func (m Metrics) PushStart() DateTime { 54 | return DateTime{cronet.MetricsPushStartGet(m.ptr)} 55 | } 56 | 57 | func (m Metrics) PushEnd() DateTime { 58 | return DateTime{cronet.MetricsPushEndGet(m.ptr)} 59 | } 60 | 61 | func (m Metrics) ResponseStart() DateTime { 62 | return DateTime{cronet.MetricsResponseStartGet(m.ptr)} 63 | } 64 | 65 | func (m Metrics) ResponseEnd() DateTime { 66 | return DateTime{cronet.MetricsRequestEndGet(m.ptr)} 67 | } 68 | 69 | func (m Metrics) SocketReused() bool { 70 | return cronet.MetricsSocketReusedGet(m.ptr) 71 | } 72 | 73 | func (m Metrics) SentByteCount() int64 { 74 | return cronet.MetricsSentByteCountGet(m.ptr) 75 | } 76 | 77 | func (m Metrics) ReceivedByteCount() int64 { 78 | return cronet.MetricsReceivedByteCountGet(m.ptr) 79 | } 80 | 81 | func (m Metrics) SetRequestStart(t DateTime) { 82 | cronet.MetricsRequestStartSet(m.ptr, t.ptr) 83 | } 84 | 85 | func (m Metrics) SetDNSStart(t DateTime) { 86 | cronet.MetricsDnsStartSet(m.ptr, t.ptr) 87 | } 88 | 89 | func (m Metrics) SetDNSEnd(t DateTime) { 90 | cronet.MetricsDnsEndSet(m.ptr, t.ptr) 91 | } 92 | 93 | func (m Metrics) SetConnectStart(t DateTime) { 94 | cronet.MetricsConnectStartSet(m.ptr, t.ptr) 95 | } 96 | 97 | func (m Metrics) SetConnectEnd(t DateTime) { 98 | cronet.MetricsConnectEndSet(m.ptr, t.ptr) 99 | } 100 | 101 | func (m Metrics) SetSSLStart(t DateTime) { 102 | cronet.MetricsSslStartSet(m.ptr, t.ptr) 103 | } 104 | 105 | func (m Metrics) SetSSLEnd(t DateTime) { 106 | cronet.MetricsSslEndSet(m.ptr, t.ptr) 107 | } 108 | 109 | func (m Metrics) SetSendingStart(t DateTime) { 110 | cronet.MetricsSendingStartSet(m.ptr, t.ptr) 111 | } 112 | 113 | func (m Metrics) SetSendingEnd(t DateTime) { 114 | cronet.MetricsSendingEndSet(m.ptr, t.ptr) 115 | } 116 | 117 | func (m Metrics) SetPushStart(t DateTime) { 118 | cronet.MetricsPushStartSet(m.ptr, t.ptr) 119 | } 120 | 121 | func (m Metrics) SetPushEnd(t DateTime) { 122 | cronet.MetricsPushEndSet(m.ptr, t.ptr) 123 | } 124 | 125 | func (m Metrics) SetResponseStart(t DateTime) { 126 | cronet.MetricsResponseStartSet(m.ptr, t.ptr) 127 | } 128 | 129 | func (m Metrics) SetRequestEnd(t DateTime) { 130 | cronet.MetricsRequestEndSet(m.ptr, t.ptr) 131 | } 132 | 133 | func (m Metrics) SetSocketReused(reused bool) { 134 | cronet.MetricsSocketReusedSet(m.ptr, reused) 135 | } 136 | 137 | func (m Metrics) SetSentByteCount(count int64) { 138 | cronet.MetricsSentByteCountSet(m.ptr, count) 139 | } 140 | 141 | func (m Metrics) SetReceivedByteCount(count int64) { 142 | cronet.MetricsReceivedByteCountSet(m.ptr, count) 143 | } 144 | -------------------------------------------------------------------------------- /cmd/build-naive/cmd_extract_lib.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var ( 15 | commandExtractLib = &cobra.Command{ 16 | Use: "extract-lib", 17 | Short: "Extract dynamic libraries from Go module dependencies", 18 | Long: `Extract dynamic libraries (.so/.dll) from Go module dependencies. 19 | 20 | This command downloads the cronet-go lib submodule for the specified target 21 | and extracts the dynamic library to the output directory. 22 | 23 | Supported targets: 24 | - linux/amd64, linux/arm64, linux/386, linux/arm (glibc only) 25 | - windows/amd64, windows/arm64 26 | 27 | Not supported (use static linking via CGO instead): 28 | - Linux musl (static only) 29 | - macOS, iOS, tvOS, Android`, 30 | Run: func(cmd *cobra.Command, args []string) { 31 | targets := parseTargets() 32 | extractLibraries(targets) 33 | }, 34 | } 35 | outputDirectory string 36 | outputName string 37 | ) 38 | 39 | func init() { 40 | mainCommand.AddCommand(commandExtractLib) 41 | commandExtractLib.Flags().StringVarP(&outputDirectory, "output", "o", ".", 42 | "Output directory for extracted libraries") 43 | commandExtractLib.Flags().StringVarP(&outputName, "name", "n", "", 44 | "Output filename (default: libcronet.so or libcronet.dll)") 45 | } 46 | 47 | func extractLibraries(targets []Target) { 48 | if err := os.MkdirAll(outputDirectory, 0o755); err != nil { 49 | log.Fatalf("failed to create output directory: %v", err) 50 | } 51 | 52 | for _, target := range targets { 53 | extractLibrary(target) 54 | } 55 | 56 | log.Print("Extract complete!") 57 | } 58 | 59 | func extractLibrary(target Target) { 60 | libraryFilename := getDynamicLibraryFilename(target) 61 | if libraryFilename == "" { 62 | log.Fatalf("target %s/%s does not have a dynamic library available (use static linking via CGO instead)", 63 | target.GOOS, target.ARCH) 64 | } 65 | 66 | directoryName := getLibraryDirectoryName(target) 67 | 68 | // Get the latest commit from the go branch 69 | goBranchCommit := runCommandOutput(".", "git", "ls-remote", "https://github.com/sagernet/cronet-go.git", "refs/heads/go") 70 | if goBranchCommit == "" { 71 | log.Fatal("failed to get go branch commit") 72 | } 73 | // Output format: "\trefs/heads/go\n" 74 | commitHash := strings.Fields(goBranchCommit)[0] 75 | 76 | modulePath := fmt.Sprintf("github.com/sagernet/cronet-go/lib/%s@%s", directoryName, commitHash) 77 | 78 | log.Printf("Downloading module %s...", modulePath) 79 | 80 | output := runCommandOutput(".", "go", "mod", "download", "-json", modulePath) 81 | 82 | var moduleInfo struct { 83 | Dir string `json:"Dir"` 84 | } 85 | if err := json.Unmarshal([]byte(output), &moduleInfo); err != nil { 86 | log.Fatalf("failed to parse module info: %v", err) 87 | } 88 | 89 | if moduleInfo.Dir == "" { 90 | log.Fatalf("module directory not found in download output") 91 | } 92 | 93 | sourcePath := filepath.Join(moduleInfo.Dir, libraryFilename) 94 | if _, err := os.Stat(sourcePath); os.IsNotExist(err) { 95 | log.Fatalf("library file not found: %s", sourcePath) 96 | } 97 | 98 | destinationFilename := libraryFilename 99 | if outputName != "" { 100 | destinationFilename = outputName 101 | } 102 | destinationPath := filepath.Join(outputDirectory, destinationFilename) 103 | copyFile(sourcePath, destinationPath) 104 | 105 | log.Printf("Extracted %s to %s", destinationFilename, outputDirectory) 106 | } 107 | 108 | func getDynamicLibraryFilename(target Target) string { 109 | switch target.GOOS { 110 | case "windows": 111 | return "libcronet.dll" 112 | case "linux": 113 | if target.Libc == "musl" { 114 | return "" // musl builds are static only 115 | } 116 | return "libcronet.so" 117 | default: 118 | return "" // macOS, iOS, tvOS, Android use static libraries 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /bidirectional_stream_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "runtime" 7 | "unsafe" 8 | 9 | "github.com/sagernet/cronet-go/internal/cronet" 10 | ) 11 | 12 | func (e Engine) StreamEngine() StreamEngine { 13 | return StreamEngine{cronet.EngineGetStreamEngine(e.ptr)} 14 | } 15 | 16 | // CreateStream creates a new stream object that uses |engine| and |callback|. 17 | func (e StreamEngine) CreateStream(callback BidirectionalStreamCallback) BidirectionalStream { 18 | if callback == nil { 19 | panic("nil bidirectional stream callback") 20 | } 21 | ptr := cronet.BidirectionalStreamCreate(e.ptr, 0, uintptr(unsafe.Pointer(&bsCallbackStructPurego))) 22 | bidirectionalStreamAccess.Lock() 23 | bidirectionalStreamMap[ptr] = &bidirectionalStreamEntry{callback: callback} 24 | bidirectionalStreamAccess.Unlock() 25 | return BidirectionalStream{ptr} 26 | } 27 | 28 | // Destroy destroys stream object. The destroy operation is asynchronous - 29 | // callbacks may still be invoked after this returns. The stream is marked 30 | // as destroyed and callbacks will silently return. 31 | func (s BidirectionalStream) Destroy() bool { 32 | bidirectionalStreamAccess.RLock() 33 | entry := bidirectionalStreamMap[s.ptr] 34 | bidirectionalStreamAccess.RUnlock() 35 | if entry != nil { 36 | entry.destroyed.Store(true) 37 | } 38 | return cronet.BidirectionalStreamDestroy(s.ptr) == 0 39 | } 40 | 41 | // Start starts the stream by sending request to |url| using |method| and |headers|. 42 | func (c BidirectionalStream) Start(method string, url string, headers map[string]string, priority int, endOfStream bool) bool { 43 | var headerArrayPtr uintptr 44 | var cStringBacking [][]byte // Keep C string backing slices alive 45 | 46 | headerLen := len(headers) 47 | if headerLen > 0 { 48 | // Allocate header structs 49 | headerStructs := make([]bidirectionalStreamHeader, headerLen) 50 | var index int 51 | for key, value := range headers { 52 | keyPtr, keyBacking := cronet.CString(key) 53 | valuePtr, valueBacking := cronet.CString(value) 54 | cStringBacking = append(cStringBacking, keyBacking, valueBacking) 55 | headerStructs[index].key = keyPtr 56 | headerStructs[index].value = valuePtr 57 | index++ 58 | } 59 | 60 | headerArray := bidirectionalStreamHeaderArray{ 61 | count: uintptr(headerLen), 62 | capacity: uintptr(headerLen), 63 | headers: uintptr(unsafe.Pointer(&headerStructs[0])), 64 | } 65 | headerArrayPtr = uintptr(unsafe.Pointer(&headerArray)) 66 | 67 | result := cronet.BidirectionalStreamStart(c.ptr, url, int32(priority), method, headerArrayPtr, endOfStream) == 0 68 | 69 | // Keep all backing slices alive until after the call 70 | runtime.KeepAlive(cStringBacking) 71 | runtime.KeepAlive(headerStructs) 72 | runtime.KeepAlive(headerArray) 73 | 74 | return result 75 | } 76 | 77 | return cronet.BidirectionalStreamStart(c.ptr, url, int32(priority), method, 0, endOfStream) == 0 78 | } 79 | 80 | func (s BidirectionalStream) DisableAutoFlush(disable bool) { 81 | cronet.BidirectionalStreamDisableAutoFlush(s.ptr, disable) 82 | } 83 | 84 | func (s BidirectionalStream) DelayRequestHeadersUntilFlush(delay bool) { 85 | cronet.BidirectionalStreamDelayRequestHeadersUntilFlush(s.ptr, delay) 86 | } 87 | 88 | func (s BidirectionalStream) Read(buffer []byte) int { 89 | if len(buffer) == 0 { 90 | return int(cronet.BidirectionalStreamRead(s.ptr, 0, 0)) 91 | } 92 | return int(cronet.BidirectionalStreamRead(s.ptr, uintptr(unsafe.Pointer(&buffer[0])), int32(len(buffer)))) 93 | } 94 | 95 | func (s BidirectionalStream) Write(buffer []byte, endOfStream bool) int { 96 | if len(buffer) == 0 { 97 | return int(cronet.BidirectionalStreamWrite(s.ptr, 0, 0, endOfStream)) 98 | } 99 | return int(cronet.BidirectionalStreamWrite(s.ptr, uintptr(unsafe.Pointer(&buffer[0])), int32(len(buffer)), endOfStream)) 100 | } 101 | 102 | func (s BidirectionalStream) Flush() { 103 | cronet.BidirectionalStreamFlush(s.ptr) 104 | } 105 | 106 | func (s BidirectionalStream) Cancel() { 107 | cronet.BidirectionalStreamCancel(s.ptr) 108 | } 109 | -------------------------------------------------------------------------------- /socket_fd_windows_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package cronet 4 | 5 | import ( 6 | "io" 7 | "net" 8 | "testing" 9 | "time" 10 | "unsafe" 11 | 12 | "golang.org/x/sys/windows" 13 | ) 14 | 15 | var ( 16 | modws2_32 = windows.NewLazySystemDLL("ws2_32.dll") 17 | 18 | procSend = modws2_32.NewProc("send") 19 | procRecv = modws2_32.NewProc("recv") 20 | ) 21 | 22 | func winsockSend(socket windows.Handle, buf []byte) (int, error) { 23 | r1, _, err := procSend.Call( 24 | uintptr(socket), 25 | uintptr(unsafe.Pointer(&buf[0])), 26 | uintptr(len(buf)), 27 | 0, 28 | ) 29 | n := int(r1) 30 | if n == -1 { 31 | return 0, err 32 | } 33 | return n, nil 34 | } 35 | 36 | func winsockRecv(socket windows.Handle, buf []byte) (int, error) { 37 | r1, _, err := procRecv.Call( 38 | uintptr(socket), 39 | uintptr(unsafe.Pointer(&buf[0])), 40 | uintptr(len(buf)), 41 | 0, 42 | ) 43 | n := int(r1) 44 | if n == -1 { 45 | return 0, err 46 | } 47 | return n, nil 48 | } 49 | 50 | func TestDupSocketFD(t *testing.T) { 51 | // Create a local TCP server 52 | listener, err := net.Listen("tcp", "127.0.0.1:0") 53 | if err != nil { 54 | t.Fatal(err) 55 | } 56 | defer listener.Close() 57 | 58 | serverDone := make(chan struct{}) 59 | go func() { 60 | defer close(serverDone) 61 | conn, err := listener.Accept() 62 | if err != nil { 63 | return 64 | } 65 | defer conn.Close() 66 | io.Copy(conn, conn) // echo server 67 | }() 68 | 69 | // Dial the server 70 | conn, err := net.Dial("tcp", listener.Addr().String()) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | // Extract and duplicate the handle 76 | tcpConn := conn.(*net.TCPConn) 77 | fd, err := dupSocketFD(tcpConn) 78 | if err != nil { 79 | conn.Close() 80 | t.Fatal(err) 81 | } 82 | 83 | // Close the original connection 84 | conn.Close() 85 | 86 | // Verify the duplicated handle is valid 87 | if fd < 0 { 88 | t.Fatal("invalid handle returned") 89 | } 90 | 91 | // Clean up 92 | _ = windows.Closesocket(windows.Handle(fd)) 93 | } 94 | 95 | func TestCreateSocketPair(t *testing.T) { 96 | fd, conn, err := createSocketPair() 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | defer windows.Closesocket(windows.Handle(fd)) 101 | defer conn.Close() 102 | 103 | if fd < 0 { 104 | t.Fatal("invalid handle returned") 105 | } 106 | 107 | // Test bidirectional communication: fd (socket handle) -> conn 108 | testData := []byte("hello from fd side") 109 | done := make(chan error, 1) 110 | go func() { 111 | _, err := winsockSend(windows.Handle(fd), testData) 112 | done <- err 113 | }() 114 | 115 | buf := make([]byte, len(testData)) 116 | conn.SetReadDeadline(time.Now().Add(5 * time.Second)) 117 | _, err = io.ReadFull(conn, buf) 118 | if err != nil { 119 | t.Fatalf("failed to read from conn: %v", err) 120 | } 121 | if string(buf) != string(testData) { 122 | t.Errorf("data mismatch: got %q, want %q", buf, testData) 123 | } 124 | 125 | if err := <-done; err != nil { 126 | t.Fatalf("failed to write to fd: %v", err) 127 | } 128 | 129 | // Test bidirectional communication: conn -> fd 130 | testData2 := []byte("hello from conn side") 131 | go func() { 132 | conn.Write(testData2) 133 | }() 134 | 135 | buf2 := make([]byte, len(testData2)) 136 | n, err := winsockRecv(windows.Handle(fd), buf2) 137 | if err != nil { 138 | t.Fatalf("failed to read from fd: %v", err) 139 | } 140 | if n != len(testData2) || string(buf2[:n]) != string(testData2) { 141 | t.Errorf("data mismatch: got %q, want %q", buf2[:n], testData2) 142 | } 143 | } 144 | 145 | func TestCreateSocketPair_MultipleCreation(t *testing.T) { 146 | // Test that we can create multiple socket pairs 147 | pairs := make([]struct { 148 | fd int 149 | conn net.Conn 150 | }, 5) 151 | 152 | for i := range pairs { 153 | fd, conn, err := createSocketPair() 154 | if err != nil { 155 | t.Fatalf("failed to create socket pair %d: %v", i, err) 156 | } 157 | pairs[i].fd = fd 158 | pairs[i].conn = conn 159 | } 160 | 161 | // Clean up 162 | for _, pair := range pairs { 163 | _ = windows.Closesocket(windows.Handle(pair.fd)) 164 | pair.conn.Close() 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /result.go: -------------------------------------------------------------------------------- 1 | package cronet 2 | 3 | // Result is runtime result code returned by Engine and URLRequest. Equivalent to 4 | // runtime exceptions in Android Java API. All results except SUCCESS trigger 5 | // native crash (via SIGABRT triggered by CHECK failure) unless 6 | // EngineParams.EnableCheckResult is set to false. 7 | type Result int 8 | 9 | const ( 10 | // ResultSuccess Operation completed successfully 11 | ResultSuccess Result = 0 12 | 13 | // ResultIllegalArgument Illegal argument 14 | ResultIllegalArgument Result = -100 15 | 16 | // ResultIllegalArgumentStoragePathMustExist Storage path must be set to existing directory 17 | ResultIllegalArgumentStoragePathMustExist Result = -101 18 | 19 | // ResultIllegalArgumentInvalidPin Public key pin is invalid 20 | ResultIllegalArgumentInvalidPin Result = -102 21 | 22 | // ResultIllegalArgumentInvalidHostname Host name is invalid 23 | ResultIllegalArgumentInvalidHostname Result = -103 24 | 25 | // ResultIllegalArgumentInvalidHttpMethod Invalid http method 26 | ResultIllegalArgumentInvalidHttpMethod Result = -104 27 | 28 | // ResultIllegalArgumentInvalidHttpHeader Invalid http header 29 | ResultIllegalArgumentInvalidHttpHeader Result = -105 30 | 31 | // ResultIllegalState Illegal state 32 | ResultIllegalState Result = -200 33 | 34 | // ResultIllegalStateStoragePathInUse Storage path is used by another engine 35 | ResultIllegalStateStoragePathInUse Result = -201 36 | 37 | // ResultIllegalStateCannotShutdownEngineFromNetworkThread Cannot shutdown engine from network thread 38 | ResultIllegalStateCannotShutdownEngineFromNetworkThread Result = -202 39 | 40 | // ResultIllegalStateEngineAlreadyStarted The engine has already started 41 | ResultIllegalStateEngineAlreadyStarted Result = -203 42 | 43 | // ResultIllegalStateRequestAlreadyStarted The request has already started 44 | ResultIllegalStateRequestAlreadyStarted Result = -204 45 | 46 | // ResultIllegalStateRequestNotInitialized The request is not initialized 47 | ResultIllegalStateRequestNotInitialized Result = -205 48 | 49 | // ResultIllegalStateRequestAlreadyInitialized The request is already initialized 50 | ResultIllegalStateRequestAlreadyInitialized Result = -206 51 | 52 | // ResultIllegalStateRequestNotStarted The request is not started 53 | ResultIllegalStateRequestNotStarted Result = -207 54 | 55 | // ResultIllegalStateUnexpectedRedirect No redirect to follow 56 | ResultIllegalStateUnexpectedRedirect Result = -208 57 | 58 | // ResultIllegalStateUnexpectedRead Unexpected read attempt 59 | ResultIllegalStateUnexpectedRead Result = -209 60 | 61 | // ResultIllegalStateReadFailed Unexpected read failure 62 | ResultIllegalStateReadFailed Result = -210 63 | 64 | // ResultNullPointer Null pointer or empty data 65 | ResultNullPointer Result = -300 66 | 67 | // ResultNullPointerHostname The hostname cannot be null 68 | ResultNullPointerHostname Result = -301 69 | 70 | // ResultNullPointerSha256Pins The set of SHA256 pins cannot be null 71 | ResultNullPointerSha256Pins Result = -302 72 | 73 | // ResultNullPointerExpirationDate The pin expiration date cannot be null 74 | ResultNullPointerExpirationDate Result = -303 75 | 76 | // ResultNullPointerEngine Engine is required 77 | ResultNullPointerEngine Result = -304 78 | 79 | // ResultNullPointerURL URL is required 80 | ResultNullPointerURL Result = -305 81 | 82 | // ResultNullPointerCallback Callback is required 83 | ResultNullPointerCallback Result = -306 84 | 85 | // ResultNullPointerExecutor Executor is required 86 | ResultNullPointerExecutor Result = -307 87 | 88 | // ResultNullPointerMethod Method is required 89 | ResultNullPointerMethod Result = -308 90 | 91 | // ResultNullPointerHeaderName Invalid header name 92 | ResultNullPointerHeaderName Result = -309 93 | 94 | // ResultNullPointerHeaderValue Invalid header value 95 | ResultNullPointerHeaderValue Result = -310 96 | 97 | // ResultNullPointerParams Params is required 98 | ResultNullPointerParams Result = -311 99 | 100 | // ResultNullPointerRequestFinishedInfoListenerExecutor Executor for RequestFinishedInfoListener is required 101 | ResultNullPointerRequestFinishedInfoListenerExecutor Result = -312 102 | ) 103 | -------------------------------------------------------------------------------- /test/quic_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | "time" 10 | 11 | cronet "github.com/sagernet/cronet-go" 12 | M "github.com/sagernet/sing/common/metadata" 13 | 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | const naiveQUICServerPort = 10002 18 | 19 | func startNaiveQUICServer(t *testing.T, certPem, keyPem string) { 20 | binary := ensureNaiveServer(t) 21 | 22 | configTemplate, err := os.ReadFile("config/sing-box-quic.json") 23 | require.NoError(t, err) 24 | 25 | certPem = filepath.ToSlash(certPem) 26 | keyPem = filepath.ToSlash(keyPem) 27 | 28 | config := strings.ReplaceAll(string(configTemplate), "/cert.pem", certPem) 29 | config = strings.ReplaceAll(config, "/key.pem", keyPem) 30 | 31 | configPath := filepath.Join(t.TempDir(), "sing-box-quic.json") 32 | err = os.WriteFile(configPath, []byte(config), 0o644) 33 | require.NoError(t, err) 34 | 35 | startNaiveServerWithConfig(t, binary, configPath) 36 | } 37 | 38 | // TestNaiveQUIC verifies NaiveClient connectivity with QUIC protocol. 39 | func TestNaiveQUIC(t *testing.T) { 40 | caPem, certPem, keyPem := generateCertificate(t, "example.org") 41 | caPemContent, err := os.ReadFile(caPem) 42 | require.NoError(t, err) 43 | 44 | startNaiveQUICServer(t, certPem, keyPem) 45 | time.Sleep(time.Second) 46 | 47 | client, err := cronet.NewNaiveClient(cronet.NaiveClientConfig{ 48 | ServerAddress: M.ParseSocksaddrHostPort("127.0.0.1", naiveQUICServerPort), 49 | ServerName: "example.org", 50 | Username: "test", 51 | Password: "test", 52 | TrustedRootCertificates: string(caPemContent), 53 | DNSResolver: localhostDNSResolver(t), 54 | QUIC: true, 55 | }) 56 | require.NoError(t, err) 57 | require.NoError(t, client.Start()) 58 | t.Cleanup(func() { client.Close() }) 59 | 60 | startEchoServer(t, 18200) 61 | 62 | conn, err := client.DialEarly(M.ParseSocksaddrHostPort("127.0.0.1", 18200)) 63 | require.NoError(t, err) 64 | defer conn.Close() 65 | 66 | testData := []byte("Hello, NaiveProxy QUIC!") 67 | _, err = conn.Write(testData) 68 | require.NoError(t, err) 69 | 70 | buf := make([]byte, len(testData)) 71 | _, err = io.ReadFull(conn, buf) 72 | require.NoError(t, err) 73 | require.Equal(t, testData, buf) 74 | } 75 | 76 | // TestNaiveQUICLargeTransfer tests data integrity with large transfers over QUIC. 77 | func TestNaiveQUICLargeTransfer(t *testing.T) { 78 | caPem, certPem, keyPem := generateCertificate(t, "example.org") 79 | caPemContent, err := os.ReadFile(caPem) 80 | require.NoError(t, err) 81 | 82 | startNaiveQUICServer(t, certPem, keyPem) 83 | time.Sleep(time.Second) 84 | 85 | client, err := cronet.NewNaiveClient(cronet.NaiveClientConfig{ 86 | ServerAddress: M.ParseSocksaddrHostPort("127.0.0.1", naiveQUICServerPort), 87 | ServerName: "example.org", 88 | Username: "test", 89 | Password: "test", 90 | TrustedRootCertificates: string(caPemContent), 91 | DNSResolver: localhostDNSResolver(t), 92 | QUIC: true, 93 | }) 94 | require.NoError(t, err) 95 | require.NoError(t, client.Start()) 96 | t.Cleanup(func() { client.Close() }) 97 | 98 | startEchoServer(t, 18201) 99 | 100 | conn, err := client.DialEarly(M.ParseSocksaddrHostPort("127.0.0.1", 18201)) 101 | require.NoError(t, err) 102 | defer conn.Close() 103 | 104 | // Generate 256KB of test data 105 | const dataSize = 256 * 1024 106 | testData := make([]byte, dataSize) 107 | for i := range testData { 108 | testData[i] = byte(i % 256) 109 | } 110 | 111 | // Write in background 112 | writeDone := make(chan error, 1) 113 | go func() { 114 | _, err := conn.Write(testData) 115 | writeDone <- err 116 | }() 117 | 118 | // Read all data back 119 | receivedData := make([]byte, dataSize) 120 | _, err = io.ReadFull(conn, receivedData) 121 | require.NoError(t, err) 122 | 123 | // Wait for write to complete 124 | require.NoError(t, <-writeDone) 125 | 126 | // Verify data integrity 127 | require.Equal(t, testData, receivedData, "data mismatch in large transfer over QUIC") 128 | } 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cronet-go 2 | 3 | [![Reference](https://pkg.go.dev/badge/github.com/sagernet/cronet-go.svg)](https://pkg.go.dev/github.com/sagernet/cronet-go) 4 | 5 | Go bindings for [naiveproxy](https://github.com/klzgrad/naiveproxy). 6 | 7 | ## Supported Platforms 8 | 9 | | Target | OS | CPU | 10 | |---------------|---------|-------| 11 | | android/386 | android | x86 | 12 | | android/amd64 | android | x64 | 13 | | android/arm | android | arm | 14 | | android/arm64 | android | arm64 | 15 | | darwin/amd64 | mac | x64 | 16 | | darwin/arm64 | mac | arm64 | 17 | | ios/arm64 | ios | arm64 | 18 | | ios/amd64 | ios | amd64 | 19 | | linux/386 | linux | x86 | 20 | | linux/amd64 | linux | x64 | 21 | | linux/arm | linux | arm | 22 | | linux/arm64 | linux | arm64 | 23 | | windows/amd64 | win | x64 | 24 | | windows/arm64 | win | arm64 | 25 | 26 | ## System Requirements 27 | 28 | | Platform | Minimum Version | 29 | |---------------|-----------------| 30 | | macOS | 12.0 (Monterey) | 31 | | iOS/tvOS | 15.0 | 32 | | Windows | 10 | 33 | | Android | 5.0 (API 21) | 34 | | Linux (glibc) | glibc 2.31 | 35 | | Linux (musl) | any | 36 | 37 | ## Downstream Build Requirements 38 | 39 | | Platform | Requirements | Go Build Flags | 40 | |--------------------------------------|---------------------------------|-----------------------------------| 41 | | Linux (glibc) | Chromium toolchain | - | 42 | | Linux (musl) | Chromium toolchain | `-tags with_musl` | 43 | | macOS / iOS | macOS Xcode | - | 44 | | iOS simulator/ tvOS / tvOS simulator | macOS Xcode + SagerNet/gomobile | - | 45 | | Windows | - | `CGO_ENABLED=0 -tags with_purego` | 46 | | Android | Android NDK | - | 47 | 48 | ## Linux Build instructions 49 | 50 | ```bash 51 | git clone --recursive --depth=1 https://github.com/sagernet/cronet-go.git 52 | cd cronet-go 53 | go run ./cmd/build-naive --target=linux/amd64 download-toolchain 54 | #go run ./cmd/build-naive --target=linux/amd64 --libc=musl download-toolchain 55 | 56 | # Outputs CC, CXX, and CGO_LDFLAGS=-fuse-ld=lld 57 | export $(go run ./cmd/build-naive --target=linux/amd64 env) 58 | #export $(go run ./cmd/build-naive --target=linux/amd64 --libc=musl env) 59 | 60 | cd /path/to/your/project 61 | go build 62 | # go build -tags with_musl 63 | ``` 64 | 65 | ### Directories to cache 66 | 67 | ```yaml 68 | - cronet-go/naiveproxy/src/third_party/llvm-build/Release+Asserts/ 69 | - cronet-go/naiveproxy/src/out/sysroot-build/ 70 | ``` 71 | 72 | ## Windows / purego Build Instructions 73 | 74 | For Windows or pure Go builds (no CGO), you need to distribute the dynamic library alongside your binary. 75 | 76 | ### Download Library 77 | 78 | Download `libcronet.dll` (Windows) or `libcronet.so` (Linux) from [GitHub Releases](https://github.com/sagernet/cronet-go/releases). 79 | 80 | ### Build with purego 81 | 82 | ```bash 83 | # Windows (purego is required) 84 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -tags with_purego -o myapp.exe 85 | 86 | # Linux with purego (optional, for dynamic linking) 87 | CGO_ENABLED=0 go build -tags with_purego -o myapp 88 | ``` 89 | 90 | ### Distribution 91 | 92 | Place the library file in the same directory as your executable: 93 | - Windows: `libcronet.dll` 94 | - Linux: `libcronet.so` 95 | 96 | ### For Downstream Developers 97 | 98 | If you need to programmatically extract libraries from Go module dependencies (e.g., for CI/CD pipelines): 99 | 100 | ```bash 101 | go run github.com/sagernet/cronet-go/cmd/build-naive@latest extract-lib --target windows/amd64 -n libcronet_amd64.dll 102 | go run github.com/sagernet/cronet-go/cmd/build-naive@latest extract-lib --target linux/amd64 -n libcronet_amd64.so 103 | ``` 104 | -------------------------------------------------------------------------------- /upload_data_provider_impl_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | // extern CRONET_EXPORT int64_t cronetUploadDataProviderGetLength(Cronet_UploadDataProviderPtr self); 9 | // extern CRONET_EXPORT void cronetUploadDataProviderRead(Cronet_UploadDataProviderPtr self, Cronet_UploadDataSinkPtr upload_data_sink, Cronet_BufferPtr buffer); 10 | // extern CRONET_EXPORT void cronetUploadDataProviderRewind(Cronet_UploadDataProviderPtr self, Cronet_UploadDataSinkPtr upload_data_sink); 11 | // extern CRONET_EXPORT void cronetUploadDataProviderClose(Cronet_UploadDataProviderPtr self); 12 | import "C" 13 | 14 | import ( 15 | "sync" 16 | "sync/atomic" 17 | "unsafe" 18 | ) 19 | 20 | type uploadDataProviderEntry struct { 21 | handler UploadDataProviderHandler 22 | destroyed atomic.Bool 23 | } 24 | 25 | var ( 26 | uploadDataAccess sync.RWMutex 27 | uploadDataProviderMap map[uintptr]*uploadDataProviderEntry 28 | ) 29 | 30 | func init() { 31 | uploadDataProviderMap = make(map[uintptr]*uploadDataProviderEntry) 32 | } 33 | 34 | func NewUploadDataProvider(handler UploadDataProviderHandler) UploadDataProvider { 35 | if handler == nil { 36 | panic("nil upload data provider handler") 37 | } 38 | ptr := C.Cronet_UploadDataProvider_CreateWith( 39 | (*[0]byte)(C.cronetUploadDataProviderGetLength), 40 | (*[0]byte)(C.cronetUploadDataProviderRead), 41 | (*[0]byte)(C.cronetUploadDataProviderRewind), 42 | (*[0]byte)(C.cronetUploadDataProviderClose), 43 | ) 44 | ptrVal := uintptr(unsafe.Pointer(ptr)) 45 | uploadDataAccess.Lock() 46 | uploadDataProviderMap[ptrVal] = &uploadDataProviderEntry{handler: handler} 47 | uploadDataAccess.Unlock() 48 | return UploadDataProvider{ptrVal} 49 | } 50 | 51 | func (p UploadDataProvider) Destroy() { 52 | uploadDataAccess.RLock() 53 | entry := uploadDataProviderMap[p.ptr] 54 | uploadDataAccess.RUnlock() 55 | if entry != nil { 56 | entry.destroyed.Store(true) 57 | } 58 | C.Cronet_UploadDataProvider_Destroy(C.Cronet_UploadDataProviderPtr(unsafe.Pointer(p.ptr))) 59 | } 60 | 61 | func instanceOfUploadDataProvider(self C.Cronet_UploadDataProviderPtr) UploadDataProviderHandler { 62 | uploadDataAccess.RLock() 63 | defer uploadDataAccess.RUnlock() 64 | entry := uploadDataProviderMap[uintptr(unsafe.Pointer(self))] 65 | if entry == nil || entry.destroyed.Load() { 66 | return nil 67 | } 68 | return entry.handler 69 | } 70 | 71 | //export cronetUploadDataProviderGetLength 72 | func cronetUploadDataProviderGetLength(self C.Cronet_UploadDataProviderPtr) C.int64_t { 73 | handler := instanceOfUploadDataProvider(self) 74 | if handler == nil { 75 | return 0 // Post-destroy callback, return 0 76 | } 77 | return C.int64_t(handler.Length(UploadDataProvider{uintptr(unsafe.Pointer(self))})) 78 | } 79 | 80 | //export cronetUploadDataProviderRead 81 | func cronetUploadDataProviderRead(self C.Cronet_UploadDataProviderPtr, sink C.Cronet_UploadDataSinkPtr, buffer C.Cronet_BufferPtr) { 82 | handler := instanceOfUploadDataProvider(self) 83 | if handler == nil { 84 | return // Post-destroy callback, silently ignore 85 | } 86 | handler.Read(UploadDataProvider{uintptr(unsafe.Pointer(self))}, UploadDataSink{uintptr(unsafe.Pointer(sink))}, Buffer{uintptr(unsafe.Pointer(buffer))}) 87 | } 88 | 89 | //export cronetUploadDataProviderRewind 90 | func cronetUploadDataProviderRewind(self C.Cronet_UploadDataProviderPtr, sink C.Cronet_UploadDataSinkPtr) { 91 | handler := instanceOfUploadDataProvider(self) 92 | if handler == nil { 93 | return // Post-destroy callback, silently ignore 94 | } 95 | handler.Rewind(UploadDataProvider{uintptr(unsafe.Pointer(self))}, UploadDataSink{uintptr(unsafe.Pointer(sink))}) 96 | } 97 | 98 | //export cronetUploadDataProviderClose 99 | func cronetUploadDataProviderClose(self C.Cronet_UploadDataProviderPtr) { 100 | ptr := uintptr(unsafe.Pointer(self)) 101 | handler := instanceOfUploadDataProvider(self) 102 | if handler == nil { 103 | return // Post-destroy callback, silently ignore 104 | } 105 | handler.Close(UploadDataProvider{ptr}) 106 | // Close is terminal callback - safe to cleanup 107 | uploadDataAccess.Lock() 108 | delete(uploadDataProviderMap, ptr) 109 | uploadDataAccess.Unlock() 110 | } 111 | -------------------------------------------------------------------------------- /url_request_params_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "unsafe" 7 | 8 | "github.com/sagernet/cronet-go/internal/cronet" 9 | ) 10 | 11 | func NewURLRequestParams() URLRequestParams { 12 | return URLRequestParams{cronet.UrlRequestParamsCreate()} 13 | } 14 | 15 | func (p URLRequestParams) Destroy() { 16 | cronet.UrlRequestParamsDestroy(p.ptr) 17 | } 18 | 19 | func (p URLRequestParams) SetMethod(method string) { 20 | cronet.UrlRequestParamsHttpMethodSet(p.ptr, method) 21 | } 22 | 23 | func (p URLRequestParams) Method() string { 24 | return cronet.UrlRequestParamsHttpMethodGet(p.ptr) 25 | } 26 | 27 | func (p URLRequestParams) AddHeader(header HTTPHeader) { 28 | cronet.UrlRequestParamsRequestHeadersAdd(p.ptr, header.ptr) 29 | } 30 | 31 | func (p URLRequestParams) HeaderSize() int { 32 | return int(cronet.UrlRequestParamsRequestHeadersSize(p.ptr)) 33 | } 34 | 35 | func (p URLRequestParams) HeaderAt(index int) HTTPHeader { 36 | return HTTPHeader{cronet.UrlRequestParamsRequestHeadersAt(p.ptr, uint32(index))} 37 | } 38 | 39 | func (p URLRequestParams) ClearHeaders() { 40 | cronet.UrlRequestParamsRequestHeadersClear(p.ptr) 41 | } 42 | 43 | func (p URLRequestParams) SetDisableCache(disable bool) { 44 | cronet.UrlRequestParamsDisableCacheSet(p.ptr, disable) 45 | } 46 | 47 | func (p URLRequestParams) DisableCache() bool { 48 | return cronet.UrlRequestParamsDisableCacheGet(p.ptr) 49 | } 50 | 51 | func (p URLRequestParams) SetPriority(priority URLRequestParamsRequestPriority) { 52 | cronet.UrlRequestParamsPrioritySet(p.ptr, int32(priority)) 53 | } 54 | 55 | func (p URLRequestParams) Priority() URLRequestParamsRequestPriority { 56 | return URLRequestParamsRequestPriority(cronet.UrlRequestParamsPriorityGet(p.ptr)) 57 | } 58 | 59 | func (p URLRequestParams) SetUploadDataProvider(provider UploadDataProvider) { 60 | cronet.UrlRequestParamsUploadDataProviderSet(p.ptr, provider.ptr) 61 | } 62 | 63 | func (p URLRequestParams) UploadDataProvider() UploadDataProvider { 64 | return UploadDataProvider{cronet.UrlRequestParamsUploadDataProviderGet(p.ptr)} 65 | } 66 | 67 | func (p URLRequestParams) SetUploadDataExecutor(executor Executor) { 68 | cronet.UrlRequestParamsUploadDataProviderExecutorSet(p.ptr, executor.ptr) 69 | } 70 | 71 | func (p URLRequestParams) UploadDataExecutor() Executor { 72 | return Executor{cronet.UrlRequestParamsUploadDataProviderExecutorGet(p.ptr)} 73 | } 74 | 75 | func (p URLRequestParams) SetAllowDirectExecutor(allow bool) { 76 | cronet.UrlRequestParamsAllowDirectExecutorSet(p.ptr, allow) 77 | } 78 | 79 | func (p URLRequestParams) AllocDirectExecutor() bool { 80 | return cronet.UrlRequestParamsAllowDirectExecutorGet(p.ptr) 81 | } 82 | 83 | func (p URLRequestParams) AddAnnotation(annotation unsafe.Pointer) { 84 | cronet.UrlRequestParamsAnnotationsAdd(p.ptr, uintptr(annotation)) 85 | } 86 | 87 | func (p URLRequestParams) AnnotationSize() int { 88 | return int(cronet.UrlRequestParamsAnnotationsSize(p.ptr)) 89 | } 90 | 91 | func (p URLRequestParams) AnnotationAt(index int) unsafe.Pointer { 92 | return unsafe.Pointer(cronet.UrlRequestParamsAnnotationsAt(p.ptr, uint32(index))) 93 | } 94 | 95 | func (p URLRequestParams) ClearAnnotations() { 96 | cronet.UrlRequestParamsAnnotationsClear(p.ptr) 97 | } 98 | 99 | func (p URLRequestParams) SetRequestFinishedListener(listener URLRequestFinishedInfoListener) { 100 | cronet.UrlRequestParamsRequestFinishedListenerSet(p.ptr, listener.ptr) 101 | } 102 | 103 | func (p URLRequestParams) RequestFinishedListener() URLRequestFinishedInfoListener { 104 | return URLRequestFinishedInfoListener{cronet.UrlRequestParamsRequestFinishedListenerGet(p.ptr)} 105 | } 106 | 107 | func (p URLRequestParams) SetRequestFinishedExecutor(executor Executor) { 108 | cronet.UrlRequestParamsRequestFinishedExecutorSet(p.ptr, executor.ptr) 109 | } 110 | 111 | func (p URLRequestParams) RequestFinishedExecutor() Executor { 112 | return Executor{cronet.UrlRequestParamsRequestFinishedExecutorGet(p.ptr)} 113 | } 114 | 115 | func (p URLRequestParams) SetIdempotency(idempotency URLRequestParamsIdempotency) { 116 | cronet.UrlRequestParamsIdempotencySet(p.ptr, int32(idempotency)) 117 | } 118 | 119 | func (p URLRequestParams) Idempotency() URLRequestParamsIdempotency { 120 | return URLRequestParamsIdempotency(cronet.UrlRequestParamsIdempotencyGet(p.ptr)) 121 | } 122 | -------------------------------------------------------------------------------- /engine_params_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "github.com/sagernet/cronet-go/internal/cronet" 7 | ) 8 | 9 | func NewEngineParams() EngineParams { 10 | return EngineParams{cronet.EngineParamsCreate()} 11 | } 12 | 13 | func (p EngineParams) Destroy() { 14 | cronet.EngineParamsDestroy(p.ptr) 15 | } 16 | 17 | func (p EngineParams) SetEnableCheckResult(enable bool) { 18 | cronet.EngineParamsEnableCheckResultSet(p.ptr, enable) 19 | } 20 | 21 | func (p EngineParams) EnableCheckResult() bool { 22 | return cronet.EngineParamsEnableCheckResultGet(p.ptr) 23 | } 24 | 25 | func (p EngineParams) SetUserAgent(userAgent string) { 26 | cronet.EngineParamsUserAgentSet(p.ptr, userAgent) 27 | } 28 | 29 | func (p EngineParams) UserAgent() string { 30 | return cronet.EngineParamsUserAgentGet(p.ptr) 31 | } 32 | 33 | func (p EngineParams) SetAcceptLanguage(acceptLanguage string) { 34 | cronet.EngineParamsAcceptLanguageSet(p.ptr, acceptLanguage) 35 | } 36 | 37 | func (p EngineParams) AcceptLanguage() string { 38 | return cronet.EngineParamsAcceptLanguageGet(p.ptr) 39 | } 40 | 41 | func (p EngineParams) SetStoragePath(storagePath string) { 42 | cronet.EngineParamsStoragePathSet(p.ptr, storagePath) 43 | } 44 | 45 | func (p EngineParams) StoragePath() string { 46 | return cronet.EngineParamsStoragePathGet(p.ptr) 47 | } 48 | 49 | func (p EngineParams) SetEnableQuic(enable bool) { 50 | cronet.EngineParamsEnableQuicSet(p.ptr, enable) 51 | } 52 | 53 | func (p EngineParams) EnableQuic() bool { 54 | return cronet.EngineParamsEnableQuicGet(p.ptr) 55 | } 56 | 57 | func (p EngineParams) SetEnableHTTP2(enable bool) { 58 | cronet.EngineParamsEnableHttp2Set(p.ptr, enable) 59 | } 60 | 61 | func (p EngineParams) EnableHTTP2() bool { 62 | return cronet.EngineParamsEnableHttp2Get(p.ptr) 63 | } 64 | 65 | func (p EngineParams) SetEnableBrotli(enable bool) { 66 | cronet.EngineParamsEnableBrotliSet(p.ptr, enable) 67 | } 68 | 69 | func (p EngineParams) EnableBrotli() bool { 70 | return cronet.EngineParamsEnableBrotliGet(p.ptr) 71 | } 72 | 73 | func (p EngineParams) SetHTTPCacheMode(mode EngineParamsHTTPCacheMode) { 74 | cronet.EngineParamsHttpCacheModeSet(p.ptr, int32(mode)) 75 | } 76 | 77 | func (p EngineParams) HTTPCacheMode() EngineParamsHTTPCacheMode { 78 | return EngineParamsHTTPCacheMode(cronet.EngineParamsHttpCacheModeGet(p.ptr)) 79 | } 80 | 81 | func (p EngineParams) SetHTTPCacheMaxSize(size int64) { 82 | cronet.EngineParamsHttpCacheMaxSizeSet(p.ptr, size) 83 | } 84 | 85 | func (p EngineParams) HTTPCacheMaxSize() int64 { 86 | return cronet.EngineParamsHttpCacheMaxSizeGet(p.ptr) 87 | } 88 | 89 | func (p EngineParams) AddQuicHint(hint QuicHint) { 90 | cronet.EngineParamsQuicHintsAdd(p.ptr, hint.ptr) 91 | } 92 | 93 | func (p EngineParams) QuicHintSize() int { 94 | return int(cronet.EngineParamsQuicHintsSize(p.ptr)) 95 | } 96 | 97 | func (p EngineParams) QuicHintAt(index int) QuicHint { 98 | return QuicHint{cronet.EngineParamsQuicHintsAt(p.ptr, uint32(index))} 99 | } 100 | 101 | func (p EngineParams) ClearQuicHints() { 102 | cronet.EngineParamsQuicHintsClear(p.ptr) 103 | } 104 | 105 | func (p EngineParams) AddPublicKeyPins(pins PublicKeyPins) { 106 | cronet.EngineParamsPublicKeyPinsAdd(p.ptr, pins.ptr) 107 | } 108 | 109 | func (p EngineParams) PublicKeyPinsSize() int { 110 | return int(cronet.EngineParamsPublicKeyPinsSize(p.ptr)) 111 | } 112 | 113 | func (p EngineParams) PublicKeyPinsAt(index int) PublicKeyPins { 114 | return PublicKeyPins{cronet.EngineParamsPublicKeyPinsAt(p.ptr, uint32(index))} 115 | } 116 | 117 | func (p EngineParams) ClearPublicKeyPins() { 118 | cronet.EngineParamsPublicKeyPinsClear(p.ptr) 119 | } 120 | 121 | func (p EngineParams) SetEnablePublicKeyPinningBypassForLocalTrustAnchors(enable bool) { 122 | cronet.EngineParamsEnablePublicKeyPinningBypassForLocalTrustAnchorsSet(p.ptr, enable) 123 | } 124 | 125 | func (p EngineParams) EnablePublicKeyPinningBypassForLocalTrustAnchors() bool { 126 | return cronet.EngineParamsEnablePublicKeyPinningBypassForLocalTrustAnchorsGet(p.ptr) 127 | } 128 | 129 | func (p EngineParams) SetNetworkThreadPriority(priority float64) { 130 | cronet.EngineParamsNetworkThreadPrioritySet(p.ptr, priority) 131 | } 132 | 133 | func (p EngineParams) NetworkThreadPriority() float64 { 134 | return cronet.EngineParamsNetworkThreadPriorityGet(p.ptr) 135 | } 136 | 137 | func (p EngineParams) SetExperimentalOptions(options string) { 138 | cronet.EngineParamsExperimentalOptionsSet(p.ptr, options) 139 | } 140 | 141 | func (p EngineParams) ExperimentalOptions() string { 142 | return cronet.EngineParamsExperimentalOptionsGet(p.ptr) 143 | } 144 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= 4 | github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 5 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 6 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 7 | github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= 8 | github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 11 | github.com/sagernet/sing v0.7.13 h1:XNYgd8e3cxMULs/LLJspdn/deHrnPWyrrglNHeCUAYM= 12 | github.com/sagernet/sing v0.7.13/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= 13 | github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= 14 | github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= 15 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 16 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 17 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 18 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 19 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 20 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 21 | golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= 22 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 23 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 24 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 25 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 26 | golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 27 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 28 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 29 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 30 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 31 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 32 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 33 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 34 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 36 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 37 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 38 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 40 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 41 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 42 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 43 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 44 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 45 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 46 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 47 | golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0= 48 | golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 49 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 50 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 51 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 52 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 53 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 54 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 55 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 56 | -------------------------------------------------------------------------------- /url_request_callback_impl_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego && !386 && !arm 2 | 3 | package cronet 4 | 5 | import ( 6 | "sync" 7 | "sync/atomic" 8 | 9 | "github.com/sagernet/cronet-go/internal/cronet" 10 | 11 | "github.com/ebitengine/purego" 12 | ) 13 | 14 | type urlRequestCallbackEntry struct { 15 | handler URLRequestCallbackHandler 16 | destroyed atomic.Bool 17 | } 18 | 19 | var ( 20 | urlRequestCallbackAccess sync.RWMutex 21 | urlRequestCallbackMap map[uintptr]*urlRequestCallbackEntry 22 | 23 | urlRequestCallbackOnRedirectReceived uintptr 24 | urlRequestCallbackOnResponseStarted uintptr 25 | urlRequestCallbackOnReadCompleted uintptr 26 | urlRequestCallbackOnSucceeded uintptr 27 | urlRequestCallbackOnFailed uintptr 28 | urlRequestCallbackOnCanceled uintptr 29 | ) 30 | 31 | func init() { 32 | urlRequestCallbackMap = make(map[uintptr]*urlRequestCallbackEntry) 33 | 34 | urlRequestCallbackOnRedirectReceived = purego.NewCallback(onRedirectReceivedCallback) 35 | urlRequestCallbackOnResponseStarted = purego.NewCallback(onResponseStartedCallback) 36 | urlRequestCallbackOnReadCompleted = purego.NewCallback(onReadCompletedCallback) 37 | urlRequestCallbackOnSucceeded = purego.NewCallback(onSucceededCallback) 38 | urlRequestCallbackOnFailed = purego.NewCallback(onFailedCallback) 39 | urlRequestCallbackOnCanceled = purego.NewCallback(onCanceledCallback) 40 | } 41 | 42 | func instanceOfURLRequestCallback(self uintptr) URLRequestCallbackHandler { 43 | urlRequestCallbackAccess.RLock() 44 | defer urlRequestCallbackAccess.RUnlock() 45 | entry := urlRequestCallbackMap[self] 46 | if entry == nil || entry.destroyed.Load() { 47 | return nil 48 | } 49 | return entry.handler 50 | } 51 | 52 | func onRedirectReceivedCallback(self, request, info, newLocationUrl uintptr) uintptr { 53 | handler := instanceOfURLRequestCallback(self) 54 | if handler == nil { 55 | return 0 // Post-destroy callback, silently ignore 56 | } 57 | handler.OnRedirectReceived( 58 | URLRequestCallback{self}, 59 | URLRequest{request}, 60 | URLResponseInfo{info}, 61 | cronet.GoString(newLocationUrl), 62 | ) 63 | return 0 64 | } 65 | 66 | func onResponseStartedCallback(self, request, info uintptr) uintptr { 67 | handler := instanceOfURLRequestCallback(self) 68 | if handler == nil { 69 | return 0 // Post-destroy callback, silently ignore 70 | } 71 | handler.OnResponseStarted( 72 | URLRequestCallback{self}, 73 | URLRequest{request}, 74 | URLResponseInfo{info}, 75 | ) 76 | return 0 77 | } 78 | 79 | func onReadCompletedCallback(self, request, info, buffer uintptr, bytesRead uint64) uintptr { 80 | handler := instanceOfURLRequestCallback(self) 81 | if handler == nil { 82 | return 0 // Post-destroy callback, silently ignore 83 | } 84 | handler.OnReadCompleted( 85 | URLRequestCallback{self}, 86 | URLRequest{request}, 87 | URLResponseInfo{info}, 88 | Buffer{buffer}, 89 | int64(bytesRead), 90 | ) 91 | return 0 92 | } 93 | 94 | func onSucceededCallback(self, request, info uintptr) uintptr { 95 | handler := instanceOfURLRequestCallback(self) 96 | if handler == nil { 97 | return 0 // Post-destroy callback, silently ignore 98 | } 99 | handler.OnSucceeded( 100 | URLRequestCallback{self}, 101 | URLRequest{request}, 102 | URLResponseInfo{info}, 103 | ) 104 | // Terminal callback - safe to cleanup 105 | cleanupURLRequestCallback(self) 106 | return 0 107 | } 108 | 109 | func onFailedCallback(self, request, info, err uintptr) uintptr { 110 | handler := instanceOfURLRequestCallback(self) 111 | if handler == nil { 112 | return 0 // Post-destroy callback, silently ignore 113 | } 114 | handler.OnFailed( 115 | URLRequestCallback{self}, 116 | URLRequest{request}, 117 | URLResponseInfo{info}, 118 | Error{err}, 119 | ) 120 | // Terminal callback - safe to cleanup 121 | cleanupURLRequestCallback(self) 122 | return 0 123 | } 124 | 125 | func onCanceledCallback(self, request, info uintptr) uintptr { 126 | handler := instanceOfURLRequestCallback(self) 127 | if handler == nil { 128 | return 0 // Post-destroy callback, silently ignore 129 | } 130 | handler.OnCanceled( 131 | URLRequestCallback{self}, 132 | URLRequest{request}, 133 | URLResponseInfo{info}, 134 | ) 135 | // Terminal callback - safe to cleanup 136 | cleanupURLRequestCallback(self) 137 | return 0 138 | } 139 | 140 | func cleanupURLRequestCallback(ptr uintptr) { 141 | urlRequestCallbackAccess.Lock() 142 | delete(urlRequestCallbackMap, ptr) 143 | urlRequestCallbackAccess.Unlock() 144 | } 145 | 146 | func NewURLRequestCallback(handler URLRequestCallbackHandler) URLRequestCallback { 147 | if handler == nil { 148 | panic("nil url request callback handler") 149 | } 150 | ptr := cronet.UrlRequestCallbackCreateWith( 151 | urlRequestCallbackOnRedirectReceived, 152 | urlRequestCallbackOnResponseStarted, 153 | urlRequestCallbackOnReadCompleted, 154 | urlRequestCallbackOnSucceeded, 155 | urlRequestCallbackOnFailed, 156 | urlRequestCallbackOnCanceled, 157 | ) 158 | urlRequestCallbackAccess.Lock() 159 | urlRequestCallbackMap[ptr] = &urlRequestCallbackEntry{handler: handler} 160 | urlRequestCallbackAccess.Unlock() 161 | return URLRequestCallback{ptr} 162 | } 163 | 164 | func (c URLRequestCallback) Destroy() { 165 | urlRequestCallbackAccess.RLock() 166 | entry := urlRequestCallbackMap[c.ptr] 167 | urlRequestCallbackAccess.RUnlock() 168 | if entry != nil { 169 | entry.destroyed.Store(true) 170 | } 171 | cronet.UrlRequestCallbackDestroy(c.ptr) 172 | } 173 | -------------------------------------------------------------------------------- /url_request_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | import "C" 9 | import "unsafe" 10 | 11 | func NewURLRequest() URLRequest { 12 | return URLRequest{uintptr(unsafe.Pointer(C.Cronet_UrlRequest_Create()))} 13 | } 14 | 15 | func (r URLRequest) Destroy() { 16 | C.Cronet_UrlRequest_Destroy(C.Cronet_UrlRequestPtr(unsafe.Pointer(r.ptr))) 17 | } 18 | 19 | // InitWithParams 20 | // Initialized URLRequest to |url| with |params|. All methods of |callback| for 21 | // request will be invoked on |executor|. The |executor| must not run tasks on 22 | // the thread calling Executor.Execute() to prevent blocking networking 23 | // operations and causing failure RESULTs during shutdown. 24 | // 25 | // @param engine Engine to process the request. 26 | // @param url URL for the request. 27 | // @param params additional parameters for the request, like headers and priority. 28 | // @param callback Callback that gets invoked on different events. 29 | // @param executor Executor on which all callbacks will be invoked. 30 | func (r URLRequest) InitWithParams(engine Engine, url string, params URLRequestParams, callback URLRequestCallback, executor Executor) Result { 31 | cURL := C.CString(url) 32 | defer C.free(unsafe.Pointer(cURL)) 33 | 34 | return Result(C.Cronet_UrlRequest_InitWithParams(C.Cronet_UrlRequestPtr(unsafe.Pointer(r.ptr)), C.Cronet_EnginePtr(unsafe.Pointer(engine.ptr)), cURL, C.Cronet_UrlRequestParamsPtr(unsafe.Pointer(params.ptr)), C.Cronet_UrlRequestCallbackPtr(unsafe.Pointer(callback.ptr)), C.Cronet_ExecutorPtr(unsafe.Pointer(executor.ptr)))) 35 | } 36 | 37 | // Start starts the request, all callbacks go to URLRequestCallbackHandler. May only be called 38 | // once. May not be called if Cancel() has been called. 39 | func (r URLRequest) Start() Result { 40 | return Result(C.Cronet_UrlRequest_Start(C.Cronet_UrlRequestPtr(unsafe.Pointer(r.ptr)))) 41 | } 42 | 43 | // FollowRedirect 44 | // Follows a pending redirect. Must only be called at most once for each 45 | // invocation of URLRequestCallbackHandler.OnRedirectReceived(). 46 | func (r URLRequest) FollowRedirect() Result { 47 | return Result(C.Cronet_UrlRequest_FollowRedirect(C.Cronet_UrlRequestPtr(unsafe.Pointer(r.ptr)))) 48 | } 49 | 50 | // Read 51 | // Attempts to read part of the response body into the provided buffer. 52 | // Must only be called at most once in response to each invocation of the 53 | // URLRequestCallbackHandler.OnResponseStarted() and 54 | // URLRequestCallbackHandler.OnReadCompleted()} methods of the URLRequestCallbackHandler. 55 | // Each call will result in an asynchronous call to 56 | // either the URLRequestCallbackHandler.OnReadCompleted() method if data 57 | // is read, its URLRequestCallbackHandler.OnSucceeded() method if 58 | // there's no more data to read, or its URLRequestCallbackHandler.OnFailed() 59 | // method if there's an error. 60 | // This method transfers ownership of |buffer| to Cronet, and app should 61 | // not access it until one of these callbacks is invoked. 62 | // 63 | // @param buffer to write response body to. The app must not read or 64 | // 65 | // modify buffer's position, limit, or data between its position and 66 | // limit until the request calls back into the URLRequestCallbackHandler. 67 | func (r URLRequest) Read(buffer Buffer) Result { 68 | return Result(C.Cronet_UrlRequest_Read(C.Cronet_UrlRequestPtr(unsafe.Pointer(r.ptr)), C.Cronet_BufferPtr(unsafe.Pointer(buffer.ptr)))) 69 | } 70 | 71 | // Cancel 72 | // cancels the request. Can be called at any time. 73 | // URLRequestCallbackHandler.OnCanceled() will be invoked when cancellation 74 | // is complete and no further callback methods will be invoked. If the 75 | // request has completed or has not started, calling Cancel() has no 76 | // effect and URLRequestCallbackHandler.OnCanceled() will not be invoked. If the 77 | // Executor passed in to UrlRequest.InitWithParams() runs 78 | // tasks on a single thread, and Cancel() is called on that thread, 79 | // no callback methods (besides URLRequestCallbackHandler.OnCanceled() will be invoked after 80 | // Cancel() is called. Otherwise, at most one callback method may be 81 | // invoked after Cancel() has completed. 82 | func (r URLRequest) Cancel() { 83 | C.Cronet_UrlRequest_Cancel(C.Cronet_UrlRequestPtr(unsafe.Pointer(r.ptr))) 84 | } 85 | 86 | // IsDone 87 | // Returns true if the request was successfully started and is now 88 | // finished (completed, canceled, or failed). 89 | func (r URLRequest) IsDone() bool { 90 | return bool(C.Cronet_UrlRequest_IsDone(C.Cronet_UrlRequestPtr(unsafe.Pointer(r.ptr)))) 91 | } 92 | 93 | // GetStatus 94 | // Queries the status of the request. 95 | // @param listener a URLRequestStatusListener that will be invoked with 96 | // 97 | // the request's current status. Listener will be invoked 98 | // back on the Executor passed in when the request was 99 | // created. 100 | func (r URLRequest) GetStatus(listener URLRequestStatusListener) { 101 | C.Cronet_UrlRequest_GetStatus(C.Cronet_UrlRequestPtr(unsafe.Pointer(r.ptr)), C.Cronet_UrlRequestStatusListenerPtr(unsafe.Pointer(listener.ptr))) 102 | } 103 | 104 | func (r URLRequest) SetClientContext(context unsafe.Pointer) { 105 | C.Cronet_UrlRequest_SetClientContext(C.Cronet_UrlRequestPtr(unsafe.Pointer(r.ptr)), C.Cronet_ClientContext(context)) 106 | } 107 | 108 | func (r URLRequest) ClientContext() unsafe.Pointer { 109 | return unsafe.Pointer(C.Cronet_UrlRequest_GetClientContext(C.Cronet_UrlRequestPtr(unsafe.Pointer(r.ptr)))) 110 | } 111 | -------------------------------------------------------------------------------- /bidirectional_stream_impl_purego.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego 2 | 3 | package cronet 4 | 5 | import ( 6 | "unsafe" 7 | 8 | "github.com/sagernet/cronet-go/internal/cronet" 9 | 10 | "github.com/ebitengine/purego" 11 | ) 12 | 13 | // bidirectionalStreamCallbackStruct mirrors the C struct bidirectional_stream_callback. 14 | // It contains 8 function pointers in the same order as the C struct. 15 | type bidirectionalStreamCallbackStruct struct { 16 | onStreamReady uintptr 17 | onResponseHeadersReceived uintptr 18 | onReadCompleted uintptr 19 | onWriteCompleted uintptr 20 | onResponseTrailersReceived uintptr 21 | onSucceeded uintptr 22 | onFailed uintptr 23 | onCanceled uintptr 24 | } 25 | 26 | // bidirectionalStreamHeaderArray mirrors the C struct bidirectional_stream_header_array 27 | type bidirectionalStreamHeaderArray struct { 28 | count uintptr 29 | capacity uintptr 30 | headers uintptr 31 | } 32 | 33 | // bidirectionalStreamHeader mirrors the C struct bidirectional_stream_header 34 | type bidirectionalStreamHeader struct { 35 | key uintptr // const char* 36 | value uintptr // const char* 37 | } 38 | 39 | var bsCallbackStructPurego bidirectionalStreamCallbackStruct 40 | 41 | func init() { 42 | bsCallbackStructPurego.onStreamReady = purego.NewCallback(bsOnStreamReadyCallback) 43 | bsCallbackStructPurego.onResponseHeadersReceived = purego.NewCallback(bsOnResponseHeadersReceivedCallback) 44 | bsCallbackStructPurego.onReadCompleted = purego.NewCallback(bsOnReadCompletedCallback) 45 | bsCallbackStructPurego.onWriteCompleted = purego.NewCallback(bsOnWriteCompletedCallback) 46 | bsCallbackStructPurego.onResponseTrailersReceived = purego.NewCallback(bsOnResponseTrailersReceivedCallback) 47 | bsCallbackStructPurego.onSucceeded = purego.NewCallback(bsOnSucceededCallback) 48 | bsCallbackStructPurego.onFailed = purego.NewCallback(bsOnFailedCallback) 49 | bsCallbackStructPurego.onCanceled = purego.NewCallback(bsOnCanceledCallback) 50 | } 51 | 52 | func bsOnStreamReadyCallback(stream uintptr) uintptr { 53 | cb := instanceOfBidirectionalStreamCallback(stream) 54 | if cb == nil { 55 | return 0 // Post-destroy callback, silently ignore 56 | } 57 | cb.OnStreamReady(BidirectionalStream{stream}) 58 | return 0 59 | } 60 | 61 | func bsOnResponseHeadersReceivedCallback(stream, headers, negotiatedProtocol uintptr) uintptr { 62 | cb := instanceOfBidirectionalStreamCallback(stream) 63 | if cb == nil { 64 | return 0 // Post-destroy callback, silently ignore 65 | } 66 | headerMap := parseHeaderArray(headers) 67 | cb.OnResponseHeadersReceived(BidirectionalStream{stream}, headerMap, cronet.GoString(negotiatedProtocol)) 68 | return 0 69 | } 70 | 71 | func bsOnReadCompletedCallback(stream, data uintptr, bytesRead int32) uintptr { 72 | cb := instanceOfBidirectionalStreamCallback(stream) 73 | if cb == nil { 74 | return 0 // Post-destroy callback, silently ignore 75 | } 76 | cb.OnReadCompleted(BidirectionalStream{stream}, int(bytesRead)) 77 | return 0 78 | } 79 | 80 | func bsOnWriteCompletedCallback(stream, data uintptr) uintptr { 81 | cb := instanceOfBidirectionalStreamCallback(stream) 82 | if cb == nil { 83 | return 0 // Post-destroy callback, silently ignore 84 | } 85 | cb.OnWriteCompleted(BidirectionalStream{stream}) 86 | return 0 87 | } 88 | 89 | func bsOnResponseTrailersReceivedCallback(stream, trailers uintptr) uintptr { 90 | cb := instanceOfBidirectionalStreamCallback(stream) 91 | if cb == nil { 92 | return 0 // Post-destroy callback, silently ignore 93 | } 94 | trailerMap := parseHeaderArray(trailers) 95 | cb.OnResponseTrailersReceived(BidirectionalStream{stream}, trailerMap) 96 | return 0 97 | } 98 | 99 | func bsOnSucceededCallback(stream uintptr) uintptr { 100 | cb := instanceOfBidirectionalStreamCallback(stream) 101 | if cb == nil { 102 | return 0 // Post-destroy callback, silently ignore 103 | } 104 | cb.OnSucceeded(BidirectionalStream{stream}) 105 | // Terminal callback - safe to cleanup 106 | cleanupBidirectionalStream(stream) 107 | return 0 108 | } 109 | 110 | func bsOnFailedCallback(stream uintptr, netError int32) uintptr { 111 | cb := instanceOfBidirectionalStreamCallback(stream) 112 | if cb == nil { 113 | return 0 // Post-destroy callback, silently ignore 114 | } 115 | cb.OnFailed(BidirectionalStream{stream}, int(netError)) 116 | // Terminal callback - safe to cleanup 117 | cleanupBidirectionalStream(stream) 118 | return 0 119 | } 120 | 121 | func bsOnCanceledCallback(stream uintptr) uintptr { 122 | cb := instanceOfBidirectionalStreamCallback(stream) 123 | if cb == nil { 124 | return 0 // Post-destroy callback, silently ignore 125 | } 126 | cb.OnCanceled(BidirectionalStream{stream}) 127 | // Terminal callback - safe to cleanup 128 | cleanupBidirectionalStream(stream) 129 | return 0 130 | } 131 | 132 | // parseHeaderArray parses the bidirectional_stream_header_array pointer into a Go map 133 | func parseHeaderArray(ptr uintptr) map[string]string { 134 | if ptr == 0 { 135 | return nil 136 | } 137 | arr := (*bidirectionalStreamHeaderArray)(unsafe.Pointer(ptr)) 138 | count := int(arr.count) 139 | if count == 0 { 140 | return nil 141 | } 142 | result := make(map[string]string, count) 143 | headers := unsafe.Slice((*bidirectionalStreamHeader)(unsafe.Pointer(arr.headers)), count) 144 | for _, h := range headers { 145 | key := cronet.GoString(h.key) 146 | if key == "" { 147 | continue 148 | } 149 | result[key] = cronet.GoString(h.value) 150 | } 151 | return result 152 | } 153 | -------------------------------------------------------------------------------- /url_request_impl_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | // extern CRONET_EXPORT Cronet_RESULT cronetUrlRequestInitWithParams(Cronet_UrlRequestPtr self, Cronet_EnginePtr engine, Cronet_String url, Cronet_UrlRequestParamsPtr params, Cronet_UrlRequestCallbackPtr callback, Cronet_ExecutorPtr executor); 9 | // extern CRONET_EXPORT Cronet_RESULT cronetUrlRequestStart(Cronet_UrlRequestPtr self); 10 | // extern CRONET_EXPORT Cronet_RESULT cronetUrlRequestFollowRedirect(Cronet_UrlRequestPtr self); 11 | // extern CRONET_EXPORT Cronet_RESULT cronetUrlRequestRead(Cronet_UrlRequestPtr self, Cronet_BufferPtr buffer); 12 | // extern CRONET_EXPORT void cronetUrlRequestCancel(Cronet_UrlRequestPtr self); 13 | // extern CRONET_EXPORT bool cronetUrlRequestIsDone(Cronet_UrlRequestPtr self); 14 | // extern CRONET_EXPORT void cronetUrlRequestGetStatus(Cronet_UrlRequestPtr self, Cronet_UrlRequestStatusListenerPtr listener); 15 | import "C" 16 | 17 | import ( 18 | "sync" 19 | "unsafe" 20 | ) 21 | 22 | // URLRequestHandler is an interface for custom URLRequest implementations (for testing/mocking). 23 | type URLRequestHandler interface { 24 | InitWithParams(self URLRequest, engine Engine, url string, params URLRequestParams, callback URLRequestCallback, executor Executor) Result 25 | Start(self URLRequest) Result 26 | FollowRedirect(self URLRequest) Result 27 | Read(self URLRequest, buffer Buffer) Result 28 | Cancel(self URLRequest) 29 | IsDone(self URLRequest) bool 30 | GetStatus(self URLRequest, listener URLRequestStatusListener) 31 | } 32 | 33 | // NewURLRequestWith creates a new URLRequest with custom handler (for testing/mocking). 34 | func NewURLRequestWith(handler URLRequestHandler) URLRequest { 35 | ptr := C.Cronet_UrlRequest_CreateWith( 36 | (*[0]byte)(C.cronetUrlRequestInitWithParams), 37 | (*[0]byte)(C.cronetUrlRequestStart), 38 | (*[0]byte)(C.cronetUrlRequestFollowRedirect), 39 | (*[0]byte)(C.cronetUrlRequestRead), 40 | (*[0]byte)(C.cronetUrlRequestCancel), 41 | (*[0]byte)(C.cronetUrlRequestIsDone), 42 | (*[0]byte)(C.cronetUrlRequestGetStatus), 43 | ) 44 | ptrVal := uintptr(unsafe.Pointer(ptr)) 45 | urlRequestHandlerAccess.Lock() 46 | urlRequestHandlerMap[ptrVal] = handler 47 | urlRequestHandlerAccess.Unlock() 48 | return URLRequest{ptrVal} 49 | } 50 | 51 | var ( 52 | urlRequestHandlerAccess sync.RWMutex 53 | urlRequestHandlerMap map[uintptr]URLRequestHandler 54 | ) 55 | 56 | func init() { 57 | urlRequestHandlerMap = make(map[uintptr]URLRequestHandler) 58 | } 59 | 60 | func instanceOfURLRequestHandler(self C.Cronet_UrlRequestPtr) URLRequestHandler { 61 | urlRequestHandlerAccess.RLock() 62 | defer urlRequestHandlerAccess.RUnlock() 63 | return urlRequestHandlerMap[uintptr(unsafe.Pointer(self))] 64 | } 65 | 66 | //export cronetUrlRequestInitWithParams 67 | func cronetUrlRequestInitWithParams(self C.Cronet_UrlRequestPtr, engine C.Cronet_EnginePtr, url C.Cronet_String, params C.Cronet_UrlRequestParamsPtr, callback C.Cronet_UrlRequestCallbackPtr, executor C.Cronet_ExecutorPtr) C.Cronet_RESULT { 68 | handler := instanceOfURLRequestHandler(self) 69 | if handler != nil { 70 | return C.Cronet_RESULT(handler.InitWithParams(URLRequest{uintptr(unsafe.Pointer(self))}, Engine{uintptr(unsafe.Pointer(engine))}, C.GoString(url), URLRequestParams{uintptr(unsafe.Pointer(params))}, URLRequestCallback{uintptr(unsafe.Pointer(callback))}, Executor{uintptr(unsafe.Pointer(executor))})) 71 | } 72 | return C.Cronet_RESULT_SUCCESS 73 | } 74 | 75 | //export cronetUrlRequestStart 76 | func cronetUrlRequestStart(self C.Cronet_UrlRequestPtr) C.Cronet_RESULT { 77 | handler := instanceOfURLRequestHandler(self) 78 | if handler != nil { 79 | return C.Cronet_RESULT(handler.Start(URLRequest{uintptr(unsafe.Pointer(self))})) 80 | } 81 | return C.Cronet_RESULT_SUCCESS 82 | } 83 | 84 | //export cronetUrlRequestFollowRedirect 85 | func cronetUrlRequestFollowRedirect(self C.Cronet_UrlRequestPtr) C.Cronet_RESULT { 86 | handler := instanceOfURLRequestHandler(self) 87 | if handler != nil { 88 | return C.Cronet_RESULT(handler.FollowRedirect(URLRequest{uintptr(unsafe.Pointer(self))})) 89 | } 90 | return C.Cronet_RESULT_SUCCESS 91 | } 92 | 93 | //export cronetUrlRequestRead 94 | func cronetUrlRequestRead(self C.Cronet_UrlRequestPtr, buffer C.Cronet_BufferPtr) C.Cronet_RESULT { 95 | handler := instanceOfURLRequestHandler(self) 96 | if handler != nil { 97 | return C.Cronet_RESULT(handler.Read(URLRequest{uintptr(unsafe.Pointer(self))}, Buffer{uintptr(unsafe.Pointer(buffer))})) 98 | } 99 | return C.Cronet_RESULT_SUCCESS 100 | } 101 | 102 | //export cronetUrlRequestCancel 103 | func cronetUrlRequestCancel(self C.Cronet_UrlRequestPtr) { 104 | handler := instanceOfURLRequestHandler(self) 105 | if handler != nil { 106 | handler.Cancel(URLRequest{uintptr(unsafe.Pointer(self))}) 107 | } 108 | } 109 | 110 | //export cronetUrlRequestIsDone 111 | func cronetUrlRequestIsDone(self C.Cronet_UrlRequestPtr) C.bool { 112 | handler := instanceOfURLRequestHandler(self) 113 | if handler != nil { 114 | return C.bool(handler.IsDone(URLRequest{uintptr(unsafe.Pointer(self))})) 115 | } 116 | return C.bool(false) 117 | } 118 | 119 | //export cronetUrlRequestGetStatus 120 | func cronetUrlRequestGetStatus(self C.Cronet_UrlRequestPtr, listener C.Cronet_UrlRequestStatusListenerPtr) { 121 | handler := instanceOfURLRequestHandler(self) 122 | if handler != nil { 123 | handler.GetStatus(URLRequest{uintptr(unsafe.Pointer(self))}, URLRequestStatusListener{uintptr(unsafe.Pointer(listener))}) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /url_request_callback_impl_purego_32bit.go: -------------------------------------------------------------------------------- 1 | //go:build with_purego && (386 || arm) 2 | 3 | package cronet 4 | 5 | import ( 6 | "sync" 7 | "sync/atomic" 8 | 9 | "github.com/sagernet/cronet-go/internal/cronet" 10 | 11 | "github.com/ebitengine/purego" 12 | ) 13 | 14 | type urlRequestCallbackEntry struct { 15 | handler URLRequestCallbackHandler 16 | destroyed atomic.Bool 17 | } 18 | 19 | var ( 20 | urlRequestCallbackAccess sync.RWMutex 21 | urlRequestCallbackMap map[uintptr]*urlRequestCallbackEntry 22 | 23 | urlRequestCallbackOnRedirectReceived uintptr 24 | urlRequestCallbackOnResponseStarted uintptr 25 | urlRequestCallbackOnReadCompleted uintptr 26 | urlRequestCallbackOnSucceeded uintptr 27 | urlRequestCallbackOnFailed uintptr 28 | urlRequestCallbackOnCanceled uintptr 29 | ) 30 | 31 | func init() { 32 | urlRequestCallbackMap = make(map[uintptr]*urlRequestCallbackEntry) 33 | 34 | urlRequestCallbackOnRedirectReceived = purego.NewCallback(onRedirectReceivedCallback) 35 | urlRequestCallbackOnResponseStarted = purego.NewCallback(onResponseStartedCallback) 36 | urlRequestCallbackOnReadCompleted = purego.NewCallback(onReadCompletedCallback) 37 | urlRequestCallbackOnSucceeded = purego.NewCallback(onSucceededCallback) 38 | urlRequestCallbackOnFailed = purego.NewCallback(onFailedCallback) 39 | urlRequestCallbackOnCanceled = purego.NewCallback(onCanceledCallback) 40 | } 41 | 42 | func instanceOfURLRequestCallback(self uintptr) URLRequestCallbackHandler { 43 | urlRequestCallbackAccess.RLock() 44 | defer urlRequestCallbackAccess.RUnlock() 45 | entry := urlRequestCallbackMap[self] 46 | if entry == nil || entry.destroyed.Load() { 47 | return nil 48 | } 49 | return entry.handler 50 | } 51 | 52 | func onRedirectReceivedCallback(self, request, info, newLocationUrl uintptr) uintptr { 53 | handler := instanceOfURLRequestCallback(self) 54 | if handler == nil { 55 | return 0 // Post-destroy callback, silently ignore 56 | } 57 | handler.OnRedirectReceived( 58 | URLRequestCallback{self}, 59 | URLRequest{request}, 60 | URLResponseInfo{info}, 61 | cronet.GoString(newLocationUrl), 62 | ) 63 | return 0 64 | } 65 | 66 | func onResponseStartedCallback(self, request, info uintptr) uintptr { 67 | handler := instanceOfURLRequestCallback(self) 68 | if handler == nil { 69 | return 0 // Post-destroy callback, silently ignore 70 | } 71 | handler.OnResponseStarted( 72 | URLRequestCallback{self}, 73 | URLRequest{request}, 74 | URLResponseInfo{info}, 75 | ) 76 | return 0 77 | } 78 | 79 | // On 32-bit platforms, uint64 is passed as two 32-bit values (low, high) on the stack 80 | func onReadCompletedCallback(self, request, info, buffer, bytesReadLow, bytesReadHigh uintptr) uintptr { 81 | handler := instanceOfURLRequestCallback(self) 82 | if handler == nil { 83 | return 0 // Post-destroy callback, silently ignore 84 | } 85 | bytesRead := uint64(bytesReadLow) | (uint64(bytesReadHigh) << 32) 86 | handler.OnReadCompleted( 87 | URLRequestCallback{self}, 88 | URLRequest{request}, 89 | URLResponseInfo{info}, 90 | Buffer{buffer}, 91 | int64(bytesRead), 92 | ) 93 | return 0 94 | } 95 | 96 | func onSucceededCallback(self, request, info uintptr) uintptr { 97 | handler := instanceOfURLRequestCallback(self) 98 | if handler == nil { 99 | return 0 // Post-destroy callback, silently ignore 100 | } 101 | handler.OnSucceeded( 102 | URLRequestCallback{self}, 103 | URLRequest{request}, 104 | URLResponseInfo{info}, 105 | ) 106 | // Terminal callback - safe to cleanup 107 | cleanupURLRequestCallback(self) 108 | return 0 109 | } 110 | 111 | func onFailedCallback(self, request, info, err uintptr) uintptr { 112 | handler := instanceOfURLRequestCallback(self) 113 | if handler == nil { 114 | return 0 // Post-destroy callback, silently ignore 115 | } 116 | handler.OnFailed( 117 | URLRequestCallback{self}, 118 | URLRequest{request}, 119 | URLResponseInfo{info}, 120 | Error{err}, 121 | ) 122 | // Terminal callback - safe to cleanup 123 | cleanupURLRequestCallback(self) 124 | return 0 125 | } 126 | 127 | func onCanceledCallback(self, request, info uintptr) uintptr { 128 | handler := instanceOfURLRequestCallback(self) 129 | if handler == nil { 130 | return 0 // Post-destroy callback, silently ignore 131 | } 132 | handler.OnCanceled( 133 | URLRequestCallback{self}, 134 | URLRequest{request}, 135 | URLResponseInfo{info}, 136 | ) 137 | // Terminal callback - safe to cleanup 138 | cleanupURLRequestCallback(self) 139 | return 0 140 | } 141 | 142 | func cleanupURLRequestCallback(ptr uintptr) { 143 | urlRequestCallbackAccess.Lock() 144 | delete(urlRequestCallbackMap, ptr) 145 | urlRequestCallbackAccess.Unlock() 146 | } 147 | 148 | func NewURLRequestCallback(handler URLRequestCallbackHandler) URLRequestCallback { 149 | if handler == nil { 150 | panic("nil url request callback handler") 151 | } 152 | ptr := cronet.UrlRequestCallbackCreateWith( 153 | urlRequestCallbackOnRedirectReceived, 154 | urlRequestCallbackOnResponseStarted, 155 | urlRequestCallbackOnReadCompleted, 156 | urlRequestCallbackOnSucceeded, 157 | urlRequestCallbackOnFailed, 158 | urlRequestCallbackOnCanceled, 159 | ) 160 | urlRequestCallbackAccess.Lock() 161 | urlRequestCallbackMap[ptr] = &urlRequestCallbackEntry{handler: handler} 162 | urlRequestCallbackAccess.Unlock() 163 | return URLRequestCallback{ptr} 164 | } 165 | 166 | func (c URLRequestCallback) Destroy() { 167 | urlRequestCallbackAccess.RLock() 168 | entry := urlRequestCallbackMap[c.ptr] 169 | urlRequestCallbackAccess.RUnlock() 170 | if entry != nil { 171 | entry.destroyed.Store(true) 172 | } 173 | cronet.UrlRequestCallbackDestroy(c.ptr) 174 | } 175 | -------------------------------------------------------------------------------- /dialer_test.go: -------------------------------------------------------------------------------- 1 | package cronet 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | ) 7 | 8 | func TestDialerMapCleanup(t *testing.T) { 9 | // Verify Engine.Destroy() properly cleans up dialerMap 10 | engine := NewEngine() 11 | 12 | engine.SetDialer(func(address string, port uint16) int { 13 | return -104 // ERR_CONNECTION_FAILED 14 | }) 15 | 16 | dialerAccess.RLock() 17 | _, exists := dialerMap[engine.ptr] 18 | dialerAccess.RUnlock() 19 | 20 | if !exists { 21 | t.Error("dialer not registered in dialerMap") 22 | } 23 | 24 | engine.Destroy() 25 | 26 | dialerAccess.RLock() 27 | _, exists = dialerMap[engine.ptr] 28 | dialerAccess.RUnlock() 29 | 30 | if exists { 31 | t.Error("dialer not cleaned up after Engine.Destroy()") 32 | } 33 | } 34 | 35 | func TestUDPDialerMapCleanup(t *testing.T) { 36 | engine := NewEngine() 37 | 38 | engine.SetUDPDialer(func(address string, port uint16) (int, string, uint16) { 39 | return -104, "", 0 // ERR_CONNECTION_FAILED 40 | }) 41 | 42 | udpDialerAccess.RLock() 43 | _, exists := udpDialerMap[engine.ptr] 44 | udpDialerAccess.RUnlock() 45 | 46 | if !exists { 47 | t.Error("dialer not registered in udpDialerMap") 48 | } 49 | 50 | engine.Destroy() 51 | 52 | udpDialerAccess.RLock() 53 | _, exists = udpDialerMap[engine.ptr] 54 | udpDialerAccess.RUnlock() 55 | 56 | if exists { 57 | t.Error("dialer not cleaned up after Engine.Destroy()") 58 | } 59 | } 60 | 61 | func TestSetDialerNil(t *testing.T) { 62 | engine := NewEngine() 63 | defer engine.Destroy() 64 | 65 | // First set a dialer 66 | engine.SetDialer(func(address string, port uint16) int { 67 | return -104 68 | }) 69 | 70 | dialerAccess.RLock() 71 | _, exists := dialerMap[engine.ptr] 72 | dialerAccess.RUnlock() 73 | 74 | if !exists { 75 | t.Error("dialer not registered") 76 | } 77 | 78 | // Then set it to nil 79 | engine.SetDialer(nil) 80 | 81 | dialerAccess.RLock() 82 | _, exists = dialerMap[engine.ptr] 83 | dialerAccess.RUnlock() 84 | 85 | if exists { 86 | t.Error("dialer not removed after SetDialer(nil)") 87 | } 88 | } 89 | 90 | func TestSetUDPDialerNil(t *testing.T) { 91 | engine := NewEngine() 92 | defer engine.Destroy() 93 | 94 | engine.SetUDPDialer(func(address string, port uint16) (int, string, uint16) { 95 | return -104, "", 0 96 | }) 97 | 98 | udpDialerAccess.RLock() 99 | _, exists := udpDialerMap[engine.ptr] 100 | udpDialerAccess.RUnlock() 101 | 102 | if !exists { 103 | t.Error("dialer not registered") 104 | } 105 | 106 | engine.SetUDPDialer(nil) 107 | 108 | udpDialerAccess.RLock() 109 | _, exists = udpDialerMap[engine.ptr] 110 | udpDialerAccess.RUnlock() 111 | 112 | if exists { 113 | t.Error("dialer not removed after SetUDPDialer(nil)") 114 | } 115 | } 116 | 117 | func TestSetDialerOverwrite(t *testing.T) { 118 | engine := NewEngine() 119 | defer engine.Destroy() 120 | 121 | callCount1 := 0 122 | callCount2 := 0 123 | 124 | // Set first dialer 125 | engine.SetDialer(func(address string, port uint16) int { 126 | callCount1++ 127 | return -104 128 | }) 129 | 130 | // Overwrite with second dialer 131 | engine.SetDialer(func(address string, port uint16) int { 132 | callCount2++ 133 | return -102 134 | }) 135 | 136 | // Verify only one entry in map 137 | dialerAccess.RLock() 138 | count := 0 139 | for k := range dialerMap { 140 | if k == engine.ptr { 141 | count++ 142 | } 143 | } 144 | dialerAccess.RUnlock() 145 | 146 | if count != 1 { 147 | t.Errorf("expected 1 entry in dialerMap, got %d", count) 148 | } 149 | } 150 | 151 | func TestDialerConcurrentAccess(t *testing.T) { 152 | engine := NewEngine() 153 | defer engine.Destroy() 154 | 155 | var wg sync.WaitGroup 156 | iterations := 100 157 | 158 | // Concurrent SetDialer calls (writers) 159 | for i := 0; i < iterations; i++ { 160 | wg.Add(1) 161 | go func(n int) { 162 | defer wg.Done() 163 | if n%2 == 0 { 164 | engine.SetDialer(func(address string, port uint16) int { 165 | return -104 166 | }) 167 | } else { 168 | engine.SetDialer(nil) 169 | } 170 | }(i) 171 | } 172 | 173 | // Concurrent dialerMap reads (simulating callback access) 174 | for i := 0; i < iterations; i++ { 175 | wg.Add(1) 176 | go func() { 177 | defer wg.Done() 178 | dialerAccess.RLock() 179 | _ = dialerMap[engine.ptr] 180 | dialerAccess.RUnlock() 181 | }() 182 | } 183 | 184 | wg.Wait() 185 | 186 | // Verify final state consistency: at most 1 entry for this engine 187 | dialerAccess.RLock() 188 | count := 0 189 | for k := range dialerMap { 190 | if k == engine.ptr { 191 | count++ 192 | } 193 | } 194 | dialerAccess.RUnlock() 195 | 196 | if count > 1 { 197 | t.Errorf("dialerMap has duplicate entries for engine: %d", count) 198 | } 199 | } 200 | 201 | func TestMultipleEnginesDialers(t *testing.T) { 202 | engine1 := NewEngine() 203 | engine2 := NewEngine() 204 | 205 | engine1.SetDialer(func(address string, port uint16) int { 206 | return -104 207 | }) 208 | 209 | engine2.SetDialer(func(address string, port uint16) int { 210 | return -102 211 | }) 212 | 213 | // Verify both dialers are registered 214 | dialerAccess.RLock() 215 | _, exists1 := dialerMap[engine1.ptr] 216 | _, exists2 := dialerMap[engine2.ptr] 217 | dialerAccess.RUnlock() 218 | 219 | if !exists1 || !exists2 { 220 | t.Error("both dialers should be registered") 221 | } 222 | 223 | // Destroy engine1, verify engine2's dialer still exists 224 | engine1.Destroy() 225 | 226 | dialerAccess.RLock() 227 | _, exists1 = dialerMap[engine1.ptr] 228 | _, exists2 = dialerMap[engine2.ptr] 229 | dialerAccess.RUnlock() 230 | 231 | if exists1 { 232 | t.Error("engine1's dialer should be removed") 233 | } 234 | if !exists2 { 235 | t.Error("engine2's dialer should still exist") 236 | } 237 | 238 | engine2.Destroy() 239 | } 240 | -------------------------------------------------------------------------------- /engine_impl_cgo.go: -------------------------------------------------------------------------------- 1 | //go:build !with_purego 2 | 3 | package cronet 4 | 5 | // #include 6 | // #include 7 | // #include 8 | // extern CRONET_EXPORT Cronet_RESULT cronetEngineStartWithParams(Cronet_EnginePtr self, Cronet_EngineParamsPtr params); 9 | // extern CRONET_EXPORT bool cronetEngineStartNetLogToFile(Cronet_EnginePtr self, Cronet_String file_name, bool log_all); 10 | // extern CRONET_EXPORT void cronetEngineStopNetLog(Cronet_EnginePtr self); 11 | // extern CRONET_EXPORT Cronet_RESULT cronetEngineShutdown(Cronet_EnginePtr self); 12 | // extern CRONET_EXPORT Cronet_String cronetEngineGetVersionString(Cronet_EnginePtr self); 13 | // extern CRONET_EXPORT Cronet_String cronetEngineGetDefaultUserAgent(Cronet_EnginePtr self); 14 | // extern CRONET_EXPORT void cronetEngineAddRequestFinishedListener(Cronet_EnginePtr self, Cronet_RequestFinishedInfoListenerPtr listener, Cronet_ExecutorPtr executor); 15 | // extern CRONET_EXPORT void cronetEngineRemoveRequestFinishedListener(Cronet_EnginePtr self, Cronet_RequestFinishedInfoListenerPtr listener); 16 | import "C" 17 | 18 | import ( 19 | "sync" 20 | "unsafe" 21 | ) 22 | 23 | // EngineHandler is an interface for custom Engine implementations (for testing/mocking). 24 | type EngineHandler interface { 25 | StartWithParams(self Engine, params EngineParams) Result 26 | StartNetLogToFile(self Engine, fileName string, logAll bool) bool 27 | StopNetLog(self Engine) 28 | Shutdown(self Engine) Result 29 | GetVersionString(self Engine) string 30 | GetDefaultUserAgent(self Engine) string 31 | AddRequestFinishedListener(self Engine, listener URLRequestFinishedInfoListener, executor Executor) 32 | RemoveRequestFinishedListener(self Engine, listener URLRequestFinishedInfoListener) 33 | } 34 | 35 | // NewEngineWith creates a new Engine with custom handler (for testing/mocking). 36 | func NewEngineWith(handler EngineHandler) Engine { 37 | ptr := C.Cronet_Engine_CreateWith( 38 | (*[0]byte)(C.cronetEngineStartWithParams), 39 | (*[0]byte)(C.cronetEngineStartNetLogToFile), 40 | (*[0]byte)(C.cronetEngineStopNetLog), 41 | (*[0]byte)(C.cronetEngineShutdown), 42 | (*[0]byte)(C.cronetEngineGetVersionString), 43 | (*[0]byte)(C.cronetEngineGetDefaultUserAgent), 44 | (*[0]byte)(C.cronetEngineAddRequestFinishedListener), 45 | (*[0]byte)(C.cronetEngineRemoveRequestFinishedListener), 46 | ) 47 | ptrVal := uintptr(unsafe.Pointer(ptr)) 48 | engineHandlerAccess.Lock() 49 | engineHandlerMap[ptrVal] = handler 50 | engineHandlerAccess.Unlock() 51 | return Engine{ptrVal} 52 | } 53 | 54 | var ( 55 | engineHandlerAccess sync.RWMutex 56 | engineHandlerMap map[uintptr]EngineHandler 57 | ) 58 | 59 | func init() { 60 | engineHandlerMap = make(map[uintptr]EngineHandler) 61 | } 62 | 63 | func instanceOfEngineHandler(self C.Cronet_EnginePtr) EngineHandler { 64 | engineHandlerAccess.RLock() 65 | defer engineHandlerAccess.RUnlock() 66 | return engineHandlerMap[uintptr(unsafe.Pointer(self))] 67 | } 68 | 69 | //export cronetEngineStartWithParams 70 | func cronetEngineStartWithParams(self C.Cronet_EnginePtr, params C.Cronet_EngineParamsPtr) C.Cronet_RESULT { 71 | handler := instanceOfEngineHandler(self) 72 | if handler != nil { 73 | return C.Cronet_RESULT(handler.StartWithParams(Engine{uintptr(unsafe.Pointer(self))}, EngineParams{uintptr(unsafe.Pointer(params))})) 74 | } 75 | return C.Cronet_RESULT_SUCCESS 76 | } 77 | 78 | //export cronetEngineStartNetLogToFile 79 | func cronetEngineStartNetLogToFile(self C.Cronet_EnginePtr, fileName C.Cronet_String, logAll C.bool) C.bool { 80 | handler := instanceOfEngineHandler(self) 81 | if handler != nil { 82 | return C.bool(handler.StartNetLogToFile(Engine{uintptr(unsafe.Pointer(self))}, C.GoString(fileName), bool(logAll))) 83 | } 84 | return C.bool(false) 85 | } 86 | 87 | //export cronetEngineStopNetLog 88 | func cronetEngineStopNetLog(self C.Cronet_EnginePtr) { 89 | handler := instanceOfEngineHandler(self) 90 | if handler != nil { 91 | handler.StopNetLog(Engine{uintptr(unsafe.Pointer(self))}) 92 | } 93 | } 94 | 95 | //export cronetEngineShutdown 96 | func cronetEngineShutdown(self C.Cronet_EnginePtr) C.Cronet_RESULT { 97 | handler := instanceOfEngineHandler(self) 98 | if handler != nil { 99 | return C.Cronet_RESULT(handler.Shutdown(Engine{uintptr(unsafe.Pointer(self))})) 100 | } 101 | return C.Cronet_RESULT_SUCCESS 102 | } 103 | 104 | //export cronetEngineGetVersionString 105 | func cronetEngineGetVersionString(self C.Cronet_EnginePtr) C.Cronet_String { 106 | handler := instanceOfEngineHandler(self) 107 | if handler != nil { 108 | return C.CString(handler.GetVersionString(Engine{uintptr(unsafe.Pointer(self))})) 109 | } 110 | return nil 111 | } 112 | 113 | //export cronetEngineGetDefaultUserAgent 114 | func cronetEngineGetDefaultUserAgent(self C.Cronet_EnginePtr) C.Cronet_String { 115 | handler := instanceOfEngineHandler(self) 116 | if handler != nil { 117 | return C.CString(handler.GetDefaultUserAgent(Engine{uintptr(unsafe.Pointer(self))})) 118 | } 119 | return nil 120 | } 121 | 122 | //export cronetEngineAddRequestFinishedListener 123 | func cronetEngineAddRequestFinishedListener(self C.Cronet_EnginePtr, listener C.Cronet_RequestFinishedInfoListenerPtr, executor C.Cronet_ExecutorPtr) { 124 | handler := instanceOfEngineHandler(self) 125 | if handler != nil { 126 | handler.AddRequestFinishedListener(Engine{uintptr(unsafe.Pointer(self))}, URLRequestFinishedInfoListener{uintptr(unsafe.Pointer(listener))}, Executor{uintptr(unsafe.Pointer(executor))}) 127 | } 128 | } 129 | 130 | //export cronetEngineRemoveRequestFinishedListener 131 | func cronetEngineRemoveRequestFinishedListener(self C.Cronet_EnginePtr, listener C.Cronet_RequestFinishedInfoListenerPtr) { 132 | handler := instanceOfEngineHandler(self) 133 | if handler != nil { 134 | handler.RemoveRequestFinishedListener(Engine{uintptr(unsafe.Pointer(self))}, URLRequestFinishedInfoListener{uintptr(unsafe.Pointer(listener))}) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /dns_socketpair_windows_test.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package cronet 4 | 5 | import ( 6 | "bytes" 7 | "encoding/binary" 8 | "testing" 9 | 10 | "golang.org/x/sys/windows" 11 | ) 12 | 13 | func TestCreatePacketSocketPair(t *testing.T) { 14 | fd, conn, err := createPacketSocketPair(false) 15 | if err != nil { 16 | t.Fatalf("createPacketSocketPair failed: %v", err) 17 | } 18 | defer windows.Closesocket(windows.Handle(fd)) 19 | defer conn.Close() 20 | 21 | if fd <= 0 { 22 | t.Errorf("expected valid fd, got %d", fd) 23 | } 24 | } 25 | 26 | func TestFramedPacketConn_ReadWrite(t *testing.T) { 27 | fd, conn, err := createPacketSocketPair(false) 28 | if err != nil { 29 | t.Fatalf("createPacketSocketPair failed: %v", err) 30 | } 31 | defer windows.Closesocket(windows.Handle(fd)) 32 | defer conn.Close() 33 | 34 | // Test write from conn to fd 35 | testData := []byte("framed message test") 36 | _, err = conn.WriteTo(testData, nil) 37 | if err != nil { 38 | t.Fatalf("WriteTo failed: %v", err) 39 | } 40 | 41 | // Read from fd side (need to manually parse length prefix) 42 | buf := make([]byte, 1024) 43 | var flags uint32 44 | var bytesReceived uint32 45 | wsaBuf := windows.WSABuf{Len: uint32(len(buf)), Buf: &buf[0]} 46 | err = windows.WSARecv(windows.Handle(fd), &wsaBuf, 1, &bytesReceived, &flags, nil, nil) 47 | if err != nil { 48 | t.Fatalf("WSARecv failed: %v", err) 49 | } 50 | 51 | if bytesReceived < 2+uint32(len(testData)) { 52 | t.Fatalf("expected at least %d bytes, got %d", 2+len(testData), bytesReceived) 53 | } 54 | 55 | length := binary.BigEndian.Uint16(buf[:2]) 56 | if length != uint16(len(testData)) { 57 | t.Errorf("expected length %d, got %d", len(testData), length) 58 | } 59 | if !bytes.Equal(buf[2:2+length], testData) { 60 | t.Errorf("expected %q, got %q", testData, buf[2:2+length]) 61 | } 62 | } 63 | 64 | func TestFramedPacketConn_BidirectionalCommunication(t *testing.T) { 65 | fd, conn, err := createPacketSocketPair(false) 66 | if err != nil { 67 | t.Fatalf("createPacketSocketPair failed: %v", err) 68 | } 69 | defer windows.Closesocket(windows.Handle(fd)) 70 | defer conn.Close() 71 | 72 | // fd → conn: send with length prefix 73 | testData := []byte("hello from fd") 74 | frame := make([]byte, 2+len(testData)) 75 | binary.BigEndian.PutUint16(frame[:2], uint16(len(testData))) 76 | copy(frame[2:], testData) 77 | 78 | wsaBuf := windows.WSABuf{Len: uint32(len(frame)), Buf: &frame[0]} 79 | var bytesSent uint32 80 | err = windows.WSASend(windows.Handle(fd), &wsaBuf, 1, &bytesSent, 0, nil, nil) 81 | if err != nil { 82 | t.Fatalf("WSASend failed: %v", err) 83 | } 84 | 85 | // Read from conn 86 | buf := make([]byte, 1024) 87 | n, _, err := conn.ReadFrom(buf) 88 | if err != nil { 89 | t.Fatalf("ReadFrom failed: %v", err) 90 | } 91 | if !bytes.Equal(buf[:n], testData) { 92 | t.Errorf("expected %q, got %q", testData, buf[:n]) 93 | } 94 | 95 | // conn → fd 96 | testData2 := []byte("hello from conn") 97 | _, err = conn.WriteTo(testData2, nil) 98 | if err != nil { 99 | t.Fatalf("WriteTo failed: %v", err) 100 | } 101 | 102 | // Read from fd with length prefix 103 | var flags uint32 104 | var bytesReceived uint32 105 | buf2 := make([]byte, 1024) 106 | wsaBuf2 := windows.WSABuf{Len: uint32(len(buf2)), Buf: &buf2[0]} 107 | err = windows.WSARecv(windows.Handle(fd), &wsaBuf2, 1, &bytesReceived, &flags, nil, nil) 108 | if err != nil { 109 | t.Fatalf("WSARecv failed: %v", err) 110 | } 111 | 112 | length := binary.BigEndian.Uint16(buf2[:2]) 113 | if !bytes.Equal(buf2[2:2+length], testData2) { 114 | t.Errorf("expected %q, got %q", testData2, buf2[2:2+length]) 115 | } 116 | } 117 | 118 | func TestFramedPacketConn_MessageBoundary(t *testing.T) { 119 | fd, conn, err := createPacketSocketPair(false) 120 | if err != nil { 121 | t.Fatalf("createPacketSocketPair failed: %v", err) 122 | } 123 | defer windows.Closesocket(windows.Handle(fd)) 124 | defer conn.Close() 125 | 126 | // Send multiple messages 127 | messages := [][]byte{ 128 | []byte("msg1"), 129 | []byte("longer message 2"), 130 | []byte("m3"), 131 | } 132 | 133 | for _, msg := range messages { 134 | _, err = conn.WriteTo(msg, nil) 135 | if err != nil { 136 | t.Fatalf("WriteTo failed: %v", err) 137 | } 138 | } 139 | 140 | // Read from fd side and verify boundaries 141 | for i, expected := range messages { 142 | // Read length prefix 143 | lengthBuf := make([]byte, 2) 144 | var flags uint32 145 | var bytesReceived uint32 146 | wsaBuf := windows.WSABuf{Len: 2, Buf: &lengthBuf[0]} 147 | err = windows.WSARecv(windows.Handle(fd), &wsaBuf, 1, &bytesReceived, &flags, nil, nil) 148 | if err != nil { 149 | t.Fatalf("WSARecv length %d failed: %v", i, err) 150 | } 151 | 152 | length := binary.BigEndian.Uint16(lengthBuf) 153 | if length != uint16(len(expected)) { 154 | t.Errorf("message %d: expected length %d, got %d", i, len(expected), length) 155 | } 156 | 157 | // Read payload 158 | payload := make([]byte, length) 159 | wsaBuf2 := windows.WSABuf{Len: uint32(length), Buf: &payload[0]} 160 | err = windows.WSARecv(windows.Handle(fd), &wsaBuf2, 1, &bytesReceived, &flags, nil, nil) 161 | if err != nil { 162 | t.Fatalf("WSARecv payload %d failed: %v", i, err) 163 | } 164 | if !bytes.Equal(payload, expected) { 165 | t.Errorf("message %d: expected %q, got %q", i, expected, payload) 166 | } 167 | } 168 | } 169 | 170 | func TestFramedPacketConn_MaxSize(t *testing.T) { 171 | fd, conn, err := createPacketSocketPair(false) 172 | if err != nil { 173 | t.Fatalf("createPacketSocketPair failed: %v", err) 174 | } 175 | defer windows.Closesocket(windows.Handle(fd)) 176 | defer conn.Close() 177 | 178 | // Test packet larger than 65535 should fail 179 | hugePacket := make([]byte, 65536) 180 | _, err = conn.WriteTo(hugePacket, nil) 181 | if err == nil { 182 | t.Error("expected error for packet larger than 65535, got nil") 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /dns_socketpair_unix_test.go: -------------------------------------------------------------------------------- 1 | //go:build unix 2 | 3 | package cronet 4 | 5 | import ( 6 | "syscall" 7 | "testing" 8 | ) 9 | 10 | func TestCreatePacketSocketPair(t *testing.T) { 11 | fd, conn, err := createPacketSocketPair(false) 12 | if err != nil { 13 | t.Fatalf("createPacketSocketPair failed: %v", err) 14 | } 15 | defer syscall.Close(fd) 16 | defer conn.Close() 17 | 18 | if fd <= 0 { 19 | t.Errorf("expected valid fd, got %d", fd) 20 | } 21 | } 22 | 23 | func TestCreatePacketSocketPair_BidirectionalCommunication(t *testing.T) { 24 | fd, conn, err := createPacketSocketPair(false) 25 | if err != nil { 26 | t.Fatalf("createPacketSocketPair failed: %v", err) 27 | } 28 | defer syscall.Close(fd) 29 | defer conn.Close() 30 | 31 | // fd → conn (datagram boundary preserved) 32 | testData := []byte("hello from fd") 33 | _, err = syscall.Write(fd, testData) 34 | if err != nil { 35 | t.Fatalf("write to fd failed: %v", err) 36 | } 37 | 38 | buf := make([]byte, 1024) 39 | n, _, err := conn.ReadFrom(buf) 40 | if err != nil { 41 | t.Fatalf("read from conn failed: %v", err) 42 | } 43 | if string(buf[:n]) != string(testData) { 44 | t.Errorf("expected %q, got %q", testData, buf[:n]) 45 | } 46 | 47 | // conn → fd: For Unix socketpairs, use Write() via net.Conn interface 48 | // (same as serveDNSPacketConn does when remoteAddress is nil) 49 | streamConn, ok := conn.(interface{ Write([]byte) (int, error) }) 50 | if !ok { 51 | t.Fatal("PacketConn should implement Write for socketpair") 52 | } 53 | testData2 := []byte("hello from conn") 54 | _, err = streamConn.Write(testData2) 55 | if err != nil { 56 | t.Fatalf("write to conn failed: %v", err) 57 | } 58 | 59 | n, err = syscall.Read(fd, buf) 60 | if err != nil { 61 | t.Fatalf("read from fd failed: %v", err) 62 | } 63 | if string(buf[:n]) != string(testData2) { 64 | t.Errorf("expected %q, got %q", testData2, buf[:n]) 65 | } 66 | } 67 | 68 | func TestCreatePacketSocketPair_MessageBoundary(t *testing.T) { 69 | fd, conn, err := createPacketSocketPair(false) 70 | if err != nil { 71 | t.Fatalf("createPacketSocketPair failed: %v", err) 72 | } 73 | defer syscall.Close(fd) 74 | defer conn.Close() 75 | 76 | // Send multiple different-sized messages 77 | messages := [][]byte{ 78 | []byte("short"), 79 | make([]byte, 512), 80 | make([]byte, 1400), 81 | } 82 | // Fill with recognizable patterns 83 | for i := range messages[1] { 84 | messages[1][i] = byte(i % 256) 85 | } 86 | for i := range messages[2] { 87 | messages[2][i] = byte((i * 7) % 256) 88 | } 89 | 90 | for _, msg := range messages { 91 | _, err = syscall.Write(fd, msg) 92 | if err != nil { 93 | t.Fatalf("write failed: %v", err) 94 | } 95 | } 96 | 97 | // Verify each message boundary is preserved 98 | for i, expected := range messages { 99 | buf := make([]byte, 2048) 100 | n, _, err := conn.ReadFrom(buf) 101 | if err != nil { 102 | t.Fatalf("read %d failed: %v", i, err) 103 | } 104 | if n != len(expected) { 105 | t.Errorf("message %d: expected length %d, got %d", i, len(expected), n) 106 | } 107 | if string(buf[:n]) != string(expected) { 108 | t.Errorf("message %d: content mismatch", i) 109 | } 110 | } 111 | } 112 | 113 | func TestCreateUDPLoopbackPair(t *testing.T) { 114 | fd, conn, err := createUDPLoopbackPair() 115 | if err != nil { 116 | t.Fatalf("createUDPLoopbackPair failed: %v", err) 117 | } 118 | defer syscall.Close(fd) 119 | defer conn.Close() 120 | 121 | if fd <= 0 { 122 | t.Errorf("expected valid fd, got %d", fd) 123 | } 124 | } 125 | 126 | func TestCreateUDPLoopbackPair_BidirectionalCommunication(t *testing.T) { 127 | fd, conn, err := createUDPLoopbackPair() 128 | if err != nil { 129 | t.Fatalf("createUDPLoopbackPair failed: %v", err) 130 | } 131 | defer syscall.Close(fd) 132 | defer conn.Close() 133 | 134 | // Set fd to blocking mode (it may be non-blocking after dup) 135 | if err := syscall.SetNonblock(fd, false); err != nil { 136 | t.Fatalf("failed to set blocking mode: %v", err) 137 | } 138 | 139 | // fd → conn 140 | testData := []byte("hello from fd via UDP loopback") 141 | _, err = syscall.Write(fd, testData) 142 | if err != nil { 143 | t.Fatalf("write to fd failed: %v", err) 144 | } 145 | 146 | buf := make([]byte, 1024) 147 | n, remoteAddr, err := conn.ReadFrom(buf) 148 | if err != nil { 149 | t.Fatalf("read from conn failed: %v", err) 150 | } 151 | if string(buf[:n]) != string(testData) { 152 | t.Errorf("expected %q, got %q", testData, buf[:n]) 153 | } 154 | if remoteAddr == nil { 155 | t.Error("expected non-nil remote address for UDP loopback") 156 | } 157 | 158 | // conn → fd: Use Write (both sockets are connected to each other) 159 | streamConn, ok := conn.(interface{ Write([]byte) (int, error) }) 160 | if !ok { 161 | t.Fatal("connected UDP socket should implement Write") 162 | } 163 | testData2 := []byte("hello from conn via UDP loopback") 164 | _, err = streamConn.Write(testData2) 165 | if err != nil { 166 | t.Fatalf("write to conn failed: %v", err) 167 | } 168 | 169 | n, err = syscall.Read(fd, buf) 170 | if err != nil { 171 | t.Fatalf("read from fd failed: %v", err) 172 | } 173 | if string(buf[:n]) != string(testData2) { 174 | t.Errorf("expected %q, got %q", testData2, buf[:n]) 175 | } 176 | } 177 | 178 | func TestCreatePacketSocketPair_ForceUDPLoopback(t *testing.T) { 179 | // Test that forceUDPLoopback=true returns UDP loopback pair 180 | fd, conn, err := createPacketSocketPair(true) 181 | if err != nil { 182 | t.Fatalf("createPacketSocketPair(true) failed: %v", err) 183 | } 184 | defer syscall.Close(fd) 185 | defer conn.Close() 186 | 187 | // Verify it's a UDP connection by checking that ReadFrom returns a remote address 188 | testData := []byte("test UDP loopback via forceUDPLoopback") 189 | _, err = syscall.Write(fd, testData) 190 | if err != nil { 191 | t.Fatalf("write to fd failed: %v", err) 192 | } 193 | 194 | buf := make([]byte, 1024) 195 | n, remoteAddr, err := conn.ReadFrom(buf) 196 | if err != nil { 197 | t.Fatalf("read from conn failed: %v", err) 198 | } 199 | if string(buf[:n]) != string(testData) { 200 | t.Errorf("expected %q, got %q", testData, buf[:n]) 201 | } 202 | // UDP loopback returns non-nil remote address, Unix socketpair returns nil 203 | if remoteAddr == nil { 204 | t.Error("forceUDPLoopback=true should return UDP loopback (non-nil remote address)") 205 | } 206 | } 207 | --------------------------------------------------------------------------------