diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7ecf0b..7c8e7df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,16 @@ jobs: go-version: stable - name: Test - run: go test -v ./... + if: ${{ matrix.os != 'windows-11-arm' }} + run: | + go test -race -v ./... + go test -race -v -tags no_spoof_tls_fingerprint ./... + # -race is not supported on windows/arm64. + - name: Test + if: ${{ matrix.os == 'windows-11-arm' }} + run: | + go test -v ./... + go test -v -tags no_spoof_tls_fingerprint ./... - name: Build run: go build -trimpath -ldflags=-s . diff --git a/go.mod b/go.mod index 8c18e35..1cf876d 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,8 @@ require ( github.com/ayn2op/arikawa/v3 v3.0.0-20260708075022-9cc760fe507e github.com/ayn2op/ningen/v3 v3.0.1-0.20260708075321-0ba1ffc70bd6 github.com/ayn2op/tview v0.0.0-20260803165347-d8123f4ec53c + github.com/bogdanfinn/fhttp v0.6.8 + github.com/bogdanfinn/tls-client v1.15.1 github.com/gdamore/tcell/v3 v3.4.1 github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 @@ -28,6 +30,11 @@ require ( require ( github.com/akavel/rsrc v0.10.2 // indirect + github.com/bdandy/go-errors v1.2.2 // indirect + github.com/bdandy/go-socks4 v1.2.3 // indirect + github.com/bogdanfinn/quic-go-utls v1.0.9-utls // indirect + github.com/bogdanfinn/utls v1.7.7-barnius // indirect + github.com/bogdanfinn/websocket v1.5.5-barnius // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/danieljoos/wincred v1.2.3 // indirect @@ -40,13 +47,17 @@ require ( github.com/josephspurrier/goversioninfo v1.7.0 // indirect github.com/lucasb-eyer/go-colorful v1.4.1 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/quic-go/qpack v0.6.0 // indirect github.com/randall77/makefat v0.0.0-20260406194835-1b91746796b7 // indirect + github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect github.com/twmb/murmur3 v1.1.8 // indirect go4.org v0.0.0-20260112195520-a5071408f32f // indirect golang.design/x/x11 v0.2.0 // indirect + golang.org/x/crypto v0.54.0 // indirect golang.org/x/exp/shiny v0.0.0-20260727155853-b88d891fe743 // indirect golang.org/x/image v0.44.0 // indirect golang.org/x/mobile v0.0.0-20260730202154-c700fe717e6e // indirect + golang.org/x/net v0.57.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/term v0.45.0 // indirect golang.org/x/text v0.40.0 // indirect diff --git a/go.sum b/go.sum index 5ec9b51..e51d353 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,20 @@ github.com/ayn2op/ningen/v3 v3.0.1-0.20260708075321-0ba1ffc70bd6 h1:/khHm6G+GWwX github.com/ayn2op/ningen/v3 v3.0.1-0.20260708075321-0ba1ffc70bd6/go.mod h1:vqLIvETLkjc2vIgQzVEBqpGNCrgNe7YJqS9TUAJsXRI= github.com/ayn2op/tview v0.0.0-20260803165347-d8123f4ec53c h1:ApOfdF/YBJ+oy5zG5yJW/IbgCOoWcfJ/qukTnfFlUXc= github.com/ayn2op/tview v0.0.0-20260803165347-d8123f4ec53c/go.mod h1:bJZdl/FfspPLEW3nuX6ubt7YbmDoSj7f2IoWH37w1NE= +github.com/bdandy/go-errors v1.2.2 h1:WdFv/oukjTJCLa79UfkGmwX7ZxONAihKu4V0mLIs11Q= +github.com/bdandy/go-errors v1.2.2/go.mod h1:NkYHl4Fey9oRRdbB1CoC6e84tuqQHiqrOcZpqFEkBxM= +github.com/bdandy/go-socks4 v1.2.3 h1:Q6Y2heY1GRjCtHbmlKfnwrKVU/k81LS8mRGLRlmDlic= +github.com/bdandy/go-socks4 v1.2.3/go.mod h1:98kiVFgpdogR8aIGLWLvjDVZ8XcKPsSI/ypGrO+bqHI= +github.com/bogdanfinn/fhttp v0.6.8 h1:LiQyHOY3i0QoxxNB7nq27/nGNNbtPj0fuBPozhR7Ws4= +github.com/bogdanfinn/fhttp v0.6.8/go.mod h1:A+EKDzMx2hb4IUbMx4TlkoHnaJEiLl8r/1Ss1Y+5e5M= +github.com/bogdanfinn/quic-go-utls v1.0.9-utls h1:tV6eDEiRbRCcepALSzxR94JUVD3N3ACIiRLgyc2Ep8s= +github.com/bogdanfinn/quic-go-utls v1.0.9-utls/go.mod h1:aHph9B9H9yPOt5xnhWKSOum27DJAqpiHzwX+gjvaXcg= +github.com/bogdanfinn/tls-client v1.15.1 h1:KiFAlED55DJ8Fcocn+/1nX6PrDFcttIHAf/GDkV6KN8= +github.com/bogdanfinn/tls-client v1.15.1/go.mod h1:LsU6mXVn8MOFDwTkyRfI7V1BZM1p0wf2ZfZsICW/1fM= +github.com/bogdanfinn/utls v1.7.7-barnius h1:OuJ497cc7F3yKNVHRsYPQdGggmk5x6+V5ZlrCR7fOLU= +github.com/bogdanfinn/utls v1.7.7-barnius/go.mod h1:aAK1VZQlpKZClF1WEQeq6kyclbkPq4hz6xTbB5xSlmg= +github.com/bogdanfinn/websocket v1.5.5-barnius h1:bY+qnxpai1qe7Jmjx+Sds/cmOSpuuLoR8x61rWltjOI= +github.com/bogdanfinn/websocket v1.5.5-barnius/go.mod h1:gvvEw6pTKHb7yOiFvIfAFTStQWyrm25BMVCTj5wRSsI= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= @@ -60,6 +74,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/randall77/makefat v0.0.0-20260406194835-1b91746796b7 h1:m1yKMZwDSXkT5o2MKhd6ihdzb2dYb6eElNE04xjOSEY= github.com/randall77/makefat v0.0.0-20260406194835-1b91746796b7/go.mod h1:T1TLSfyWVBRXVGzWd0o9BI4kfoO9InEgfQe4NV3mLz8= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -74,6 +90,8 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc= +github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng= github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg= github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= @@ -85,6 +103,8 @@ github.com/zalando/go-keyring v0.2.8 h1:6sD/Ucpl7jNq10rM2pgqTs0sZ9V3qMrqfIIy5YPc github.com/zalando/go-keyring v0.2.8/go.mod h1:tsMo+VpRq5NGyKfxoBVjCuMrG47yj8cmakZDO5QGii0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go4.org v0.0.0-20260112195520-a5071408f32f h1:ziUVAjmTPwQMBmYR1tbdRFJPtTcQUI12fH9QQjfb0Sw= go4.org v0.0.0-20260112195520-a5071408f32f/go.mod h1:ZRJnO5ZI4zAwMFp+dS1+V6J6MSyAowhRqAE+DPa1Xp0= golang.design/x/clipboard v0.8.0 h1:6VEcH28wwcSgKc+vnxHHDWiRjrakTQIAJnPUrt3aOgg= @@ -93,6 +113,8 @@ golang.design/x/x11 v0.2.0 h1:Uiwu2guGihsJX/ZCzpoDPFz5gR/Qntm08mvoBCmRydo= golang.design/x/x11 v0.2.0/go.mod h1:/5q1mFkdc1rL8mvB7DsQFi6as4tIkBv4FXjcP07mrkE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp/shiny v0.0.0-20260727155853-b88d891fe743 h1:rQLtGtwpYzQUyxCqrT3i4lGgvC1Q4G72JTHYncg6AvY= golang.org/x/exp/shiny v0.0.0-20260727155853-b88d891fe743/go.mod h1:JRiBNgSZB8uVKtHGrvbGDQ8p9Ghn3+lkFSk1KqRIiPc= golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= @@ -103,13 +125,17 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211104170005-ce137452f963/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -123,6 +149,7 @@ golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/internal/gateway/dialer.go b/internal/gateway/dialer.go new file mode 100644 index 0000000..cc92c00 --- /dev/null +++ b/internal/gateway/dialer.go @@ -0,0 +1,18 @@ +//go:build !no_spoof_tls_fingerprint + +package gateway + +import ( + "github.com/ayn2op/discordo/internal/tls" + tls_client "github.com/bogdanfinn/tls-client" + "github.com/gorilla/websocket" +) + +func NewDialer() websocket.Dialer { + client := tls.NewClient(tls_client.WithForceHttp1()) + dialer := *websocket.DefaultDialer + dialer.Proxy = nil + dialer.EnableCompression = true + dialer.NetDialTLSContext = client.GetTLSDialer() + return dialer +} diff --git a/internal/gateway/dialer_standard.go b/internal/gateway/dialer_standard.go new file mode 100644 index 0000000..600e25a --- /dev/null +++ b/internal/gateway/dialer_standard.go @@ -0,0 +1,11 @@ +//go:build no_spoof_tls_fingerprint + +package gateway + +import "github.com/gorilla/websocket" + +func NewDialer() websocket.Dialer { + dialer := *websocket.DefaultDialer + dialer.EnableCompression = true + return dialer +} diff --git a/internal/gateway/dialer_standard_test.go b/internal/gateway/dialer_standard_test.go new file mode 100644 index 0000000..f14ca7a --- /dev/null +++ b/internal/gateway/dialer_standard_test.go @@ -0,0 +1,18 @@ +//go:build no_spoof_tls_fingerprint + +package gateway + +import "testing" + +func TestNewDialer(t *testing.T) { + dialer := NewDialer() + if dialer.NetDialTLSContext != nil { + t.Error("custom TLS dialer is enabled") + } + if dialer.Proxy == nil { + t.Error("environment proxy is disabled") + } + if !dialer.EnableCompression { + t.Error("compression is disabled") + } +} diff --git a/internal/gateway/dialer_test.go b/internal/gateway/dialer_test.go new file mode 100644 index 0000000..264027c --- /dev/null +++ b/internal/gateway/dialer_test.go @@ -0,0 +1,25 @@ +//go:build !no_spoof_tls_fingerprint + +package gateway + +import ( + "testing" + + "github.com/gorilla/websocket" +) + +func TestNewDialer(t *testing.T) { + dialer := NewDialer() + if dialer.NetDialTLSContext == nil { + t.Error("TLS dialer is nil") + } + if dialer.Proxy != nil { + t.Error("proxy is enabled") + } + if !dialer.EnableCompression { + t.Error("compression is disabled") + } + if websocket.DefaultDialer.NetDialTLSContext != nil { + t.Error("default dialer was modified") + } +} diff --git a/internal/gateway/gateway.go b/internal/gateway/gateway.go new file mode 100644 index 0000000..768df6a --- /dev/null +++ b/internal/gateway/gateway.go @@ -0,0 +1,20 @@ +package gateway + +import ( + "github.com/ayn2op/arikawa/v3/api" + discordgateway "github.com/ayn2op/arikawa/v3/gateway" + "github.com/ayn2op/arikawa/v3/utils/ws" + "github.com/ayn2op/discordo/internal/http" +) + +const gatewayURL = "wss://gateway.discord.gg" + +func New(id discordgateway.Identifier) *discordgateway.Gateway { + codec := ws.NewCodec(discordgateway.OpUnmarshalers) + codec.Headers.Set("Origin", api.BaseEndpoint) + codec.Headers.Set("User-Agent", http.BrowserUserAgent()) + + conn := ws.NewConnWithDialer(codec, NewDialer()) + socket := ws.NewCustomWebsocket(conn, discordgateway.AddGatewayParams(gatewayURL)) + return discordgateway.FromWebsocketGateway(ws.NewGateway(socket, nil), discordgateway.State{Identifier: id}) +} diff --git a/internal/http/client.go b/internal/http/client.go index 92aa24d..0a3b636 100644 --- a/internal/http/client.go +++ b/internal/http/client.go @@ -1,18 +1,95 @@ +//go:build !no_spoof_tls_fingerprint + package http import ( + "context" + "io" + "maps" "net/http" + "net/url" "github.com/ayn2op/arikawa/v3/api" "github.com/ayn2op/arikawa/v3/utils/httputil" "github.com/ayn2op/arikawa/v3/utils/httputil/httpdriver" + "github.com/ayn2op/discordo/internal/tls" + fhttp "github.com/bogdanfinn/fhttp" + tls_client "github.com/bogdanfinn/tls-client" ) func NewClient(token string) *api.Client { - stdClient := new(http.Client) - stdClient.Transport = NewTransport() - httpClient := httputil.NewClientWithDriver(httpdriver.WrapClient(*stdClient)) - apiClient := api.NewCustomClient(token, httpClient) - apiClient.UserAgent = BrowserUserAgent - return apiClient + httpClient := httputil.NewClientWithDriver(wrapClient(tls.NewClient())) + client := api.NewCustomClient(token, httpClient) + client.UserAgent = BrowserUserAgent() + return client +} + +type client struct { + tls_client.HttpClient +} + +var _ httpdriver.Client = (*client)(nil) + +func wrapClient(c tls_client.HttpClient) httpdriver.Client { + return &client{HttpClient: c} +} + +func (c *client) NewRequest(ctx context.Context, method, url string) (httpdriver.Request, error) { + req, err := fhttp.NewRequestWithContext(ctx, method, url, nil) + if err != nil { + return nil, err + } + return (*request)(req), nil +} + +func (c *client) Do(req httpdriver.Request) (httpdriver.Response, error) { + resp, err := c.HttpClient.Do((*fhttp.Request)(req.(*request))) + if err != nil { + return nil, err + } + return (*response)(resp), nil +} + +type request fhttp.Request + +var _ httpdriver.Request = (*request)(nil) + +func (r *request) GetPath() string { + return r.URL.Path +} + +func (r *request) GetContext() context.Context { + return (*fhttp.Request)(r).Context() +} + +func (r *request) AddHeader(header http.Header) { + maps.Copy(r.Header, fhttp.Header(header)) +} + +func (r *request) AddQuery(values url.Values) { + query := r.URL.Query() + for key, value := range values { + query[key] = append(query[key], value...) + } + r.URL.RawQuery = query.Encode() +} + +func (r *request) WithBody(body io.ReadCloser) { + r.Body = body +} + +type response fhttp.Response + +var _ httpdriver.Response = (*response)(nil) + +func (r *response) GetStatus() int { + return r.StatusCode +} + +func (r *response) GetHeader() http.Header { + return http.Header(r.Header) +} + +func (r *response) GetBody() io.ReadCloser { + return r.Body } diff --git a/internal/http/client_standard.go b/internal/http/client_standard.go new file mode 100644 index 0000000..6c28eb4 --- /dev/null +++ b/internal/http/client_standard.go @@ -0,0 +1,19 @@ +//go:build no_spoof_tls_fingerprint + +package http + +import ( + "net/http" + + "github.com/ayn2op/arikawa/v3/api" + "github.com/ayn2op/arikawa/v3/utils/httputil" + "github.com/ayn2op/arikawa/v3/utils/httputil/httpdriver" +) + +func NewClient(token string) *api.Client { + stdClient := http.Client{Transport: NewTransport()} + httpClient := httputil.NewClientWithDriver(httpdriver.WrapClient(stdClient)) + client := api.NewCustomClient(token, httpClient) + client.UserAgent = BrowserUserAgent() + return client +} diff --git a/internal/http/properties.go b/internal/http/properties.go index 8e252f1..172c73a 100644 --- a/internal/http/properties.go +++ b/internal/http/properties.go @@ -3,6 +3,7 @@ package http import ( "github.com/ayn2op/arikawa/v3/discord" "github.com/ayn2op/arikawa/v3/gateway" + "github.com/ayn2op/discordo/internal/tls" "github.com/google/uuid" ) @@ -11,14 +12,13 @@ import ( const ( OS = "Windows" OSVersion = "10" - - Browser = "Chrome" - BrowserVersion = "143.0.0.0" - BrowserUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " + Browser + "/" + BrowserVersion + " Safari/537.36" - - Locale = discord.EnglishUS + Locale = discord.EnglishUS ) +func BrowserUserAgent() string { + return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " + tls.BrowserName() + "/" + tls.BrowserVersion() + " Safari/537.36" +} + func CommonProperties() gateway.IdentifyProperties { return gateway.IdentifyProperties{ gateway.IdentifyDevice: "", @@ -26,9 +26,9 @@ func CommonProperties() gateway.IdentifyProperties { gateway.IdentifyOS: OS, "os_version": OSVersion, - gateway.IdentifyBrowser: Browser, - "browser_version": BrowserVersion, - "browser_user_agent": BrowserUserAgent, + gateway.IdentifyBrowser: tls.BrowserName(), + "browser_version": tls.BrowserVersion(), + "browser_user_agent": BrowserUserAgent(), "client_build_number": ClientBuildNumber, "client_event_source": nil, diff --git a/internal/http/properties_test.go b/internal/http/properties_test.go new file mode 100644 index 0000000..d4a7147 --- /dev/null +++ b/internal/http/properties_test.go @@ -0,0 +1,35 @@ +package http + +import ( + "strings" + "testing" + + "github.com/ayn2op/arikawa/v3/gateway" + "github.com/ayn2op/discordo/internal/tls" +) + +func TestBrowserIdentity(t *testing.T) { + properties := CommonProperties() + + tests := []struct { + name string + got any + want any + }{ + {"properties browser", properties[gateway.IdentifyBrowser], tls.BrowserName()}, + {"properties version", properties["browser_version"], tls.BrowserVersion()}, + {"properties user agent", properties["browser_user_agent"], BrowserUserAgent()}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.got != test.want { + t.Fatalf("got = %v, want = %v", test.got, test.want) + } + }) + } + + if product := tls.BrowserName() + "/" + tls.BrowserVersion(); !strings.Contains(BrowserUserAgent(), product) { + t.Fatalf("user agent does not contain %q", product) + } +} diff --git a/internal/tls/client.go b/internal/tls/client.go new file mode 100644 index 0000000..63d9e50 --- /dev/null +++ b/internal/tls/client.go @@ -0,0 +1,18 @@ +//go:build !no_spoof_tls_fingerprint + +package tls + +import ( + "fmt" + + tls_client "github.com/bogdanfinn/tls-client" +) + +func NewClient(options ...tls_client.HttpClientOption) tls_client.HttpClient { + options = append([]tls_client.HttpClientOption{tls_client.WithClientProfile(clientProfile)}, options...) + client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...) + if err != nil { + panic(fmt.Errorf("failed to create new tls http client: %w", err)) + } + return client +} diff --git a/internal/tls/client_test.go b/internal/tls/client_test.go new file mode 100644 index 0000000..80b7e8d --- /dev/null +++ b/internal/tls/client_test.go @@ -0,0 +1,15 @@ +//go:build !no_spoof_tls_fingerprint + +package tls + +import "testing" + +func TestNewClient(t *testing.T) { + client := NewClient() + if client == nil { + t.Fatal("client is nil") + } + if client.GetTLSDialer() == nil { + t.Fatal("TLS dialer is nil") + } +} diff --git a/internal/tls/profile.go b/internal/tls/profile.go new file mode 100644 index 0000000..c836ed6 --- /dev/null +++ b/internal/tls/profile.go @@ -0,0 +1,13 @@ +package tls + +import "github.com/bogdanfinn/tls-client/profiles" + +var clientProfile = profiles.Chrome_146 + +func BrowserName() string { + return clientProfile.GetClientHelloId().Client +} + +func BrowserVersion() string { + return clientProfile.GetClientHelloId().Version + ".0.0.0" +} diff --git a/internal/tls/profile_test.go b/internal/tls/profile_test.go new file mode 100644 index 0000000..41bdc8a --- /dev/null +++ b/internal/tls/profile_test.go @@ -0,0 +1,28 @@ +package tls + +import ( + "testing" + + "github.com/bogdanfinn/tls-client/profiles" +) + +func TestBrowserIdentity(t *testing.T) { + hello := profiles.Chrome_146.GetClientHelloId() + + tests := []struct { + name string + got string + want string + }{ + {"name", BrowserName(), hello.Client}, + {"version", BrowserVersion(), hello.Version + ".0.0.0"}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.got != test.want { + t.Fatalf("got = %q, want = %q", test.got, test.want) + } + }) + } +} diff --git a/internal/ui/chat/model.go b/internal/ui/chat/model.go index 8299721..d9f32b0 100644 --- a/internal/ui/chat/model.go +++ b/internal/ui/chat/model.go @@ -15,6 +15,7 @@ import ( "github.com/ayn2op/arikawa/v3/utils/httputil" "github.com/ayn2op/arikawa/v3/utils/ws" "github.com/ayn2op/discordo/internal/config" + clientgateway "github.com/ayn2op/discordo/internal/gateway" "github.com/ayn2op/discordo/internal/http" "github.com/ayn2op/discordo/internal/ui" "github.com/ayn2op/ningen/v3" @@ -85,7 +86,8 @@ func NewModel(app *tview.Application, cfg *config.Config, token string) *Model { Properties: http.IdentifyProperties(), }) - session := session.NewCustom(id, http.NewClient(token), handler.New()) + session := session.NewWithGateway(clientgateway.New(id), handler.New()) + session.Client = http.NewClient(token) state := state.NewFromSession(session, defaultstore.New()) m.state = ningen.FromState(state) m.state.DontWaitForReady = true diff --git a/internal/ui/login/qr/msg.go b/internal/ui/login/qr/msg.go index 4eae565..f944fd3 100644 --- a/internal/ui/login/qr/msg.go +++ b/internal/ui/login/qr/msg.go @@ -12,6 +12,7 @@ import ( "time" "github.com/ayn2op/arikawa/v3/utils/httputil" + "github.com/ayn2op/discordo/internal/gateway" "github.com/ayn2op/discordo/internal/http" "github.com/ayn2op/tview" "github.com/gorilla/websocket" @@ -39,8 +40,9 @@ type connCloseMsg struct{} func (m *Model) connect() tview.Cmd { return func() tview.Msg { headers := http.Headers() - headers.Set("User-Agent", http.BrowserUserAgent) - conn, _, err := websocket.DefaultDialer.Dial(remoteAuthGatewayURL, headers) + headers.Set("User-Agent", http.BrowserUserAgent()) + dialer := gateway.NewDialer() + conn, _, err := dialer.Dial(remoteAuthGatewayURL, headers) if err != nil { return newErrMsg(err) }