mirror of
https://github.com/status-im/status-go.git
synced 2025-02-07 04:13:56 +00:00
This change allows to connect to the mail server that we were using before the app was restarted. Separate loop is listening for whisper events, and when we receive event that request was completed we will update time on a peer record. Records are stored in leveldb. Body of the record is marshaled using json. At this point the only field is a timestamp when record was used. This loop doesn't control connections, it only tracks what mail server we ended up using. It works asynchronously to connection management loop. Which tracks events that are related to connection state and expiry of the requests. When app starts we look into the database and select the most recently used record. This record is added to connection management loop first. So if this server is available we will stick to using it. If we weren't able to connect to the same server in configured timeout (5s) we will try to connect to any other server from list of active servers. closes: #1285
254 lines
7.3 KiB
Go
254 lines
7.3 KiB
Go
package shhext
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/crypto/sha3"
|
|
"github.com/ethereum/go-ethereum/node"
|
|
"github.com/ethereum/go-ethereum/p2p"
|
|
"github.com/ethereum/go-ethereum/p2p/enode"
|
|
"github.com/ethereum/go-ethereum/rpc"
|
|
"github.com/status-im/status-go/services/shhext/chat"
|
|
"github.com/status-im/status-go/services/shhext/dedup"
|
|
"github.com/status-im/status-go/services/shhext/mailservers"
|
|
whisper "github.com/status-im/whisper/whisperv6"
|
|
"github.com/syndtr/goleveldb/leveldb"
|
|
)
|
|
|
|
const (
|
|
// defaultConnectionsTarget used in Service.Start if configured connection target is 0.
|
|
defaultConnectionsTarget = 1
|
|
// defaultTimeoutWaitAdded is a timeout to use to establish initial connections.
|
|
defaultTimeoutWaitAdded = 5 * time.Second
|
|
)
|
|
|
|
var errProtocolNotInitialized = errors.New("procotol is not initialized")
|
|
|
|
// EnvelopeEventsHandler used for two different event types.
|
|
type EnvelopeEventsHandler interface {
|
|
EnvelopeSent(common.Hash)
|
|
EnvelopeExpired(common.Hash)
|
|
MailServerRequestCompleted(common.Hash, common.Hash, []byte, error)
|
|
MailServerRequestExpired(common.Hash)
|
|
}
|
|
|
|
// Service is a service that provides some additional Whisper API.
|
|
type Service struct {
|
|
w *whisper.Whisper
|
|
config *ServiceConfig
|
|
tracker *tracker
|
|
server *p2p.Server
|
|
nodeID *ecdsa.PrivateKey
|
|
deduplicator *dedup.Deduplicator
|
|
protocol *chat.ProtocolService
|
|
debug bool
|
|
dataDir string
|
|
installationID string
|
|
pfsEnabled bool
|
|
|
|
peerStore *mailservers.PeerStore
|
|
cache *mailservers.Cache
|
|
connManager *mailservers.ConnectionManager
|
|
lastUsedMonitor *mailservers.LastUsedConnectionMonitor
|
|
}
|
|
|
|
type ServiceConfig struct {
|
|
DataDir string
|
|
InstallationID string
|
|
Debug bool
|
|
PFSEnabled bool
|
|
MailServerConfirmations bool
|
|
EnableConnectionManager bool
|
|
EnableLastUsedMonitor bool
|
|
ConnectionTarget int
|
|
}
|
|
|
|
// Make sure that Service implements node.Service interface.
|
|
var _ node.Service = (*Service)(nil)
|
|
|
|
// New returns a new Service. dataDir is a folder path to a network-independent location
|
|
func New(w *whisper.Whisper, handler EnvelopeEventsHandler, db *leveldb.DB, config *ServiceConfig) *Service {
|
|
cache := mailservers.NewCache(db)
|
|
ps := mailservers.NewPeerStore(cache)
|
|
track := &tracker{
|
|
w: w,
|
|
handler: handler,
|
|
cache: map[common.Hash]EnvelopeState{},
|
|
batches: map[common.Hash]map[common.Hash]struct{}{},
|
|
mailPeers: ps,
|
|
mailServerConfirmation: config.MailServerConfirmations,
|
|
}
|
|
return &Service{
|
|
w: w,
|
|
config: config,
|
|
tracker: track,
|
|
deduplicator: dedup.NewDeduplicator(w, db),
|
|
debug: config.Debug,
|
|
dataDir: config.DataDir,
|
|
installationID: config.InstallationID,
|
|
pfsEnabled: config.PFSEnabled,
|
|
peerStore: ps,
|
|
cache: cache,
|
|
}
|
|
}
|
|
|
|
// UpdateMailservers updates information about selected mail servers.
|
|
func (s *Service) UpdateMailservers(nodes []*enode.Node) error {
|
|
if err := s.peerStore.Update(nodes); err != nil {
|
|
return err
|
|
}
|
|
if s.connManager != nil {
|
|
s.connManager.Notify(nodes)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Protocols returns a new protocols list. In this case, there are none.
|
|
func (s *Service) Protocols() []p2p.Protocol {
|
|
return []p2p.Protocol{}
|
|
}
|
|
|
|
// InitProtocol create an instance of ProtocolService given an address and password
|
|
func (s *Service) InitProtocol(address string, password string) error {
|
|
if !s.pfsEnabled {
|
|
return nil
|
|
}
|
|
|
|
digest := sha3.Sum256([]byte(password))
|
|
hashedPassword := fmt.Sprintf("%x", digest)
|
|
|
|
if err := os.MkdirAll(filepath.Clean(s.dataDir), os.ModePerm); err != nil {
|
|
return err
|
|
}
|
|
v0Path := filepath.Join(s.dataDir, fmt.Sprintf("%x.db", address))
|
|
v1Path := filepath.Join(s.dataDir, fmt.Sprintf("%s.db", s.installationID))
|
|
v2Path := filepath.Join(s.dataDir, fmt.Sprintf("%s.v2.db", s.installationID))
|
|
|
|
if err := chat.MigrateDBFile(v0Path, v1Path, "ON", password); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := chat.MigrateDBFile(v1Path, v2Path, password, hashedPassword); err != nil {
|
|
// Remove db file as created with a blank password and never used,
|
|
// and there's no need to rekey in this case
|
|
os.Remove(v1Path)
|
|
os.Remove(v2Path)
|
|
}
|
|
|
|
persistence, err := chat.NewSQLLitePersistence(v2Path, hashedPassword)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
addedBundlesHandler := func(addedBundles []chat.IdentityAndIDPair) {
|
|
handler := EnvelopeSignalHandler{}
|
|
for _, bundle := range addedBundles {
|
|
handler.BundleAdded(bundle[0], bundle[1])
|
|
}
|
|
}
|
|
|
|
s.protocol = chat.NewProtocolService(chat.NewEncryptionService(persistence, chat.DefaultEncryptionServiceConfig(s.installationID)), addedBundlesHandler)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) ProcessPublicBundle(myIdentityKey *ecdsa.PrivateKey, bundle *chat.Bundle) ([]chat.IdentityAndIDPair, error) {
|
|
if s.protocol == nil {
|
|
return nil, errProtocolNotInitialized
|
|
}
|
|
|
|
return s.protocol.ProcessPublicBundle(myIdentityKey, bundle)
|
|
}
|
|
|
|
func (s *Service) GetBundle(myIdentityKey *ecdsa.PrivateKey) (*chat.Bundle, error) {
|
|
if s.protocol == nil {
|
|
return nil, errProtocolNotInitialized
|
|
}
|
|
|
|
return s.protocol.GetBundle(myIdentityKey)
|
|
}
|
|
|
|
// EnableInstallation enables an installation for multi-device sync.
|
|
func (s *Service) EnableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
|
|
if s.protocol == nil {
|
|
return errProtocolNotInitialized
|
|
}
|
|
|
|
return s.protocol.EnableInstallation(myIdentityKey, installationID)
|
|
}
|
|
|
|
// DisableInstallation disables an installation for multi-device sync.
|
|
func (s *Service) DisableInstallation(myIdentityKey *ecdsa.PublicKey, installationID string) error {
|
|
if s.protocol == nil {
|
|
return errProtocolNotInitialized
|
|
}
|
|
|
|
return s.protocol.DisableInstallation(myIdentityKey, installationID)
|
|
}
|
|
|
|
// APIs returns a list of new APIs.
|
|
func (s *Service) APIs() []rpc.API {
|
|
apis := []rpc.API{
|
|
{
|
|
Namespace: "shhext",
|
|
Version: "1.0",
|
|
Service: NewPublicAPI(s),
|
|
Public: true,
|
|
},
|
|
}
|
|
|
|
if s.debug {
|
|
apis = append(apis, rpc.API{
|
|
Namespace: "debug",
|
|
Version: "1.0",
|
|
Service: NewDebugAPI(s),
|
|
Public: true,
|
|
})
|
|
}
|
|
|
|
return apis
|
|
}
|
|
|
|
// Start is run when a service is started.
|
|
// It does nothing in this case but is required by `node.Service` interface.
|
|
func (s *Service) Start(server *p2p.Server) error {
|
|
if s.config.EnableConnectionManager {
|
|
connectionsTarget := s.config.ConnectionTarget
|
|
if connectionsTarget == 0 {
|
|
connectionsTarget = defaultConnectionsTarget
|
|
}
|
|
s.connManager = mailservers.NewConnectionManager(server, s.w, connectionsTarget, defaultTimeoutWaitAdded)
|
|
s.connManager.Start()
|
|
if err := mailservers.EnsureUsedRecordsAddedFirst(s.peerStore, s.connManager); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if s.config.EnableLastUsedMonitor {
|
|
s.lastUsedMonitor = mailservers.NewLastUsedConnectionMonitor(s.peerStore, s.cache, s.w)
|
|
s.lastUsedMonitor.Start()
|
|
}
|
|
s.tracker.Start()
|
|
s.nodeID = server.PrivateKey
|
|
s.server = server
|
|
return nil
|
|
}
|
|
|
|
// Stop is run when a service is stopped.
|
|
// It does nothing in this case but is required by `node.Service` interface.
|
|
func (s *Service) Stop() error {
|
|
if s.config.EnableConnectionManager {
|
|
s.connManager.Stop()
|
|
}
|
|
if s.config.EnableLastUsedMonitor {
|
|
s.lastUsedMonitor.Stop()
|
|
}
|
|
s.tracker.Stop()
|
|
return nil
|
|
}
|