adding 'servers' interface to manage multiple servers and support for encrypted configs

This commit is contained in:
Dan Ballard 2021-10-04 16:21:41 -07:00
parent e7499ee9fb
commit d361d71a2a
8 changed files with 350 additions and 79 deletions

View File

@ -14,6 +14,8 @@ The app takes the following arguments
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

View File

@ -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,10 +17,6 @@ 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")
@ -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 'hash name': %s\n", server.HashName())
server.Setup(serverConfig)
// 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))
log.Infof("Server bundle (import into client to use server): %s\n", log.Magenta(server.Server()))
if *flagExportTofu {
ioutil.WriteFile(path.Join(serverConfig.ConfigDir, "tofubundle"), []byte(tofubundle), 0600)
// Todo: change all to server export
ioutil.WriteFile(path.Join(serverConfig.ConfigDir, "tofubundle"), []byte(server.TofuBundle()), 0600)
}
// Graceful Shutdown

122
server.go

File diff suppressed because one or more lines are too long

View File

@ -2,15 +2,22 @@ package server
import (
"crypto/rand"
v1 "cwtch.im/cwtch/storage/v1"
"encoding/json"
"git.openprivacy.ca/cwtch.im/tapir/primitives"
"git.openprivacy.ca/openprivacy/log"
"github.com/gtank/ristretto255"
"golang.org/x/crypto/ed25519"
"io/ioutil"
"os"
"path"
)
const (
// SaltFile is the standard filename to store an encrypted config's SALT under beside it
SaltFile = "SALT"
)
// Reporting is a struct for storing a the config a server needs to be a peer, and connect to a group to report
type Reporting struct {
LogMetricsToFile bool `json:"logMetricsToFile"`
@ -22,6 +29,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"`
@ -46,17 +55,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}
id, pk := primitives.InitializeEphemeralIdentity()
tid, tpk := primitives.InitializeEphemeralIdentity()
@ -71,8 +71,6 @@ func LoadConfig(configDir, filename string) Config {
ReportingServerAddr: "",
}
config.AutoStart = false
config.ConfigDir = configDir
config.FilePath = filename
k := new(ristretto255.Scalar)
b := make([]byte, 64)
@ -83,16 +81,87 @@ 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) {
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.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) {
log.Infof("Loading config from %s\n", path.Join(configDir, filename))
config := initDefaultConfig(configDir, filename, encrypted)
raw, err := ioutil.ReadFile(path.Join(configDir, filename))
if err != nil {
return nil, err
}
if encrypted {
salt, err := ioutil.ReadFile(path.Join(configDir, SaltFile))
if err != nil {
return nil, err
}
key := v1.CreateKey(password, salt)
settingsStore := v1.NewFileStore(configDir, ServerConfigFile, key)
raw, err = settingsStore.Read()
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 {
log.Infof("Saving config to %s\n", path.Join(config.ConfigDir, config.FilePath))
bytes, _ := json.MarshalIndent(config, "", "\t")
if config.Encrypted {
settingStore := v1.NewFileStore(config.ConfigDir, config.FilePath, config.key)
return settingStore.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 {
salt, err := ioutil.ReadFile(path.Join(config.ConfigDir, SaltFile))
if err != nil {
return false
}
oldkey := v1.CreateKey(checkpass, salt[:])
return oldkey == config.key
}

View File

@ -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
}

134
servers.go Normal file
View File

@ -0,0 +1,134 @@
package server
import (
"cwtch.im/cwtch/model"
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/connectivity"
"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
LaunchServers()
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}
}
// 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 {
server := NewServer(newConfig)
s.servers[server.Onion()] = server
loadedServers = append(loadedServers, server.Onion())
}
}
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")
}
// LaunchServers Run() all loaded servers
func (s *servers) LaunchServers() {
s.lock.Lock()
defer s.lock.Unlock()
for _, server := range s.servers {
server.Run(s.acn)
}
}
// ShutdownServer Shutsdown the specified server
func (s *servers) ShutdownServer(onion string) {
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()
}
}

View File

@ -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
)

View File

@ -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"