mirror of
https://github.com/jlelse/GoBlog
synced 2024-07-27 06:45:54 +00:00
Rework tor integration to work without library
This commit is contained in:
parent
d60e613bbd
commit
760e467803
@ -55,7 +55,6 @@ type configServer struct {
|
||||
HttpsKey string `mapstructure:"httpsKey"`
|
||||
HttpsRedirect bool `mapstructure:"httpsRedirect"`
|
||||
Tor bool `mapstructure:"tor"`
|
||||
TorSingleHop bool `mapstructure:"torSingleHop"`
|
||||
SecurityHeaders bool `mapstructure:"securityHeaders"`
|
||||
CSPDomains []string `mapstructure:"cspDomains"`
|
||||
publicHostname string
|
||||
|
@ -38,7 +38,6 @@ Here's an (incomplete) list of features:
|
||||
- Sitemap
|
||||
- Automatic HTTPS using Let's Encrypt
|
||||
- Tor Hidden Service
|
||||
- Tailscale integration for private blogs with HTTPS
|
||||
- Fast in-memory caching for even faster performance
|
||||
- Automatic asset minification of HTML, CSS and JavaScript
|
||||
- Statistics page with information about posts
|
||||
|
@ -40,7 +40,6 @@ server:
|
||||
- media.example.com
|
||||
# Tor
|
||||
tor: true # Publish onion service, requires Tor to be installed and available in path
|
||||
torSingleHop: true # Enable single hop mode (non-anonymous)
|
||||
|
||||
# Cache
|
||||
cache:
|
||||
|
2
go.mod
2
go.mod
@ -12,8 +12,6 @@ require (
|
||||
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
|
||||
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
|
||||
github.com/carlmjohnson/requests v0.23.5
|
||||
// master
|
||||
github.com/cretz/bine v0.2.1-0.20221201125941-b9d31d9c7866
|
||||
github.com/dchest/captcha v1.0.0
|
||||
github.com/dgraph-io/ristretto v0.1.1
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
|
4
go.sum
4
go.sum
@ -37,8 +37,6 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cretz/bine v0.2.1-0.20221201125941-b9d31d9c7866 h1:8Cji9JDEuYibvDQrWHvc42r9/HxjlD2kahN67LECXFk=
|
||||
github.com/cretz/bine v0.2.1-0.20221201125941-b9d31d9c7866/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@ -300,7 +298,6 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
@ -315,7 +312,6 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
|
100
pkgs/tor/tor.go
Normal file
100
pkgs/tor/tor.go
Normal file
@ -0,0 +1,100 @@
|
||||
package tor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/sha512"
|
||||
"encoding/base32"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
// https://github.com/torproject/torspec/blob/8961bb4d83fccb2b987f9899ca83aa430f84ab0c/rend-spec-v3.txt#L2259-L2281
|
||||
func onionAddressFromEd25519(publicKey ed25519.PublicKey) string {
|
||||
checksum := sha3.Sum256(append(append([]byte(".onion checksum"), publicKey...), 0x03))
|
||||
checkdigits := checksum[:2]
|
||||
serviceID := base32.StdEncoding.EncodeToString(append(append(publicKey[:], checkdigits...), 0x03))
|
||||
return strings.ToLower(serviceID) + ".onion"
|
||||
}
|
||||
|
||||
// https://github.com/torproject/torspec/blob/8961bb4d83fccb2b987f9899ca83aa430f84ab0c/rend-spec-v3.txt#L2391-L2449
|
||||
func deriveSecretKey(privateKey ed25519.PrivateKey) []byte {
|
||||
hash := sha512.Sum512(privateKey[:32])
|
||||
hash[0] &= 248
|
||||
hash[31] &= 127
|
||||
hash[31] |= 64
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
// StartTor creates a temporary directory, writes the required files to it,
|
||||
// and then creates a listener and starts the tor process to publish the onion service.
|
||||
func StartTor(privateKey ed25519.PrivateKey, remotePort int) (net.Listener, string, func() error, error) {
|
||||
publicKey := privateKey.Public().(ed25519.PublicKey)
|
||||
onion := onionAddressFromEd25519(publicKey)
|
||||
|
||||
// Create the hidden service directory
|
||||
dir, err := os.MkdirTemp("", "hidden_service_*")
|
||||
if err != nil {
|
||||
return nil, "", nil, fmt.Errorf("failed to create directory: %v", err)
|
||||
}
|
||||
|
||||
// Write the keys to a file
|
||||
secretKeyFilePath := filepath.Join(dir, "hs_ed25519_secret_key")
|
||||
secretKeyFileContent := append([]byte("== ed25519v1-secret: type0 ==\x00\x00\x00"), deriveSecretKey(privateKey)...)
|
||||
err = os.WriteFile(secretKeyFilePath, secretKeyFileContent, 0600)
|
||||
if err != nil {
|
||||
return nil, "", nil, fmt.Errorf("failed to write private key: %v", err)
|
||||
}
|
||||
|
||||
publicKeyFilePath := filepath.Join(dir, "hs_ed25519_public_key")
|
||||
publicKeyFileContent := append([]byte("== ed25519v1-public: type0 ==\x00\x00\x00"), publicKey...)
|
||||
err = os.WriteFile(publicKeyFilePath, publicKeyFileContent, 0600)
|
||||
if err != nil {
|
||||
return nil, "", nil, fmt.Errorf("failed to write public key: %v", err)
|
||||
}
|
||||
|
||||
hostnameFilePath := filepath.Join(dir, "hostname")
|
||||
err = os.WriteFile(hostnameFilePath, []byte(onion), 0600)
|
||||
if err != nil {
|
||||
return nil, "", nil, fmt.Errorf("failed to write hostname: %v", err)
|
||||
}
|
||||
|
||||
// Create listener
|
||||
listener, err := net.Listen("tcp4", "127.0.0.1:")
|
||||
if err != nil {
|
||||
return nil, "", nil, fmt.Errorf("failed to create listener: %v", err)
|
||||
}
|
||||
|
||||
// Start the Tor process
|
||||
cmd := exec.CommandContext(
|
||||
context.Background(),
|
||||
"tor", "--ignore-missing-torrc",
|
||||
"--HiddenServiceDir", dir,
|
||||
"--HiddenServicePort", strconv.Itoa(remotePort)+" "+listener.Addr().String(),
|
||||
"--HiddenServiceVersion", "3",
|
||||
"--SocksPort", "0",
|
||||
)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return nil, "", nil, fmt.Errorf("failed to start Tor: %v", err)
|
||||
}
|
||||
|
||||
// Wait
|
||||
go cmd.Wait()
|
||||
|
||||
return listener, onion, func() error {
|
||||
if err := os.RemoveAll(dir); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, nil
|
||||
}
|
61
tor.go
61
tor.go
@ -1,19 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ed25519"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/cretz/bine/tor"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"go.goblog.app/app/pkgs/tor"
|
||||
)
|
||||
|
||||
const torUsedKey contextKey = "tor"
|
||||
@ -34,35 +32,12 @@ func (a *goBlog) startOnionService(h http.Handler) error {
|
||||
}
|
||||
// Start tor
|
||||
a.info("Starting and registering onion service")
|
||||
t, err := tor.Start(context.Background(), &tor.StartConf{
|
||||
TempDataDirBase: os.TempDir(),
|
||||
NoAutoSocksPort: true,
|
||||
ExtraArgs: a.torExtraArgs(),
|
||||
})
|
||||
listener, addr, cancel, err := tor.StartTor(torKey, 80)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = t.Close()
|
||||
}()
|
||||
// Wait at most a few minutes to publish the service
|
||||
listenCtx, listenCancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||
defer listenCancel()
|
||||
// Create a v3 onion service to listen on any port but show as 80
|
||||
onion, err := t.Listen(listenCtx, &tor.ListenConf{
|
||||
Key: torKey,
|
||||
RemotePorts: []int{80},
|
||||
NonAnonymous: a.cfg.Server.TorSingleHop,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = onion.Close()
|
||||
}()
|
||||
a.torAddress = "http://" + onion.String()
|
||||
torUrl, _ := url.Parse(a.torAddress)
|
||||
a.torHostname = torUrl.Hostname()
|
||||
a.torAddress = "http://" + addr
|
||||
a.torHostname = addr
|
||||
a.info("Onion service published", "address", a.torAddress)
|
||||
// Clear cache
|
||||
a.cache.purge()
|
||||
@ -74,15 +49,20 @@ func (a *goBlog) startOnionService(h http.Handler) error {
|
||||
WriteTimeout: 5 * time.Minute,
|
||||
}
|
||||
a.shutdown.Add(a.shutdownServer(s, "tor"))
|
||||
if err = s.Serve(onion); err != nil && err != http.ErrServerClosed {
|
||||
a.shutdown.Add(func() {
|
||||
if err := cancel(); err != nil {
|
||||
a.error("failed to shutdown tor", "err", err)
|
||||
}
|
||||
})
|
||||
if err = s.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*goBlog) createTorPrivateKey(torDataPath string) (crypto.PrivateKey, error) {
|
||||
func (*goBlog) createTorPrivateKey(torDataPath string) (ed25519.PrivateKey, error) {
|
||||
torKeyPath := filepath.Join(torDataPath, "onion.pk")
|
||||
var torKey crypto.PrivateKey
|
||||
var torKey ed25519.PrivateKey
|
||||
if _, err := os.Stat(torKeyPath); os.IsNotExist(err) {
|
||||
// Tor private key not found, create it
|
||||
_, torKey, err = ed25519.GenerateKey(nil)
|
||||
@ -103,18 +83,15 @@ func (*goBlog) createTorPrivateKey(torDataPath string) (crypto.PrivateKey, error
|
||||
d, _ := os.ReadFile(torKeyPath)
|
||||
block, _ := pem.Decode(d)
|
||||
x509Encoded := block.Bytes
|
||||
torKey, err = x509.ParsePKCS8PrivateKey(x509Encoded)
|
||||
parsedTorKey, err := x509.ParsePKCS8PrivateKey(x509Encoded)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ok := false
|
||||
torKey, ok = parsedTorKey.(ed25519.PrivateKey)
|
||||
if !ok {
|
||||
return nil, errors.New("could not parse Tor key as ed25519 private key")
|
||||
}
|
||||
}
|
||||
return torKey, nil
|
||||
}
|
||||
|
||||
func (a *goBlog) torExtraArgs() []string {
|
||||
s := []string{"--SocksPort", "0"}
|
||||
if a.cfg.Server.TorSingleHop {
|
||||
s = append(s, "--HiddenServiceNonAnonymousMode", "1", "--HiddenServiceSingleHopMode", "1")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
FLAGS="-tags=linux,libsqlite3,sqlite_fts5"
|
||||
EXTRA="
|
||||
github.com/cretz/bine@master
|
||||
"
|
||||
EXTRA=""
|
||||
|
||||
# Update all direct dependencies to latest version
|
||||
echo "Check for updates..."
|
||||
|
Loading…
Reference in New Issue
Block a user