You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

221 lines
7.0 KiB

package server
import (
tor2 ""
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 interface {
Identity() primitives.Identity
Run(acn connectivity.ACN) error
KeyBundle() *model.KeyBundle
CheckStatus() (bool, error)
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
metricsPack metrics.Monitors
tokenTapirService tapir.Service
tokenServer *privacypass.TokenServer
tokenService primitives.Identity
tokenServicePrivKey ed25519.PrivateKey
tokenServiceStopped bool
onionServiceStopped bool
running bool
existingMessageCount int
lock sync.RWMutex
// 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
bs := new(persistence.BoltPersistence)
bs.Open(path.Join(serverConfig.ConfigDir, "tokens.db"))
server.tokenServer = privacypass.NewTokenServerFromStore(&serverConfig.TokenServiceK, bs)
log.Infof("Y: %v", server.tokenServer.Y)
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 {
return s.config.Identity()
// Run starts a server with the given privateKey
func (s *server) Run(acn connectivity.ACN) error {
defer s.lock.Unlock()
if s.running {
return nil
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", 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)
if err != nil {
return fmt.Errorf("could not open database: %v", err)
// Needed because we only collect metrics on a per-session basis
// TODO fix metrics so they persist across sessions?
s.existingMessageCount = len(ms.FetchMessages())
s.tokenTapirService = new(tor2.BaseOnionService)
s.tokenTapirService.Init(acn, s.tokenServicePrivKey, &s.tokenService)
tokenApplication := new(applications.TokenApplication)
tokenApplication.TokenService = s.tokenServer
powTokenApp := new(applications.ApplicationChain).
ChainApplication(new(applications.ProofOfWorkApplication), applications.SuccessfulProofOfWorkCapability).
ChainApplication(tokenApplication, applications.HasTokensCapability)
go func() {
s.tokenServiceStopped = true
go func() {
s.service.Listen(NewTokenBoardServer(ms, s.tokenServer))
s.onionServiceStopped = true
s.running = true
s.SetAttribute(AttrEnabled, "true")
return nil
// KeyBundle provides the signed keybundle of the server
func (s *server) KeyBundle() *model.KeyBundle {
kb := model.NewKeyBundle()
identity := s.config.Identity()
kb.Keys[model.KeyTypeServerOnion] = model.Key(identity.Hostname())
kb.Keys[model.KeyTypeTokenOnion] = model.Key(s.tokenService.Hostname())
kb.Keys[model.KeyTypePrivacyPass] = model.Key(s.tokenServer.Y.String())
return kb
// 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) {
defer s.lock.RUnlock()
if s.onionServiceStopped == true || s.tokenServiceStopped == true {
return s.running, fmt.Errorf("one of more server components are down: onion:%v token service: %v", s.onionServiceStopped, s.tokenServiceStopped)
return s.running, nil
// Shutdown kills the app closing all connections and freeing all goroutines
func (s *server) Shutdown() {
log.Infof("Shutting down server")
defer s.lock.Unlock()
if s.running {
log.Infof("Closing Token server Database...")
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.
type Statistics struct {
TotalMessages int
// 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 {
// TODO Statistics from Metrics is very awkward. Metrics needs an overhaul to make safe
total := s.existingMessageCount
if s.metricsPack.TotalMessageCounter != nil {
total += s.metricsPack.TotalMessageCounter.Count()
return Statistics{
TotalMessages: total,
func (s *server) Delete(password string) error {
defer s.lock.Unlock()
if s.config.Encrypted && !s.config.CheckPassword(password) {
return errors.New("Cannot delete server, passwords do not match")
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 {
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)