diff --git a/config.toml.sample b/config.toml.sample index 36000c0..9420f37 100644 --- a/config.toml.sample +++ b/config.toml.sample @@ -1,4 +1,5 @@ [app] +# Address to listen, use "tor" to run an hidden service. address = "0.0.0.0:9000" # No trailing slashes. diff --git a/go.mod b/go.mod index 52a6e82..0ee038d 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module github.com/knadh/niltalk go 1.13 require ( + github.com/clementauger/tor-prebuilt v0.0.0-20200815153310-0d7058794224 + github.com/cretz/bine v0.1.0 github.com/go-chi/chi v4.1.0+incompatible github.com/gomodule/redigo v2.0.0+incompatible github.com/gorilla/websocket v1.4.2 diff --git a/go.sum b/go.sum index 890d0a2..8e9a46e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/clementauger/tor-prebuilt v0.0.0-20200815153310-0d7058794224 h1:Bm1RJ6O3xTpatREKCjtK3kCmG8SYDgHNGp/qUy0fYek= +github.com/clementauger/tor-prebuilt v0.0.0-20200815153310-0d7058794224/go.mod h1:QVD8AVR2PuMTcxIbUiUBgexRjeVUtymMMVNrGagoVA4= +github.com/cretz/bine v0.1.0 h1:1/fvhLE+fk0bPzjdO5Ci+0ComYxEMuB1JhM4X5skT3g= +github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -38,6 +42,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8 h1:fpnn/HnJONpIu6hkXi1u/7rR0NzilgWr4T0JmWkEitk= golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/main.go b/main.go index 42753b2..f5b5413 100644 --- a/main.go +++ b/main.go @@ -59,6 +59,7 @@ func loadConfig() { f.StringSlice("config", []string{"config.toml"}, "Path to one or more TOML config files to load in order") f.Bool("new-config", false, "generate sample config file") + f.Bool("onion", false, "Show the onion URL") f.String("static-dir", "", "(optional) path to directory with static files") f.Bool("version", false, "Show build version") f.Parse(os.Args[1:]) @@ -238,6 +239,16 @@ func main() { } else { logger.Fatal("app.storage must be one of redis|memory|fs") } + + if ko.Bool("onion") { + pk, err := getOrCreatePK(store) + if err != nil { + logger.Fatal(err) + } + fmt.Printf("http://%v.onion\n", onionAddr(pk)) + os.Exit(0) + } + app.hub = hub.NewHub(app.cfg, store, logger) // Compile static templates. @@ -264,11 +275,30 @@ func main() { }) // Start the app. - srv := &http.Server{ - Addr: ko.String("app.address"), - Handler: r, + var srv interface { + ListenAndServe() error + } + + if appAddress := ko.String("app.address"); appAddress == "tor" { + pk, err := getOrCreatePK(store) + if err != nil { + logger.Fatalf("could not create the private key file: %v", err) + } + + srv = &torServer{ + PrivateKey: pk, + Handler: r, + } + logger.Printf("starting server on http://%v.onion", onionAddr(pk)) + + } else { + srv = &http.Server{ + Addr: appAddress, + Handler: r, + } + logger.Printf("starting server on http://%v", appAddress) } - logger.Printf("starting server on %v", ko.String("app.address")) + if err := srv.ListenAndServe(); err != nil { logger.Fatalf("couldn't start server: %v", err) } diff --git a/store/redis/redis.go b/store/redis/redis.go index 8617e11..d6af6dc 100644 --- a/store/redis/redis.go +++ b/store/redis/redis.go @@ -188,3 +188,18 @@ func (r *Redis) ClearSessions(roomID string) error { _, err := redis.Bool(c.Do("DEL", fmt.Sprintf(r.cfg.PrefixSession, roomID))) return err } + +// Get value from a key. +func (r *Redis) Get(key string) ([]byte, error) { + c := r.pool.Get() + defer c.Close() + return redis.Bytes(c.Do("GET", key)) +} + +// Set a value. +func (r *Redis) Set(key string, data []byte) error { + c := r.pool.Get() + defer c.Close() + _, err := c.Do("SET", key, data) + return err +} diff --git a/store/store.go b/store/store.go index 51e895c..9658924 100644 --- a/store/store.go +++ b/store/store.go @@ -17,6 +17,9 @@ type Store interface { GetSession(sessID, roomID string) (Sess, error) RemoveSession(sessID, roomID string) error ClearSessions(roomID string) error + + Get(key string) ([]byte, error) + Set(key string, value []byte) error } // Room represents the properties of a room in the store. diff --git a/tor.go b/tor.go new file mode 100644 index 0000000..e6c9aaa --- /dev/null +++ b/tor.go @@ -0,0 +1,91 @@ +package main + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "crypto/x509" + "encoding/pem" + "fmt" + "io/ioutil" + "net/http" + "time" + + "github.com/clementauger/tor-prebuilt/embedded" + "github.com/cretz/bine/tor" + "github.com/cretz/bine/torutil" + tued25519 "github.com/cretz/bine/torutil/ed25519" + "github.com/knadh/niltalk/store" +) + +func getOrCreatePK(store store.Store) (privateKey ed25519.PrivateKey, err error) { + key := "onionkey" + d, err := store.Get(key) + if len(d) == 0 || err != nil { + _, privateKey, err = ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + var x509Encoded []byte + x509Encoded, err = x509.MarshalPKCS8PrivateKey(privateKey) + if err != nil { + return nil, err + } + pemEncoded := pem.EncodeToMemory(&pem.Block{Type: "ED25519 PRIVATE KEY", Bytes: x509Encoded}) + err = store.Set(key, pemEncoded) + } else { + block, _ := pem.Decode(d) + x509Encoded := block.Bytes + var tPk interface{} + tPk, err = x509.ParsePKCS8PrivateKey(x509Encoded) + if err != nil { + return nil, err + } + if x, ok := tPk.(ed25519.PrivateKey); ok { + privateKey = x + } else { + err = fmt.Errorf("invalid key type %T wanted ed25519.PrivateKey", tPk) + } + } + return privateKey, err +} + +type torServer struct { + Handler http.Handler + // PrivateKey path to a pem encoded ed25519 private key + PrivateKey ed25519.PrivateKey +} + +func onionAddr(pk ed25519.PrivateKey) string { + return torutil.OnionServiceIDFromV3PublicKey(tued25519.PublicKey([]byte(pk.Public().(ed25519.PublicKey)))) +} + +func (ts *torServer) ListenAndServe() error { + + d, err := ioutil.TempDir("", "") + if err != nil { + return err + } + + // Start tor with default config (can set start conf's DebugWriter to os.Stdout for debug logs) + // fmt.Println("Starting and registering onion service, please wait a couple of minutes...") + t, err := tor.Start(nil, &tor.StartConf{TempDataDirBase: d, ProcessCreator: embedded.NewCreator(), NoHush: true}) + if err != nil { + return fmt.Errorf("unable to start Tor: %v", err) + } + defer 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: ts.PrivateKey, Version3: true, RemotePorts: []int{80}}) + if err != nil { + return fmt.Errorf("unable to create onion service: %v", err) + } + defer onion.Close() + + // fmt.Printf("server listening at http://%v.onion\n", onion.ID) + + return http.Serve(onion, ts.Handler) +}