Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions backend/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import (
"github.com/BitBoxSwiss/bitbox-wallet-app/util/observable"
"github.com/BitBoxSwiss/bitbox-wallet-app/util/observable/action"
"github.com/BitBoxSwiss/bitbox-wallet-app/util/socksproxy"
"github.com/BitBoxSwiss/bitbox-wallet-app/util/useragent"
"github.com/btcsuite/btcd/chaincfg"
"github.com/ethereum/go-ethereum/params"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -198,6 +199,8 @@ type Environment interface {
// BluetoothConnect tries to connect to the peripheral by the given identifier.
// Use `backend.bluetooth.State()` to track failure.
BluetoothConnect(identifier string)
// UserAgentHost returns the host platform/device token used in the app's outbound user agent.
UserAgentHost() string
}

// Backend ties everything together and is the main starting point to use the BitBox wallet library.
Expand Down Expand Up @@ -287,6 +290,13 @@ func NewBackend(arguments *arguments.Arguments, environment Environment) (*Backe
useProxy,
backendConfig.AppConfig().Backend.Proxy.ProxyAddress,
)
userAgentHost := useragent.HostFromRuntime()
if environment != nil {
if host := environment.UserAgentHost(); host != "" {
userAgentHost = host
}
}
backendProxy = backendProxy.WithUserAgent(useragent.String(versioninfo.Version.String(), userAgentHost))
hclient, err := backendProxy.GetHTTPClient()
if err != nil {
return nil, err
Expand Down
4 changes: 4 additions & 0 deletions backend/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ func (e environment) OnAuthSettingChanged(bool) {}

func (e environment) BluetoothConnect(string) {}

func (e environment) UserAgentHost() string {
return "linux"
}

type mockTransactionsSource struct {
}

Expand Down
9 changes: 9 additions & 0 deletions backend/bridgecommon/bridgecommon.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ type BackendEnvironment struct {
AuthFunc func()
OnAuthSettingChangedFunc func(bool)
BluetoothConnectFunc func(string)
UserAgentHostFunc func() string
}

// NotifyUser implements backend.Environment.
Expand Down Expand Up @@ -272,6 +273,14 @@ func (env *BackendEnvironment) BluetoothConnect(identifier string) {
}
}

// UserAgentHost implements backend.Environment.
func (env *BackendEnvironment) UserAgentHost() string {
if env.UserAgentHostFunc != nil {
return env.UserAgentHostFunc()
}
return ""
}

// Serve serves the BitBox API for use in a native client.
func Serve(
testnet bool,
Expand Down
4 changes: 4 additions & 0 deletions backend/bridgecommon/bridgecommon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ func (e environment) OnAuthSettingChanged(bool) {}

func (e environment) BluetoothConnect(string) {}

func (e environment) UserAgentHost() string {
return "linux"
}

// TestServeShutdownServe checks that you can call Serve twice in a row.
func TestServeShutdownServe(t *testing.T) {
bridgecommon.Serve(
Expand Down
1 change: 1 addition & 0 deletions backend/handlers/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func (e *backendEnv) DetectDarkTheme() bool { return false }
func (e *backendEnv) Auth() {}
func (e *backendEnv) OnAuthSettingChanged(bool) {}
func (e *backendEnv) BluetoothConnect(string) {}
func (e *backendEnv) UserAgentHost() string { return "linux" }

func TestGetNativeLocale(t *testing.T) {
const ptLocale = "pt"
Expand Down
2 changes: 2 additions & 0 deletions backend/mobileserver/mobileserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type GoEnvironmentInterface interface {
Auth()
OnAuthSettingChanged(bool)
BluetoothConnect(string)
UserAgentHost() string
}

// readWriteCloser implements io.ReadWriteCloser, translating from GoReadWriteCloserInterface. All methods
Expand Down Expand Up @@ -194,6 +195,7 @@ func Serve(dataDir string, testnet bool, environment GoEnvironmentInterface, goA
AuthFunc: environment.Auth,
OnAuthSettingChangedFunc: environment.OnAuthSettingChanged,
BluetoothConnectFunc: environment.BluetoothConnect,
UserAgentHostFunc: environment.UserAgentHost,
},
)
}
Expand Down
6 changes: 6 additions & 0 deletions cmd/servewallet/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/BitBoxSwiss/bitbox-wallet-app/util/config"
"github.com/BitBoxSwiss/bitbox-wallet-app/util/logging"
"github.com/BitBoxSwiss/bitbox-wallet-app/util/system"
"github.com/BitBoxSwiss/bitbox-wallet-app/util/useragent"
"github.com/sirupsen/logrus"
)

Expand Down Expand Up @@ -96,6 +97,11 @@ func (webdevEnvironment) OnAuthSettingChanged(enabled bool) {
func (webdevEnvironment) BluetoothConnect(identifier string) {
}

// UserAgentHost implements backend.Environment.
func (webdevEnvironment) UserAgentHost() string {
return useragent.HostFromRuntime()
}

// NativeLocale naively implements backend.Environment.
// This version is unlikely to work on Windows.
func (webdevEnvironment) NativeLocale() string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@ public String nativeLocale() {
return locale.toString();
}

public String userAgentHost() {
return "android";
}

@Override
public void setDarkTheme(boolean isDark) {
Util.log("Set Dark Theme GoViewModel - isdark: " + isDark);
Expand Down
5 changes: 5 additions & 0 deletions frontends/ios/BitBoxApp/BitBoxApp/BitBoxAppApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import SwiftUI
import UIKit
import Mobileserver
import LocalAuthentication
import Network
Expand Down Expand Up @@ -84,6 +85,10 @@ class GoEnvironment: NSObject, MobileserverGoEnvironmentInterfaceProtocol, UIDoc
return Locale.current.identifier
}

func userAgentHost() -> String {
return UIDevice.current.userInterfaceIdiom == .pad ? "ipad" : "iphone"
}

func notifyUser(_ p0: String?) {
}

Expand Down
2 changes: 2 additions & 0 deletions frontends/qt/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import (
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/mobileserver"
"github.com/BitBoxSwiss/bitbox-wallet-app/util/logging"
"github.com/BitBoxSwiss/bitbox-wallet-app/util/system"
"github.com/BitBoxSwiss/bitbox-wallet-app/util/useragent"
)

// nativeCommunication implements bridge.NativeCommunication.
Expand Down Expand Up @@ -198,6 +199,7 @@ func serve(
},
OnAuthSettingChangedFunc: func(bool) {},
BluetoothConnectFunc: func(string) {},
UserAgentHostFunc: useragent.HostFromRuntime,
},
)
}
Expand Down
22 changes: 19 additions & 3 deletions util/socksproxy/socksproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/url"

