Skip to content

Commit

Permalink
Mobile companion app and tor implementation (#2715)
Browse files Browse the repository at this point in the history
Co-authored-by: Brian Stafford <[email protected]>
  • Loading branch information
peterzen and buck54321 authored Jan 21, 2025
1 parent 333e9f6 commit 57a93e8
Show file tree
Hide file tree
Showing 95 changed files with 4,021 additions and 22 deletions.
52 changes: 52 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Android build
on:
workflow_dispatch:
inputs:
name:
description: "Release-Build"
default: "Build Android companion app"

jobs:
build:
runs-on: ubuntu-latest

steps:
# Checkout project code
- name: Checking out branch
uses: actions/checkout@1e31de5234b9f8995739874a8ce0492dc87873e2 # v4.0.0
with:
# Use sparse checkout to only select files in mobile app directory
# Turning off cone mode ensures that files in the project root are not included during checkout
sparse-checkout: 'companionapp/android'
sparse-checkout-cone-mode: false

# This step is needed because expo-github-action does not support paths.
# Therefore all mobile app assets should be moved to the project root.
- name: Move mobile app files to root
run: |
ls -lah
shopt -s dotglob
mv companionapp/android/* .
ls -lah
- name: Setup Java
uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
with:
distribution: 'temurin'
java-version: 17

- name: Setup Android SDK
uses: android-actions/setup-android@00854ea68c109d98c75d956347303bf7c45b0277 # v3.2.1

- name: Build Debug apk
run: ./gradlew assembleDebug --stacktrace

- name: Get apk path
id: debugApk
run: echo "apkfile=$(find app/build/outputs/apk/debug/*.apk)" >> $GITHUB_OUTPUT

- name: Upload Debug Build to Artifacts
uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1
with:
name: debug-apk
path: ${{ steps.debugApk.outputs.apkfile }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,5 @@ server/cmd/validatemarkets
client/cmd/translationsreport/translationsreport
client/cmd/translationsreport/worksheets
server/cmd/dexadm/dexadm
client/tor/build
server/cmd/geogame/geogame
5 changes: 3 additions & 2 deletions client/app/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ type WebConfig struct {
HTTPProfile bool `long:"httpprof" description:"Start HTTP profiler on /pprof."`
// Deprecated
Experimental bool `long:"experimental" description:"DEPRECATED: Enable experimental features"`
Tor bool `long:"tor" description:"Enable tor hidden service"`
}

// LogConfig encapsulates the logging-related settings.
Expand Down Expand Up @@ -185,6 +186,7 @@ func (cfg *Config) Web(c *core.Core, mm *mm.MarketMaker, log dex.Logger, utc boo
}

return &webserver.Config{
DataDir: filepath.Join(cfg.AppData, "srv"),
Core: c,
MarketMaker: mmCore,
Addr: cfg.WebAddr,
Expand All @@ -193,9 +195,8 @@ func (cfg *Config) Web(c *core.Core, mm *mm.MarketMaker, log dex.Logger, utc boo
UTC: utc,
CertFile: certFile,
KeyFile: keyFile,
NoEmbed: cfg.NoEmbedSite,
HttpProf: cfg.HTTPProfile,
Language: cfg.Language,
Tor: cfg.Tor,
}
}

Expand Down
3 changes: 3 additions & 0 deletions client/tor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Embedding tor

To embed `tor` in dexc binary, compile it with `./build_linux.sh` and build using `go build -tags tor`.
5 changes: 5 additions & 0 deletions client/tor/binary_notor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//go:build !tor

package tor

var torBinary []byte
8 changes: 8 additions & 0 deletions client/tor/binary_tor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//go:build tor

package tor

import _ "embed"

//go:embed build/tor
var torBinary []byte
Empty file added client/tor/build/tor
Empty file.
25 changes: 25 additions & 0 deletions client/tor/build_linux.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash

BASE_DIR=$(realpath $(dirname $0))
BUILD_DIR="${BASE_DIR}/build"
REPO_DIR="${BUILD_DIR}/torrepo"

COMMIT_HASH=3cb6a690be60fcdab60130402ff88dcfc0657596

rm -r -f "${REPO_DIR}"
mkdir -p "${REPO_DIR}"
cd "${REPO_DIR}"

git init
git remote add origin https://gitlab.torproject.org/tpo/core/tor
git fetch --depth 1 origin ${COMMIT_HASH}
git checkout FETCH_HEAD

./autogen.sh
./configure --disable-asciidoc
make -j$(nproc)

cd "${BUILD_DIR}"
rm -f tor
cp "${REPO_DIR}/src/app/tor" .
rm -rf "${REPO_DIR}"
195 changes: 195 additions & 0 deletions client/tor/tor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.

package tor

import (
"context"
_ "embed"
"errors"
"fmt"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"syscall"
"time"

"decred.org/dcrdex/dex"
"github.com/jrick/logrotate/rotator"
)

type HiddenService struct {
log dex.Logger
exePath string
dataDir string
logPath string

serverAddr string
onionAddr string
}

func New(dataDir string, log dex.Logger) (_ *HiddenService, err error) {
if len(torBinary) == 0 {
log.Error("It doesn't look like tor was packaged correctly")
log.Error("Tor is only available on Linux for now")
log.Error("If you are on linux and building bisonwallet from source, run client/build_linux.sh and then rebuild bisonwallet")
log.Error("If this is a release, we really botched it")
return nil, errors.New("tor not packaged")
}

logDir := filepath.Join(dataDir, "logs")
if err := os.MkdirAll(logDir, 0700); err != nil {
return nil, fmt.Errorf("error creating tor directory: %w", err)
}

exePath := filepath.Join(dataDir, "tor")
if err := os.WriteFile(exePath, torBinary, 0700); err != nil {
return nil, fmt.Errorf("error writing tor binary: %w", err)
}

return &HiddenService{
log: log,
exePath: exePath,
logPath: filepath.Join(logDir, "tor.Log"),
dataDir: dataDir,
}, nil
}

const torrcTemplate = `#Bison Wallet tor relay configuration
SOCKSPort %s
DataDirectory %s
HiddenServiceDir %s
HiddenServicePort 80 %s
`

func (s *HiddenService) Connect(ctx context.Context) (*sync.WaitGroup, error) {
torrcPath := filepath.Join(s.dataDir, "torrc")

hiddenServiceDir := filepath.Join(s.dataDir, "hidden-service")

ports, err := findOpenPort(2)
if err != nil {
return nil, fmt.Errorf("error finding open port: %w", err)
}

socksPort, serverPort := ports[0], ports[1]
s.serverAddr = "127.0.0.1:" + serverPort

torrcData := []byte(fmt.Sprintf(torrcTemplate, socksPort, s.dataDir, hiddenServiceDir, s.serverAddr))
if err := os.WriteFile(torrcPath, torrcData, 0600); err != nil {
return nil, fmt.Errorf("error writing torrc file: %w", err)
}

var wg sync.WaitGroup

wg.Add(1)
go func() {
defer wg.Done()

const maxLogRolls = 8
const logFileMaxSizeKB = 16 * 1024 // 16 MB
logRotator, err := rotator.New(s.logPath, logFileMaxSizeKB, false, maxLogRolls)
if err != nil {
s.log.Errorf("error intializing log files: %w", err)
return
}
defer logRotator.Close()

cmd := exec.CommandContext(ctx, s.exePath, "-f", torrcPath)
cmd.Stdout = logRotator
cmd.Stderr = logRotator
cmd.Cancel = func() error {
if cmd.Process == nil { // probably not possible?
return nil
}
if err := cmd.Process.Signal(syscall.SIGINT); err != nil {
cmd.Process.Kill()
return err
}
return nil
}
if err := cmd.Start(); err != nil {
s.log.Errorf("tor node exited with an error: %v", err)
return
}

<-ctx.Done()

errC := make(chan error)
go func() {
_, err := cmd.Process.Wait()
errC <- err
}()

select {
case err := <-errC:
if err != nil {
s.log.Errorf("Error attempting clean shutdown: %v", err)
}
case <-time.After(time.Second * 5):
cmd.Process.Kill()
s.log.Error("Timed out waiting for clean shutdown")
}
}()

hostnamePath := filepath.Join(hiddenServiceDir, "hostname")
timeout := time.After(time.Second * 10)
checkTick := time.After(0)
out:
for {
select {
case <-checkTick:
b, err := os.ReadFile(hostnamePath)
if err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("error attemtpting to read hostname file: %w", err)
}
} else {
s.onionAddr = strings.TrimSpace(string(b))
break out
}
case <-timeout:
return nil, fmt.Errorf("timed out waiting for tor to publish hostname")
case <-ctx.Done():
return nil, ctx.Err()
}
checkTick = time.After(time.Millisecond * 100)
}

return &wg, nil
}

func (s *HiddenService) ServerAddress() string {
return s.serverAddr
}

func (s *HiddenService) OnionAddress() string {
return "http://" + s.onionAddr
}

func findOpenPort(n int) ([]string, error) {
ports := make([]string, n)
for i := 0; i < n; i++ {
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
if err != nil {
return nil, err
}

l, err := net.ListenTCP("tcp", addr)
if err != nil {
return nil, err
}
defer l.Close()
_, port, err := net.SplitHostPort(l.Addr().String())
if err != nil {
return nil, err
}
ports[i] = port
}

return ports, nil
}
36 changes: 36 additions & 0 deletions client/tor/tor_live_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//go:build live

package tor

import (
"context"
"fmt"
"os"
"testing"

"decred.org/dcrdex/dex"
)

func TestConnect(t *testing.T) {
dataDir, _ := os.MkdirTemp("", "")
defer os.RemoveAll(dataDir)

log := dex.StdOutLogger("T", dex.LevelDebug)
relay, err := New(dataDir, log)
if err != nil {
t.Fatalf("New error: %v", err)
}

cm := dex.NewConnectionMaster(relay)
defer cm.Wait()

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

if err := cm.ConnectOnce(ctx); err != nil {
t.Fatalf("Connect error: %v", err)
}

fmt.Println("Generated server address:", relay.ServerAddress())
fmt.Println("Generated onion address:", relay.OnionAddress())
}
2 changes: 2 additions & 0 deletions client/webserver/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1546,13 +1546,15 @@ func (s *WebServer) apiUser(w http.ResponseWriter, r *http.Request) {
Langs []string `json:"langs"`
Inited bool `json:"inited"`
OK bool `json:"ok"`
OnionUrl string `json:"onionUrl"`
MMStatus *mm.Status `json:"mmStatus"`
}{
User: u,
Lang: s.lang.Load().(string),
Langs: s.langs,
Inited: s.core.IsInitialized(),
OK: true,
OnionUrl: s.onion,
MMStatus: mmStatus,
}
writeJSON(w, response)
Expand Down
Loading

0 comments on commit 57a93e8

Please sign in to comment.