Merge pull request 'adding 'servers' interface to manage multiple servers and support for encrypted configs' (#16) from servers into trunk

Reviewed-on: #16
This commit is contained in:
Dan Ballard 2021-11-01 16:01:12 +00:00
commit c148e120f3
13 changed files with 498 additions and 106 deletions

View File

@ -8,12 +8,14 @@
The app takes the following arguments The app takes the following arguments
- -debug: enabled debug logging - -debug: enabled debug logging
- -exportTofuBundle: Export the tofubundle to a file called tofubundle - -exportServerBundle: Export the server bundle to a file called serverbundle
The app takes the following environment variables The app takes the following environment variables
- CWTCH_HOME: sets the config dir for the app - CWTCH_HOME: sets the config dir for the app
`env CONFIG_HOME=./conf ./app`
## Using the Server ## Using the Server
When run the app will output standard log lines, one of which will contain the `tofubundle` in purple. This is the part you need to capture and import into a Cwtch client app so you can use the server for hosting groups When run the app will output standard log lines, one of which will contain the `tofubundle` in purple. This is the part you need to capture and import into a Cwtch client app so you can use the server for hosting groups

View File

@ -2,10 +2,8 @@ package main
import ( import (
"crypto/rand" "crypto/rand"
"cwtch.im/cwtch/model"
"encoding/base64" "encoding/base64"
"flag" "flag"
"fmt"
cwtchserver "git.openprivacy.ca/cwtch.im/server" cwtchserver "git.openprivacy.ca/cwtch.im/server"
"git.openprivacy.ca/cwtch.im/tapir/primitives" "git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity/tor" "git.openprivacy.ca/openprivacy/connectivity/tor"
@ -19,13 +17,9 @@ import (
"time" "time"
) )
const (
serverConfigFile = "serverConfig.json"
)
func main() { func main() {
flagDebug := flag.Bool("debug", false, "Enable debug logging") flagDebug := flag.Bool("debug", false, "Enable debug logging")
flagExportTofu := flag.Bool("exportTofuBundle", false, "Export the tofubundle to a file called tofubundle") flagExportServer := flag.Bool("exportServerBundle", false, "Export the server bundle to a file called serverbundle")
flag.Parse() flag.Parse()
log.AddEverythingFromPattern("server/app/main") log.AddEverythingFromPattern("server/app/main")
@ -52,19 +46,25 @@ func main() {
ReportingGroupID: "", ReportingGroupID: "",
ReportingServerAddr: "", ReportingServerAddr: "",
} }
config.Save(".", "serverConfig.json") config.ConfigDir = "."
config.FilePath = cwtchserver.ServerConfigFile
config.Encrypted = false
config.Save()
return return
} }
serverConfig := cwtchserver.LoadConfig(configDir, serverConfigFile) serverConfig, err := cwtchserver.LoadCreateDefaultConfigFile(configDir, cwtchserver.ServerConfigFile, false, "")
if err != nil {
log.Errorf("Could not load/create config file: %s\n", err)
return
}
// we don't need real randomness for the port, just to avoid a possible conflict... // we don't need real randomness for the port, just to avoid a possible conflict...
mrand.Seed(int64(time.Now().Nanosecond())) mrand.Seed(int64(time.Now().Nanosecond()))
controlPort := mrand.Intn(1000) + 9052 controlPort := mrand.Intn(1000) + 9052
// generate a random password // generate a random password
key := make([]byte, 64) key := make([]byte, 64)
_, err := rand.Read(key) _, err = rand.Read(key)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -79,25 +79,15 @@ func main() {
} }
defer acn.Close() defer acn.Close()
server := new(cwtchserver.Server) server := cwtchserver.NewServer(serverConfig)
log.Infoln("starting cwtch server...") log.Infoln("starting cwtch server...")
log.Infof("Server %s\n", server.Onion())
server.Setup(serverConfig) log.Infof("Server bundle (import into client to use server): %s\n", log.Magenta(server.ServerBundle()))
// TODO create a random group for testing if *flagExportServer {
group, _ := model.NewGroup(tor.GetTorV3Hostname(serverConfig.PublicKey)) // Todo: change all to server export
invite, err := group.Invite() ioutil.WriteFile(path.Join(serverConfig.ConfigDir, "serverbundle"), []byte(server.TofuBundle()), 0600)
if err != nil {
panic(err)
}
bundle := server.KeyBundle().Serialize()
tofubundle := fmt.Sprintf("tofubundle:server:%s||%s", base64.StdEncoding.EncodeToString(bundle), invite)
log.Infof("Server Tofu Bundle (import into client to use server): %s", log.Magenta(tofubundle))
log.Infof("Server Config: server address:%s", base64.StdEncoding.EncodeToString(bundle))
if *flagExportTofu {
ioutil.WriteFile(path.Join(serverConfig.ConfigDir, "tofubundle"), []byte(tofubundle), 0600)
} }
// Graceful Shutdown // Graceful Shutdown
@ -105,8 +95,8 @@ func main() {
signal.Notify(c, os.Interrupt, syscall.SIGTERM) signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() { go func() {
<-c <-c
server.Shutdown()
acn.Close() acn.Close()
server.Close()
os.Exit(1) os.Exit(1)
}() }()

View File

@ -64,5 +64,5 @@ COPY ./docker/docker-entrypoint /usr/local/bin/
VOLUME /etc/tor /var/lib/tor /var/lib/cwtch VOLUME /etc/tor /var/lib/tor /var/lib/cwtch
ENTRYPOINT ["docker-entrypoint"] ENTRYPOINT ["docker-entrypoint"]
CMD ["/usr/local/bin/cwtch","--exportTofuBundle"] CMD ["/usr/local/bin/cwtch","--exportServerBundle"]

10
go.mod
View File

@ -3,12 +3,12 @@ module git.openprivacy.ca/cwtch.im/server
go 1.14 go 1.14
require ( require (
cwtch.im/cwtch v0.8.5 cwtch.im/cwtch v0.12.2
git.openprivacy.ca/cwtch.im/tapir v0.4.2 git.openprivacy.ca/cwtch.im/tapir v0.4.9
git.openprivacy.ca/openprivacy/connectivity v1.4.3 git.openprivacy.ca/openprivacy/connectivity v1.5.0
git.openprivacy.ca/openprivacy/log v1.0.2 git.openprivacy.ca/openprivacy/log v1.0.3
github.com/gtank/ristretto255 v0.1.2 github.com/gtank/ristretto255 v0.1.2
github.com/mattn/go-sqlite3 v1.14.7 github.com/mattn/go-sqlite3 v1.14.7
github.com/struCoder/pidusage v0.2.1 github.com/struCoder/pidusage v0.2.1
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee
) )

14
go.sum
View File

@ -4,19 +4,33 @@ cwtch.im/cwtch v0.8.0 h1:QDRaDBTXefFRPZPqUMtxoNhOcgXv0rl0bGjysSOmJX0=
cwtch.im/cwtch v0.8.0/go.mod h1:+SY/4ueF1U7mK+CX8hZFbtd+GC1lx/cReo110KgtQAw= cwtch.im/cwtch v0.8.0/go.mod h1:+SY/4ueF1U7mK+CX8hZFbtd+GC1lx/cReo110KgtQAw=
cwtch.im/cwtch v0.8.5 h1:W67jAF2oRwqWytbZEv1UeCqW0cU2x69tgUw8iy27xFA= cwtch.im/cwtch v0.8.5 h1:W67jAF2oRwqWytbZEv1UeCqW0cU2x69tgUw8iy27xFA=
cwtch.im/cwtch v0.8.5/go.mod h1:5GHxaaeVnKeXSU64IvtCKzkqhU8DRiLoVM+tiBT8kkc= cwtch.im/cwtch v0.8.5/go.mod h1:5GHxaaeVnKeXSU64IvtCKzkqhU8DRiLoVM+tiBT8kkc=
cwtch.im/cwtch v0.12.2 h1:I+ndKadCRCITw4SPbd+1cpRv+z/7iHjjTUv8OzRwTrE=
cwtch.im/cwtch v0.12.2/go.mod h1:QpTkQK7MqNt0dQK9/pBk5VpkvFhy6xuoxJIn401B8fM=
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
git.openprivacy.ca/cwtch.im/tapir v0.4.0 h1:clG8uORt0NKEhT4P+Dpw1pzyUuYzYBMevGqn2pciKk8= git.openprivacy.ca/cwtch.im/tapir v0.4.0 h1:clG8uORt0NKEhT4P+Dpw1pzyUuYzYBMevGqn2pciKk8=
git.openprivacy.ca/cwtch.im/tapir v0.4.0/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E= git.openprivacy.ca/cwtch.im/tapir v0.4.0/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E=
git.openprivacy.ca/cwtch.im/tapir v0.4.1 h1:9LMpQX41IzecNNlRc1FZKXHg6wlFss679tFsa3vzb3Y= git.openprivacy.ca/cwtch.im/tapir v0.4.1 h1:9LMpQX41IzecNNlRc1FZKXHg6wlFss679tFsa3vzb3Y=
git.openprivacy.ca/cwtch.im/tapir v0.4.1/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E= git.openprivacy.ca/cwtch.im/tapir v0.4.1/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E=
git.openprivacy.ca/cwtch.im/tapir v0.4.2 h1:bxMWZnVJXX4dqqOFS7ELW4iFkVL4GS8wiRkjRv5rJe8= git.openprivacy.ca/cwtch.im/tapir v0.4.2 h1:bxMWZnVJXX4dqqOFS7ELW4iFkVL4GS8wiRkjRv5rJe8=
git.openprivacy.ca/cwtch.im/tapir v0.4.2/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E= git.openprivacy.ca/cwtch.im/tapir v0.4.2/go.mod h1:eH6dZxXrhW0C4KZX18ksUa6XJCrEvtg8cJJ/Fy6gv+E=
git.openprivacy.ca/cwtch.im/tapir v0.4.4 h1:KyuTVmr9GYptTCeR7JDODjmhBBbnIBf9V3NSC4+6bHc=
git.openprivacy.ca/cwtch.im/tapir v0.4.4/go.mod h1:qMFTdmDZITc1BLP1jSW0gVpLmvpg+Zjsh5ek8StwbFE=
git.openprivacy.ca/cwtch.im/tapir v0.4.9 h1:LXonlztwvI1F1++0IyomIcDH1/Bxzo+oN8YjGonNvjM=
git.openprivacy.ca/cwtch.im/tapir v0.4.9/go.mod h1:p4bHo3DAO8wwimU6JAeZXbfPQ4jnoA2bV+4YvknWTNQ=
git.openprivacy.ca/openprivacy/bine v0.0.4 h1:CO7EkGyz+jegZ4ap8g5NWRuDHA/56KKvGySR6OBPW+c= git.openprivacy.ca/openprivacy/bine v0.0.4 h1:CO7EkGyz+jegZ4ap8g5NWRuDHA/56KKvGySR6OBPW+c=
git.openprivacy.ca/openprivacy/bine v0.0.4/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU= git.openprivacy.ca/openprivacy/bine v0.0.4/go.mod h1:13ZqhKyqakDsN/ZkQkIGNULsmLyqtXc46XBcnuXm/mU=
git.openprivacy.ca/openprivacy/connectivity v1.4.3 h1:i2Ad/U9FlL9dKr2bhRck7lJ8NoWyGtoEfUwoCyMT0fU= git.openprivacy.ca/openprivacy/connectivity v1.4.3 h1:i2Ad/U9FlL9dKr2bhRck7lJ8NoWyGtoEfUwoCyMT0fU=
git.openprivacy.ca/openprivacy/connectivity v1.4.3/go.mod h1:bR0Myx9nm2YzWtsThRelkNMV4Pp7sPDa123O1qsAbVo= git.openprivacy.ca/openprivacy/connectivity v1.4.3/go.mod h1:bR0Myx9nm2YzWtsThRelkNMV4Pp7sPDa123O1qsAbVo=
git.openprivacy.ca/openprivacy/connectivity v1.4.5 h1:UYMdCWPzEAP7LbqdMXGNXmfKjWlvfnKdmewBtnbgQRI=
git.openprivacy.ca/openprivacy/connectivity v1.4.5/go.mod h1:JVRCIdL+lAG6ohBFWiKeC/MN42nnC0sfFszR9XG6vPQ=
git.openprivacy.ca/openprivacy/connectivity v1.5.0 h1:ZxsR/ZaVKXIkD2x6FlajZn62ciNQjamrI4i/5xIpdoQ=
git.openprivacy.ca/openprivacy/connectivity v1.5.0/go.mod h1:UjQiGBnWbotmBzIw59B8H6efwDadjkKzm3RPT1UaIRw=
git.openprivacy.ca/openprivacy/log v1.0.1/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw= git.openprivacy.ca/openprivacy/log v1.0.1/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
git.openprivacy.ca/openprivacy/log v1.0.2 h1:HLP4wsw4ljczFAelYnbObIs821z+jgMPCe8uODPnGQM= git.openprivacy.ca/openprivacy/log v1.0.2 h1:HLP4wsw4ljczFAelYnbObIs821z+jgMPCe8uODPnGQM=
git.openprivacy.ca/openprivacy/log v1.0.2/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw= git.openprivacy.ca/openprivacy/log v1.0.2/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
git.openprivacy.ca/openprivacy/log v1.0.3 h1:E/PMm4LY+Q9s3aDpfySfEDq/vYQontlvNj/scrPaga0=
git.openprivacy.ca/openprivacy/log v1.0.3/go.mod h1:gGYK8xHtndRLDymFtmjkG26GaMQNgyhioNS82m812Iw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

136
server.go
View File

@ -3,6 +3,8 @@ package server
import ( import (
"crypto/ed25519" "crypto/ed25519"
"cwtch.im/cwtch/model" "cwtch.im/cwtch/model"
"encoding/base64"
"errors"
"fmt" "fmt"
"git.openprivacy.ca/cwtch.im/server/metrics" "git.openprivacy.ca/cwtch.im/server/metrics"
"git.openprivacy.ca/cwtch.im/server/storage" "git.openprivacy.ca/cwtch.im/server/storage"
@ -15,14 +17,35 @@ import (
"git.openprivacy.ca/openprivacy/connectivity" "git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/connectivity/tor" "git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log" "git.openprivacy.ca/openprivacy/log"
"os"
"path" "path"
"sync" "sync"
) )
const (
// ServerConfigFile is the standard filename for a server's config to be written to in a directory
ServerConfigFile = "serverConfig.json"
)
// Server encapsulates a complete, compliant Cwtch server. // Server encapsulates a complete, compliant Cwtch server.
type Server struct { type Server interface {
Identity() primitives.Identity
Run(acn connectivity.ACN) error
KeyBundle() *model.KeyBundle
CheckStatus() (bool, error)
Shutdown()
GetStatistics() Statistics
Delete(password string) error
Onion() string
ServerBundle() string
TofuBundle() string
GetAttribute(string) string
SetAttribute(string, string)
}
type server struct {
service tapir.Service service tapir.Service
config Config config *Config
metricsPack metrics.Monitors metricsPack metrics.Monitors
tokenTapirService tapir.Service tokenTapirService tapir.Service
tokenServer *privacypass.TokenServer tokenServer *privacypass.TokenServer
@ -35,31 +58,40 @@ type Server struct {
lock sync.RWMutex lock sync.RWMutex
} }
// Setup initialized a server from a given configuration // NewServer creates and configures a new server based on the supplied configuration
func (s *Server) Setup(serverConfig Config) { func NewServer(serverConfig *Config) Server {
s.config = serverConfig server := new(server)
bs := new(persistence.BoltPersistence) server.running = false
bs.Open(path.Join(serverConfig.ConfigDir, "tokens.db")) server.config = serverConfig
s.tokenServer = privacypass.NewTokenServerFromStore(&serverConfig.TokenServiceK, bs) server.tokenService = server.config.TokenServiceIdentity()
log.Infof("Y: %v", s.tokenServer.Y) server.tokenServicePrivKey = server.config.TokenServerPrivateKey
s.tokenService = s.config.TokenServiceIdentity() return server
s.tokenServicePrivKey = s.config.TokenServerPrivateKey
} }
// Identity returns the main onion identity of the server // Identity returns the main onion identity of the server
func (s *Server) Identity() primitives.Identity { func (s *server) Identity() primitives.Identity {
return s.config.Identity() return s.config.Identity()
} }
// Run starts a server with the given privateKey // Run starts a server with the given privateKey
func (s *Server) Run(acn connectivity.ACN) error { func (s *server) Run(acn connectivity.ACN) error {
addressIdentity := tor.GetTorV3Hostname(s.config.PublicKey) s.lock.Lock()
defer s.lock.Unlock()
if s.running {
return nil
}
bs := new(persistence.BoltPersistence)
bs.Open(path.Join(s.config.ConfigDir, "tokens.db"))
s.tokenServer = privacypass.NewTokenServerFromStore(&s.config.TokenServiceK, bs)
log.Infof("Y: %v", s.tokenServer.Y)
identity := primitives.InitializeIdentity("", &s.config.PrivateKey, &s.config.PublicKey) identity := primitives.InitializeIdentity("", &s.config.PrivateKey, &s.config.PublicKey)
var service tapir.Service var service tapir.Service
service = new(tor2.BaseOnionService) service = new(tor2.BaseOnionService)
service.Init(acn, s.config.PrivateKey, &identity) service.Init(acn, s.config.PrivateKey, &identity)
s.service = service s.service = service
log.Infof("cwtch server running on cwtch:%s\n", addressIdentity+".onion:") log.Infof("cwtch server running on cwtch:%s\n", s.Onion())
s.metricsPack.Start(service, s.config.ConfigDir, s.config.ServerReporting.LogMetricsToFile) s.metricsPack.Start(service, s.config.ConfigDir, s.config.ServerReporting.LogMetricsToFile)
ms, err := storage.InitializeSqliteMessageStore(path.Join(s.config.ConfigDir, "cwtch.messages"), s.metricsPack.MessageCounter) ms, err := storage.InitializeSqliteMessageStore(path.Join(s.config.ConfigDir, "cwtch.messages"), s.metricsPack.MessageCounter)
@ -87,14 +119,13 @@ func (s *Server) Run(acn connectivity.ACN) error {
s.onionServiceStopped = true s.onionServiceStopped = true
}() }()
s.lock.Lock()
s.running = true s.running = true
s.lock.Unlock() s.SetAttribute(AttrEnabled, "true")
return nil return nil
} }
// KeyBundle provides the signed keybundle of the server // KeyBundle provides the signed keybundle of the server
func (s *Server) KeyBundle() *model.KeyBundle { func (s *server) KeyBundle() *model.KeyBundle {
kb := model.NewKeyBundle() kb := model.NewKeyBundle()
identity := s.config.Identity() identity := s.config.Identity()
kb.Keys[model.KeyTypeServerOnion] = model.Key(identity.Hostname()) kb.Keys[model.KeyTypeServerOnion] = model.Key(identity.Hostname())
@ -105,7 +136,7 @@ func (s *Server) KeyBundle() *model.KeyBundle {
} }
// CheckStatus returns true if the server is running and/or an error if any part of the server needs to be restarted. // CheckStatus returns true if the server is running and/or an error if any part of the server needs to be restarted.
func (s *Server) CheckStatus() (bool, error) { func (s *server) CheckStatus() (bool, error) {
s.lock.RLock() s.lock.RLock()
defer s.lock.RUnlock() defer s.lock.RUnlock()
if s.onionServiceStopped == true || s.tokenServiceStopped == true { if s.onionServiceStopped == true || s.tokenServiceStopped == true {
@ -115,14 +146,19 @@ func (s *Server) CheckStatus() (bool, error) {
} }
// Shutdown kills the app closing all connections and freeing all goroutines // Shutdown kills the app closing all connections and freeing all goroutines
func (s *Server) Shutdown() { func (s *server) Shutdown() {
log.Infof("Shutting down server")
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
s.service.Shutdown() if s.running {
s.tokenTapirService.Shutdown() s.service.Shutdown()
s.metricsPack.Stop() s.tokenTapirService.Shutdown()
s.running = true log.Infof("Closing Token server Database...")
s.tokenServer.Close()
s.metricsPack.Stop()
s.running = false
s.SetAttribute(AttrEnabled, "false")
}
} }
// Statistics is an encapsulation of information about the server that an operator might want to know at a glance. // Statistics is an encapsulation of information about the server that an operator might want to know at a glance.
@ -132,7 +168,7 @@ type Statistics struct {
// GetStatistics is a stub method for providing some high level information about // GetStatistics is a stub method for providing some high level information about
// the server operation to bundling applications (e.g. the UI) // the server operation to bundling applications (e.g. the UI)
func (s *Server) GetStatistics() Statistics { func (s *server) GetStatistics() Statistics {
// TODO Statistics from Metrics is very awkward. Metrics needs an overhaul to make safe // TODO Statistics from Metrics is very awkward. Metrics needs an overhaul to make safe
total := s.existingMessageCount total := s.existingMessageCount
if s.metricsPack.TotalMessageCounter != nil { if s.metricsPack.TotalMessageCounter != nil {
@ -144,17 +180,43 @@ func (s *Server) GetStatistics() Statistics {
} }
} }
// ConfigureAutostart sets whether this server should autostart (in the Cwtch UI/bundling application) func (s *server) Delete(password string) error {
func (s *Server) ConfigureAutostart(autostart bool) {
s.config.AutoStart = autostart
s.config.Save(s.config.ConfigDir, s.config.FilePath)
}
// Close shuts down the cwtch server in a safe way.
func (s *Server) Close() {
log.Infof("Shutting down server")
s.lock.Lock() s.lock.Lock()
defer s.lock.Unlock() defer s.lock.Unlock()
log.Infof("Closing Token Server Database...") if s.config.Encrypted && !s.config.CheckPassword(password) {
s.tokenServer.Close() return errors.New("Cannot delete server, passwords do not match")
}
os.RemoveAll(s.config.ConfigDir)
return nil
}
func (s *server) Onion() string {
return s.config.Onion()
}
// ServerBundle returns a bundle of the server keys required to access it (torv3 keys are addresses)
func (s *server) ServerBundle() string {
bundle := s.KeyBundle().Serialize()
return fmt.Sprintf("server:%s", base64.StdEncoding.EncodeToString(bundle))
}
// TofuBundle returns a Server Bundle + a newly created group invite
func (s *server) TofuBundle() string {
group, _ := model.NewGroup(tor.GetTorV3Hostname(s.config.PublicKey))
invite, err := group.Invite()
if err != nil {
panic(err)
}
bundle := s.KeyBundle().Serialize()
return fmt.Sprintf("tofubundle:server:%s||%s", base64.StdEncoding.EncodeToString(bundle), invite)
}
// GetAttribute gets a server attribute
func (s *server) GetAttribute(key string) string {
return s.config.GetAttribute(key)
}
// SetAttribute sets a server attribute
func (s *server) SetAttribute(key, val string) {
s.config.SetAttribute(key, val)
} }

View File

@ -2,13 +2,45 @@ package server
import ( import (
"crypto/rand" "crypto/rand"
v1 "cwtch.im/cwtch/storage/v1"
"encoding/json" "encoding/json"
"git.openprivacy.ca/cwtch.im/tapir/primitives" "git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/connectivity/tor"
"git.openprivacy.ca/openprivacy/log" "git.openprivacy.ca/openprivacy/log"
"github.com/gtank/ristretto255" "github.com/gtank/ristretto255"
"golang.org/x/crypto/ed25519" "golang.org/x/crypto/ed25519"
"io/ioutil" "io/ioutil"
"os"
"path" "path"
"sync"
)
const (
// SaltFile is the standard filename to store an encrypted config's SALT under beside it
SaltFile = "SALT"
// AttrAutostart is the attribute key for autostart setting
AttrAutostart = "autostart"
// AttrDescription is the attribute key for a user set server description
AttrDescription = "description"
// AttrEnabled is the attribute key for user toggle of server being enabled
AttrEnabled = "enabled"
// AttrStorageType is used by clients that may need info about stored server config types/styles
AttrStorageType = "storageType"
)
const (
// StorageTypeDefaultPassword is a AttrStorageType that indicated a app default password was used
StorageTypeDefaultPassword = "storage-default-password"
// StorageTypePassword is a AttrStorageType that indicated a user password was used to protect the profile
StorageTypePassword = "storage-password"
// StoreageTypeNoPassword is a AttrStorageType that indicated a no password was used to protect the profile
StoreageTypeNoPassword = "storage-no-password"
) )
// Reporting is a struct for storing a the config a server needs to be a peer, and connect to a group to report // Reporting is a struct for storing a the config a server needs to be a peer, and connect to a group to report
@ -22,7 +54,9 @@ type Reporting struct {
type Config struct { type Config struct {
ConfigDir string `json:"-"` ConfigDir string `json:"-"`
FilePath string `json:"-"` FilePath string `json:"-"`
MaxBufferLines int `json:"maxBufferLines"` Encrypted bool `json:"-"`
key [32]byte
MaxBufferLines int `json:"maxBufferLines"`
PublicKey ed25519.PublicKey `json:"publicKey"` PublicKey ed25519.PublicKey `json:"publicKey"`
PrivateKey ed25519.PrivateKey `json:"privateKey"` PrivateKey ed25519.PrivateKey `json:"privateKey"`
@ -33,7 +67,11 @@ type Config struct {
TokenServiceK ristretto255.Scalar `json:"tokenServiceK"` TokenServiceK ristretto255.Scalar `json:"tokenServiceK"`
ServerReporting Reporting `json:"serverReporting"` ServerReporting Reporting `json:"serverReporting"`
AutoStart bool `json:"autostart"`
Attributes map[string]string `json:"attributes"`
lock sync.Mutex
encFileStore v1.FileStore
} }
// Identity returns an encapsulation of the servers keys // Identity returns an encapsulation of the servers keys
@ -46,17 +84,8 @@ func (config *Config) TokenServiceIdentity() primitives.Identity {
return primitives.InitializeIdentity("", &config.TokenServerPrivateKey, &config.TokenServerPublicKey) return primitives.InitializeIdentity("", &config.TokenServerPrivateKey, &config.TokenServerPublicKey)
} }
// Save dumps the latest version of the config to a json file given by filename func initDefaultConfig(configDir, filename string, encrypted bool) *Config {
func (config *Config) Save(dir, filename string) { config := &Config{Encrypted: encrypted, ConfigDir: configDir, FilePath: filename, Attributes: make(map[string]string)}
log.Infof("Saving config to %s\n", path.Join(dir, filename))
bytes, _ := json.MarshalIndent(config, "", "\t")
ioutil.WriteFile(path.Join(dir, filename), bytes, 0600)
}
// LoadConfig loads a Config from a json file specified by filename
func LoadConfig(configDir, filename string) Config {
log.Infof("Loading config from %s\n", path.Join(configDir, filename))
config := Config{}
id, pk := primitives.InitializeEphemeralIdentity() id, pk := primitives.InitializeEphemeralIdentity()
tid, tpk := primitives.InitializeEphemeralIdentity() tid, tpk := primitives.InitializeEphemeralIdentity()
@ -70,9 +99,8 @@ func LoadConfig(configDir, filename string) Config {
ReportingGroupID: "", ReportingGroupID: "",
ReportingServerAddr: "", ReportingServerAddr: "",
} }
config.AutoStart = false config.Attributes[AttrAutostart] = "false"
config.ConfigDir = configDir config.Attributes[AttrEnabled] = "true"
config.FilePath = filename
k := new(ristretto255.Scalar) k := new(ristretto255.Scalar)
b := make([]byte, 64) b := make([]byte, 64)
@ -83,16 +111,113 @@ func LoadConfig(configDir, filename string) Config {
} }
k.FromUniformBytes(b) k.FromUniformBytes(b)
config.TokenServiceK = *k config.TokenServiceK = *k
raw, err := ioutil.ReadFile(path.Join(configDir, filename))
if err == nil {
err = json.Unmarshal(raw, &config)
if err != nil {
log.Errorf("reading config: %v", err)
}
}
// Always save (first time generation, new version with new variables populated)
config.Save(configDir, filename)
return config return config
} }
// LoadCreateDefaultConfigFile loads a Config from or creates a default config and saves it to a json file specified by filename
// if the encrypted flag is true the config is store encrypted by password
func LoadCreateDefaultConfigFile(configDir, filename string, encrypted bool, password string) (*Config, error) {
if _, err := os.Stat(path.Join(configDir, filename)); os.IsNotExist(err) {
return CreateConfig(configDir, filename, encrypted, password)
}
return LoadConfig(configDir, filename, encrypted, password)
}
// CreateConfig creates a default config and saves it to a json file specified by filename
// if the encrypted flag is true the config is store encrypted by password
func CreateConfig(configDir, filename string, encrypted bool, password string) (*Config, error) {
log.Debugf("CreateConfig for server with configDir: %s\n", configDir)
os.Mkdir(configDir, 0700)
config := initDefaultConfig(configDir, filename, encrypted)
if encrypted {
key, _, err := v1.InitV1Directory(configDir, password)
if err != nil {
log.Errorf("could not create server directory: %s", err)
return nil, err
}
config.key = key
config.encFileStore = v1.NewFileStore(configDir, ServerConfigFile, key)
}
config.Save()
return config, nil
}
// LoadConfig loads a Config from a json file specified by filename
func LoadConfig(configDir, filename string, encrypted bool, password string) (*Config, error) {
config := initDefaultConfig(configDir, filename, encrypted)
var raw []byte
var err error
if encrypted {
salt, err := ioutil.ReadFile(path.Join(configDir, SaltFile))
if err != nil {
return nil, err
}
key := v1.CreateKey(password, salt)
config.encFileStore = v1.NewFileStore(configDir, ServerConfigFile, key)
raw, err = config.encFileStore.Read()
if err != nil {
log.Errorf("read enc bytes failed: %s\n", err)
return nil, err
}
} else {
raw, err = ioutil.ReadFile(path.Join(configDir, filename))
if err != nil {
return nil, err
}
}
if err = json.Unmarshal(raw, &config); err != nil {
log.Errorf("reading config: %v", err)
return nil, err
}
// Always save (first time generation, new version with new variables populated)
config.Save()
return config, nil
}
// Save dumps the latest version of the config to a json file given by filename
func (config *Config) Save() error {
config.lock.Lock()
defer config.lock.Unlock()
bytes, _ := json.MarshalIndent(config, "", "\t")
if config.Encrypted {
return config.encFileStore.Write(bytes)
}
return ioutil.WriteFile(path.Join(config.ConfigDir, config.FilePath), bytes, 0600)
}
// CheckPassword returns true if the given password produces the same key as the current stored key, otherwise false.
func (config *Config) CheckPassword(checkpass string) bool {
config.lock.Lock()
defer config.lock.Unlock()
salt, err := ioutil.ReadFile(path.Join(config.ConfigDir, SaltFile))
if err != nil {
return false
}
oldkey := v1.CreateKey(checkpass, salt[:])
return oldkey == config.key
}
// Onion returns the .onion url for the server
func (config *Config) Onion() string {
config.lock.Lock()
defer config.lock.Unlock()
return tor.GetTorV3Hostname(config.PublicKey) + ".onion"
}
// SetAttribute sets a server attribute
func (config *Config) SetAttribute(key, val string) {
config.lock.Lock()
config.Attributes[key] = val
config.lock.Unlock()
config.Save()
}
// GetAttribute gets a server attribute
func (config *Config) GetAttribute(key string) string {
config.lock.Lock()
defer config.lock.Unlock()
return config.Attributes[key]
}

View File

@ -50,14 +50,14 @@ func (ta *TokenboardServer) Listen() {
for { for {
data := ta.connection.Expect() data := ta.connection.Expect()
if len(data) == 0 { if len(data) == 0 {
log.Debugf("Server Closing Connection") log.Debugf("server Closing Connection")
ta.connection.Close() ta.connection.Close()
return // connection is closed return // connection is closed
} }
var message groups.Message var message groups.Message
if err := json.Unmarshal(data, &message); err != nil { if err := json.Unmarshal(data, &message); err != nil {
log.Debugf("Server Closing Connection Because of Malformed Client Packet %v", err) log.Debugf("server Closing Connection Because of Malformed Client Packet %v", err)
ta.connection.Close() ta.connection.Close()
return // connection is closed return // connection is closed
} }
@ -69,7 +69,7 @@ func (ta *TokenboardServer) Listen() {
log.Debugf("Received a Post Message Request: %v", ta.connection.Hostname()) log.Debugf("Received a Post Message Request: %v", ta.connection.Hostname())
ta.postMessageRequest(postrequest) ta.postMessageRequest(postrequest)
} else { } else {
log.Debugf("Server Closing Connection Because of PostRequestMessage Client Packet") log.Debugf("server Closing Connection Because of PostRequestMessage Client Packet")
ta.connection.Close() ta.connection.Close()
return // connection is closed return // connection is closed
} }
@ -97,7 +97,7 @@ func (ta *TokenboardServer) Listen() {
ta.connection.Send(data) ta.connection.Send(data)
} }
} else { } else {
log.Debugf("Server Closing Connection Because of Malformed ReplayRequestMessage Packet") log.Debugf("server Closing Connection Because of Malformed ReplayRequestMessage Packet")
ta.connection.Close() ta.connection.Close()
return // connection is closed return // connection is closed
} }

139
servers.go Normal file
View File

@ -0,0 +1,139 @@
package server
import (
"cwtch.im/cwtch/model"
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"io/ioutil"
"path"
"sync"
)
// Servers is an interface to manage multiple Cwtch servers
// Unlike a standalone server, server's dirs will be under one "$CwtchDir/servers" and use a cwtch style localID to obscure
// what servers are hosted. Users are of course free to use a default password. This means Config file will be encrypted
// with cwtch/storage/v1/file_enc and monitor files will not be generated
type Servers interface {
LoadServers(password string) ([]string, error)
CreateServer(password string) (Server, error)
GetServer(onion string) Server
ListServers() []string
DeleteServer(onion string, currentPassword string) error
LaunchServer(string)
ShutdownServer(string)
Shutdown()
}
type servers struct {
lock sync.Mutex
servers map[string]Server
directory string
acn connectivity.ACN
}
// NewServers returns a Servers interface to manage a collection of servers
// expecting directory: $CWTCH_HOME/servers
func NewServers(acn connectivity.ACN, directory string) Servers {
return &servers{acn: acn, directory: directory, servers: make(map[string]Server)}
}
// LoadServers will attempt to load any servers in the servers directory that are encrypted with the supplied password
// returns a list of onions identifiers for servers loaded or an error
func (s *servers) LoadServers(password string) ([]string, error) {
s.lock.Lock()
defer s.lock.Unlock()
dirs, err := ioutil.ReadDir(s.directory)
if err != nil {
return nil, fmt.Errorf("error: cannot read server directory: %v", err)
}
loadedServers := []string{}
for _, dir := range dirs {
newConfig, err := LoadConfig(path.Join(s.directory, dir.Name()), ServerConfigFile, true, password)
if err == nil {
if _, exists := s.servers[newConfig.Onion()]; !exists {
log.Debugf("Loaded config, building server for %s\n", newConfig.Onion())
server := NewServer(newConfig)
s.servers[server.Onion()] = server
loadedServers = append(loadedServers, server.Onion())
}
}
}
log.Infof("LoadServers returning: %s\n", loadedServers)
return loadedServers, nil
}
// CreateServer creates a new server and stores it, also returns an interface to it
func (s *servers) CreateServer(password string) (Server, error) {
newLocalID := model.GenerateRandomID()
directory := path.Join(s.directory, newLocalID)
config, err := CreateConfig(directory, ServerConfigFile, true, password)
if err != nil {
return nil, err
}
server := NewServer(config)
s.lock.Lock()
defer s.lock.Unlock()
s.servers[server.Onion()] = server
return server, nil
}
// GetServer returns a server interface for the supplied onion
func (s *servers) GetServer(onion string) Server {
s.lock.Lock()
defer s.lock.Unlock()
return s.servers[onion]
}
// ListServers returns a list of server onion identifies this servers struct is managing
func (s *servers) ListServers() []string {
s.lock.Lock()
defer s.lock.Unlock()
list := []string{}
for onion := range s.servers {
list = append(list, onion)
}
return list
}
// DeleteServer delete's the requested server (assuming the passwords match
func (s *servers) DeleteServer(onion string, password string) error {
s.lock.Lock()
defer s.lock.Unlock()
server := s.servers[onion]
if server != nil {
server.Shutdown()
err := server.Delete(password)
delete(s.servers, onion)
return err
}
return errors.New("server not found")
}
// LaunchServer Run() the specified server
func (s *servers) LaunchServer(onion string) {
s.lock.Lock()
defer s.lock.Unlock()
if server, exists := s.servers[onion]; exists {
server.Run(s.acn)
}
}
// ShutdownServer Shutsdown the specified server
func (s *servers) ShutdownServer(onion string) {
s.lock.Lock()
defer s.lock.Unlock()
s.servers[onion].Shutdown()
}
// Shutdown shutsdown all the servers
func (s *servers) Shutdown() {
s.lock.Lock()
defer s.lock.Unlock()
for _, server := range s.servers {
server.Shutdown()
}
}

59
servers_test.go Normal file
View File

@ -0,0 +1,59 @@
package server
import (
"git.openprivacy.ca/openprivacy/connectivity"
"git.openprivacy.ca/openprivacy/log"
"os"
"testing"
)
const TestDir = "./serversTest"
const DefaultPassword = "be gay do crime"
const TestServerDesc = "a test Server"
func TestServers(t *testing.T) {
log.SetLevel(log.LevelDebug)
log.Infof("clean up / setup...\n")
os.RemoveAll(TestDir)
os.Mkdir(TestDir, 0700)
acn := connectivity.NewLocalACN()
log.Infof("NewServers()...\n")
servers := NewServers(acn, TestDir)
s, err := servers.CreateServer(DefaultPassword)
if err != nil {
t.Errorf("could not create server: %s", err)
return
}
s.SetAttribute(AttrDescription, TestServerDesc)
serverOnion := s.Onion()
s.Shutdown()
log.Infof("NewServers()...\n" )
servers2 := NewServers(acn, TestDir)
log.Infof("LoadServers()...\n")
list, err := servers2.LoadServers(DefaultPassword)
log.Infof("Loaded!\n")
if err != nil {
t.Errorf("clould not load server: %s", err)
return
}
if len(list) != 1 {
t.Errorf("expected to load 1 server, got %d", len(list))
return
}
if list[0] != serverOnion {
t.Errorf("expected loaded server to have onion: %s but got %s", serverOnion, list[0])
}
s1 := servers.GetServer(list[0])
if s1.GetAttribute(AttrDescription) != TestServerDesc {
t.Errorf("expected server description of '%s' but got '%s'", TestServerDesc, s1.GetAttribute(AttrDescription))
}
servers2.Shutdown()
os.RemoveAll(TestDir)
}

View File

@ -2,10 +2,10 @@ package storage
import ( import (
"cwtch.im/cwtch/protocol/groups" "cwtch.im/cwtch/protocol/groups"
"git.openprivacy.ca/cwtch.im/server/metrics"
"database/sql" "database/sql"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"git.openprivacy.ca/cwtch.im/server/metrics"
"git.openprivacy.ca/openprivacy/log" "git.openprivacy.ca/openprivacy/log"
_ "github.com/mattn/go-sqlite3" // sqlite3 driver _ "github.com/mattn/go-sqlite3" // sqlite3 driver
) )

View File

@ -2,8 +2,8 @@ package storage
import ( import (
"cwtch.im/cwtch/protocol/groups" "cwtch.im/cwtch/protocol/groups"
"git.openprivacy.ca/cwtch.im/server/metrics"
"encoding/binary" "encoding/binary"
"git.openprivacy.ca/cwtch.im/server/metrics"
"git.openprivacy.ca/openprivacy/log" "git.openprivacy.ca/openprivacy/log"
"os" "os"
"testing" "testing"

View File

@ -4,7 +4,8 @@ set -e
pwd pwd
GORACE="haltonerror=1" GORACE="haltonerror=1"
go test -race ${1} -coverprofile=server.metrics.cover.out -v ./metrics go test -race ${1} -coverprofile=server.metrics.cover.out -v ./metrics
go test -race ${1} -coverprofile=server.metrics.cover.out -v ./storage go test -race ${1} -coverprofile=server.storage.cover.out -v ./storage
go test -race ${1} -coverprofile=server.cover.out -v ./
echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \ echo "mode: set" > coverage.out && cat *.cover.out | grep -v mode: | sort -r | \
awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out
rm -rf *.cover.out rm -rf *.cover.out