"github.com/BitBoxSwiss/bitbox-wallet-app/util/logging"
"github.com/BitBoxSwiss/bitbox-wallet-app/util/useragent"
"github.com/sirupsen/logrus"
"golang.org/x/net/proxy"
)
Expand All @@ -17,6 +18,7 @@ type SocksProxy struct {
useProxy bool
proxyAddress string
fullProxyAddress string
userAgent string
log *logrus.Entry
}

Expand All @@ -37,6 +39,12 @@ func NewSocksProxy(useProxy bool, proxyAddress string) SocksProxy {
return proxy
}

// WithUserAgent configures the user agent for BitBox/Shift-owned HTTP requests.
func (socksProxy SocksProxy) WithUserAgent(ua string) SocksProxy {
socksProxy.userAgent = ua
return socksProxy
}

// Validate validates the socks5 proxy endpoint.
// We check if we could instantiate a proxied http client.
// Currently, no actual connectivity checks as performed.
Expand Down Expand Up @@ -88,8 +96,16 @@ func (socksProxy *SocksProxy) GetHTTPClient() (*http.Client, error) {
// Make a http.Transport that uses the proxy dialer, and a
// http.Client that uses the transport.
tbTransport := &http.Transport{Dial: tbDialer.Dial}
client := &http.Client{Transport: tbTransport}
return client, nil
return socksProxy.httpClient(tbTransport), nil
}
return socksProxy.httpClient(nil), nil
}

func (socksProxy *SocksProxy) httpClient(transport http.RoundTripper) *http.Client {
if socksProxy.userAgent == "" {
return &http.Client{Transport: transport}
}
return &http.Client{
Transport: useragent.NewTransport(transport, socksProxy.userAgent),
}
return &http.Client{}, nil
}
26 changes: 26 additions & 0 deletions util/socksproxy/socksproxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
package socksproxy

import (
"io"
"net/http"
"strings"
"testing"

"github.com/stretchr/testify/require"
Expand All @@ -16,3 +19,26 @@ func TestValidate(t *testing.T) {
require.Error(t, NewSocksProxy(true, "127.0.0.1:XXXX").Validate())
require.Error(t, NewSocksProxy(true, "127.0.0.1:9050 ").Validate())
}

func TestGetHTTPClientUserAgent(t *testing.T) {
const ua = "BitBoxApp/1.2.3 (linux)"

socksProxy := NewSocksProxy(false, "").WithUserAgent(ua)
client := socksProxy.httpClient(roundTripFunc(func(req *http.Request) (*http.Response, error) {
require.Equal(t, ua, req.Header.Get("User-Agent"))
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader("{}")),
Header: http.Header{},
}, nil
}))

_, err := client.Get("https://bitboxapp.shiftcrypto.io/banners.json")
require.NoError(t, err)
}

type roundTripFunc func(req *http.Request) (*http.Response, error)

func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return f(req)
}
73 changes: 73 additions & 0 deletions util/useragent/useragent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: Apache-2.0

package useragent

import (
"net/http"
"runtime"
"strings"
)

var ownedHostSuffixes = []string{
".shiftcrypto.io",
".shiftcrypto.dev",
".bitbox.swiss",
}

// String returns the BitBoxApp user agent.
func String(version, host string) string {
return "BitBoxApp/" + version + " (" + host + ")"
}

// HostFromRuntime returns the user agent host token for the current Go runtime.
func HostFromRuntime() string {
switch runtime.GOOS {
case "darwin":
return "mac"
case "windows":
return "win"
default:
return runtime.GOOS
}
}

// IsOwnedHost returns true if host belongs to BitBox/Shift infrastructure.
func IsOwnedHost(host string) bool {
host = strings.ToLower(strings.TrimSuffix(host, "."))
if host == "digitalbitbox.com" {
return true
}
for _, suffix := range ownedHostSuffixes {
if strings.HasSuffix(host, suffix) {
return true
}
}
return false
}

type transport struct {
base http.RoundTripper
userAgent string
}

// NewTransport returns a transport that adds the user agent only to BitBox/Shift-owned hosts.
func NewTransport(base http.RoundTripper, userAgent string) http.RoundTripper {
if base == nil {
base = http.DefaultTransport
}
return &transport{
base: base,
userAgent: userAgent,
}
}

func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
if req.Header.Get("User-Agent") != "" || !IsOwnedHost(req.URL.Hostname()) {
return t.base.RoundTrip(req)
}

cloned := req.Clone(req.Context())
cloned.Header = req.Header.Clone()
cloned.Header.Set("User-Agent", t.userAgent)
return t.base.RoundTrip(cloned)
}
Loading