forked from cwtch.im/server
Merge pull request 'adding 'servers' interface to manage multiple servers and support for encrypted configs' (#16) from servers into trunk
Reviewed-on: cwtch.im/server#16
This commit is contained in:
commit
c148e120f3
|
@ -8,12 +8,14 @@
|
|||
|
||||
The app takes the following arguments
|
||||
- -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
|
||||
- CWTCH_HOME: sets the config dir for the app
|
||||
|
||||
`env CONFIG_HOME=./conf ./app`
|
||||
|
||||
## 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
|
||||
|
|
46
app/main.go
46
app/main.go
|
@ -2,10 +2,8 @@ package main
|
|||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"cwtch.im/cwtch/model"
|
||||
"encoding/base64"
|
||||
"flag"
|
||||
"fmt"
|
||||
cwtchserver "git.openprivacy.ca/cwtch.im/server"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||
|
@ -19,13 +17,9 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
serverConfigFile = "serverConfig.json"
|
||||
)
|
||||
|
||||
func main() {
|
||||
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()
|
||||
|
||||
log.AddEverythingFromPattern("server/app/main")
|
||||
|
@ -52,19 +46,25 @@ func main() {
|
|||
ReportingGroupID: "",
|
||||
ReportingServerAddr: "",
|
||||
}
|
||||
config.Save(".", "serverConfig.json")
|
||||
config.ConfigDir = "."
|
||||
config.FilePath = cwtchserver.ServerConfigFile
|
||||
config.Encrypted = false
|
||||
config.Save()
|
||||
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...
|
||||
mrand.Seed(int64(time.Now().Nanosecond()))
|
||||
controlPort := mrand.Intn(1000) + 9052
|
||||
|
||||
// generate a random password
|
||||
key := make([]byte, 64)
|
||||
_, err := rand.Read(key)
|
||||
_, err = rand.Read(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -79,25 +79,15 @@ func main() {
|
|||
}
|
||||
defer acn.Close()
|
||||
|
||||
server := new(cwtchserver.Server)
|
||||
server := cwtchserver.NewServer(serverConfig)
|
||||
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
|
||||
group, _ := model.NewGroup(tor.GetTorV3Hostname(serverConfig.PublicKey))
|
||||
invite, err := group.Invite()
|
||||
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)
|
||||
if *flagExportServer {
|
||||
// Todo: change all to server export
|
||||
ioutil.WriteFile(path.Join(serverConfig.ConfigDir, "serverbundle"), []byte(server.TofuBundle()), 0600)
|
||||
}
|
||||
|
||||
// Graceful Shutdown
|
||||
|
@ -105,8 +95,8 @@ func main() {
|
|||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-c
|
||||
server.Shutdown()
|
||||
acn.Close()
|
||||
server.Close()
|
||||
os.Exit(1)
|
||||
}()
|
||||
|
||||
|
|
|
@ -64,5 +64,5 @@ COPY ./docker/docker-entrypoint /usr/local/bin/
|
|||
VOLUME /etc/tor /var/lib/tor /var/lib/cwtch
|
||||
|
||||
ENTRYPOINT ["docker-entrypoint"]
|
||||
CMD ["/usr/local/bin/cwtch","--exportTofuBundle"]
|
||||
CMD ["/usr/local/bin/cwtch","--exportServerBundle"]
|
||||
|
||||
|
|
8
go.mod
8
go.mod
|
@ -3,10 +3,10 @@ module git.openprivacy.ca/cwtch.im/server
|
|||
go 1.14
|
||||
|
||||
require (
|
||||
cwtch.im/cwtch v0.8.5
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.4.2
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.4.3
|
||||
git.openprivacy.ca/openprivacy/log v1.0.2
|
||||
cwtch.im/cwtch v0.12.2
|
||||
git.openprivacy.ca/cwtch.im/tapir v0.4.9
|
||||
git.openprivacy.ca/openprivacy/connectivity v1.5.0
|
||||
git.openprivacy.ca/openprivacy/log v1.0.3
|
||||
github.com/gtank/ristretto255 v0.1.2
|
||||
github.com/mattn/go-sqlite3 v1.14.7
|
||||
github.com/struCoder/pidusage v0.2.1
|
||||
|
|
14
go.sum
14
go.sum
|
@ -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.5 h1:W67jAF2oRwqWytbZEv1UeCqW0cU2x69tgUw8iy27xFA=
|
||||
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/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/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/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/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/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.2 h1:HLP4wsw4ljczFAelYnbObIs821z+jgMPCe8uODPnGQM=
|
||||
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
|
130
server.go
130
server.go
|
@ -3,6 +3,8 @@ package server
|
|||
import (
|
||||
"crypto/ed25519"
|
||||
"cwtch.im/cwtch/model"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/cwtch.im/server/metrics"
|
||||
"git.openprivacy.ca/cwtch.im/server/storage"
|
||||
|
@ -15,14 +17,35 @@ import (
|
|||
"git.openprivacy.ca/openprivacy/connectivity"
|
||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"os"
|
||||
"path"
|
||||
"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.
|
||||
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
|
||||
config Config
|
||||
config *Config
|
||||
metricsPack metrics.Monitors
|
||||
tokenTapirService tapir.Service
|
||||
tokenServer *privacypass.TokenServer
|
||||
|
@ -35,31 +58,40 @@ type Server struct {
|
|||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// Setup initialized a server from a given configuration
|
||||
func (s *Server) Setup(serverConfig Config) {
|
||||
s.config = serverConfig
|
||||
bs := new(persistence.BoltPersistence)
|
||||
bs.Open(path.Join(serverConfig.ConfigDir, "tokens.db"))
|
||||
s.tokenServer = privacypass.NewTokenServerFromStore(&serverConfig.TokenServiceK, bs)
|
||||
log.Infof("Y: %v", s.tokenServer.Y)
|
||||
s.tokenService = s.config.TokenServiceIdentity()
|
||||
s.tokenServicePrivKey = s.config.TokenServerPrivateKey
|
||||
// NewServer creates and configures a new server based on the supplied configuration
|
||||
func NewServer(serverConfig *Config) Server {
|
||||
server := new(server)
|
||||
server.running = false
|
||||
server.config = serverConfig
|
||||
server.tokenService = server.config.TokenServiceIdentity()
|
||||
server.tokenServicePrivKey = server.config.TokenServerPrivateKey
|
||||
return 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()
|
||||
}
|
||||
|
||||
// Run starts a server with the given privateKey
|
||||
func (s *Server) Run(acn connectivity.ACN) error {
|
||||
addressIdentity := tor.GetTorV3Hostname(s.config.PublicKey)
|
||||
func (s *server) Run(acn connectivity.ACN) error {
|
||||
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)
|
||||
var service tapir.Service
|
||||
service = new(tor2.BaseOnionService)
|
||||
service.Init(acn, s.config.PrivateKey, &identity)
|
||||
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)
|
||||
|
||||
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.lock.Lock()
|
||||
s.running = true
|
||||
s.lock.Unlock()
|
||||
s.SetAttribute(AttrEnabled, "true")
|
||||
return nil
|
||||
}
|
||||
|
||||
// KeyBundle provides the signed keybundle of the server
|
||||
func (s *Server) KeyBundle() *model.KeyBundle {
|
||||
func (s *server) KeyBundle() *model.KeyBundle {
|
||||
kb := model.NewKeyBundle()
|
||||
identity := s.config.Identity()
|
||||
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.
|
||||
func (s *Server) CheckStatus() (bool, error) {
|
||||
func (s *server) CheckStatus() (bool, error) {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
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
|
||||
func (s *Server) Shutdown() {
|
||||
func (s *server) Shutdown() {
|
||||
log.Infof("Shutting down server")
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
if s.running {
|
||||
s.service.Shutdown()
|
||||
s.tokenTapirService.Shutdown()
|
||||
log.Infof("Closing Token server Database...")
|
||||
s.tokenServer.Close()
|
||||
s.metricsPack.Stop()
|
||||
s.running = true
|
||||
|
||||
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.
|
||||
|
@ -132,7 +168,7 @@ type Statistics struct {
|
|||
|
||||
// GetStatistics is a stub method for providing some high level information about
|
||||
// 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
|
||||
total := s.existingMessageCount
|
||||
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) 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")
|
||||
func (s *server) Delete(password string) error {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
log.Infof("Closing Token Server Database...")
|
||||
s.tokenServer.Close()
|
||||
if s.config.Encrypted && !s.config.CheckPassword(password) {
|
||||
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)
|
||||
}
|
||||
|
|
177
serverConfig.go
177
serverConfig.go
|
@ -2,13 +2,45 @@ package server
|
|||
|
||||
import (
|
||||
"crypto/rand"
|
||||
v1 "cwtch.im/cwtch/storage/v1"
|
||||
"encoding/json"
|
||||
"git.openprivacy.ca/cwtch.im/tapir/primitives"
|
||||
"git.openprivacy.ca/openprivacy/connectivity/tor"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"github.com/gtank/ristretto255"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"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
|
||||
|
@ -22,6 +54,8 @@ type Reporting struct {
|
|||
type Config struct {
|
||||
ConfigDir string `json:"-"`
|
||||
FilePath string `json:"-"`
|
||||
Encrypted bool `json:"-"`
|
||||
key [32]byte
|
||||
MaxBufferLines int `json:"maxBufferLines"`
|
||||
|
||||
PublicKey ed25519.PublicKey `json:"publicKey"`
|
||||
|
@ -33,7 +67,11 @@ type Config struct {
|
|||
TokenServiceK ristretto255.Scalar `json:"tokenServiceK"`
|
||||
|
||||
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
|
||||
|
@ -46,17 +84,8 @@ func (config *Config) TokenServiceIdentity() primitives.Identity {
|
|||
return primitives.InitializeIdentity("", &config.TokenServerPrivateKey, &config.TokenServerPublicKey)
|
||||
}
|
||||
|
||||
// Save dumps the latest version of the config to a json file given by filename
|
||||
func (config *Config) Save(dir, filename 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{}
|
||||
func initDefaultConfig(configDir, filename string, encrypted bool) *Config {
|
||||
config := &Config{Encrypted: encrypted, ConfigDir: configDir, FilePath: filename, Attributes: make(map[string]string)}
|
||||
|
||||
id, pk := primitives.InitializeEphemeralIdentity()
|
||||
tid, tpk := primitives.InitializeEphemeralIdentity()
|
||||
|
@ -70,9 +99,8 @@ func LoadConfig(configDir, filename string) Config {
|
|||
ReportingGroupID: "",
|
||||
ReportingServerAddr: "",
|
||||
}
|
||||
config.AutoStart = false
|
||||
config.ConfigDir = configDir
|
||||
config.FilePath = filename
|
||||
config.Attributes[AttrAutostart] = "false"
|
||||
config.Attributes[AttrEnabled] = "true"
|
||||
|
||||
k := new(ristretto255.Scalar)
|
||||
b := make([]byte, 64)
|
||||
|
@ -83,16 +111,113 @@ func LoadConfig(configDir, filename string) Config {
|
|||
}
|
||||
k.FromUniformBytes(b)
|
||||
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
|
||||
}
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
|
|
@ -50,14 +50,14 @@ func (ta *TokenboardServer) Listen() {
|
|||
for {
|
||||
data := ta.connection.Expect()
|
||||
if len(data) == 0 {
|
||||
log.Debugf("Server Closing Connection")
|
||||
log.Debugf("server Closing Connection")
|
||||
ta.connection.Close()
|
||||
return // connection is closed
|
||||
}
|
||||
|
||||
var message groups.Message
|
||||
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()
|
||||
return // connection is closed
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ func (ta *TokenboardServer) Listen() {
|
|||
log.Debugf("Received a Post Message Request: %v", ta.connection.Hostname())
|
||||
ta.postMessageRequest(postrequest)
|
||||
} else {
|
||||
log.Debugf("Server Closing Connection Because of PostRequestMessage Client Packet")
|
||||
log.Debugf("server Closing Connection Because of PostRequestMessage Client Packet")
|
||||
ta.connection.Close()
|
||||
return // connection is closed
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ func (ta *TokenboardServer) Listen() {
|
|||
ta.connection.Send(data)
|
||||
}
|
||||
} else {
|
||||
log.Debugf("Server Closing Connection Because of Malformed ReplayRequestMessage Packet")
|
||||
log.Debugf("server Closing Connection Because of Malformed ReplayRequestMessage Packet")
|
||||
ta.connection.Close()
|
||||
return // connection is closed
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -2,10 +2,10 @@ package storage
|
|||
|
||||
import (
|
||||
"cwtch.im/cwtch/protocol/groups"
|
||||
"git.openprivacy.ca/cwtch.im/server/metrics"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/cwtch.im/server/metrics"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
_ "github.com/mattn/go-sqlite3" // sqlite3 driver
|
||||
)
|
||||
|
|
|
@ -2,8 +2,8 @@ package storage
|
|||
|
||||
import (
|
||||
"cwtch.im/cwtch/protocol/groups"
|
||||
"git.openprivacy.ca/cwtch.im/server/metrics"
|
||||
"encoding/binary"
|
||||
"git.openprivacy.ca/cwtch.im/server/metrics"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"os"
|
||||
"testing"
|
||||
|
|
|
@ -4,7 +4,8 @@ set -e
|
|||
pwd
|
||||
GORACE="haltonerror=1"
|
||||
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 | \
|
||||
awk '{if($1 != last) {print $0;last=$1}}' >> coverage.out
|
||||
rm -rf *.cover.out
|
||||
|
|
Loading…
Reference in New Issue