2fas-server/internal/pass/sync/sync.go

195 lines
5.6 KiB
Go
Raw Permalink Normal View History

package sync
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"firebase.google.com/go/v4/messaging"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/twofas/2fas-server/internal/common/logging"
"github.com/twofas/2fas-server/internal/pass/connection"
"github.com/twofas/2fas-server/internal/pass/fcm"
"github.com/twofas/2fas-server/internal/pass/sign"
)
type Syncing struct {
store store
signSvc *sign.Service
fcmClient fcm.Client
}
type store interface {
RequestSync(fcmToken string)
ConfirmSync(fcmToken string) bool
IsSyncConfirmed(fcmToken string) bool
}
func NewApp(signService *sign.Service, fcmClient fcm.Client) *Syncing {
return &Syncing{
store: NewMemoryStore(),
signSvc: signService,
fcmClient: fcmClient,
}
}
const (
syncTokenValidityDuration = 3 * time.Minute
)
type ConfigureBrowserExtensionResponse struct {
BrowserExtensionPairingToken string `json:"browser_extension_pairing_token"`
ConnectionToken string `json:"connection_token"`
}
type ExtensionWaitForConnectionInput struct {
ResponseWriter http.ResponseWriter
HttpReq *http.Request
}
type MobileSyncPayload struct {
MobileSyncToken string `json:"mobile_sync_token"`
}
func (s *Syncing) ServeSyncingRequestWS(w http.ResponseWriter, r *http.Request, fcmToken string) error {
log := logging.WithField("fcm_token", fcmToken)
conn, err := connection.Upgrade(w, r)
if err != nil {
return fmt.Errorf("failed to upgrade connection: %w", err)
}
defer conn.Close()
log.Infof("Starting sync request WS for %q", fcmToken)
s.requestSync(r.Context(), fcmToken)
if syncDone := s.isSyncConfirmed(r.Context(), fcmToken); syncDone {
if err := s.sendTokenAndCloseConn(fcmToken, conn); err != nil {
log.Errorf("Failed to send token: %v", err)
}
2024-04-27 15:24:58 +02:00
log.Infof("Sync ws finished")
return nil
}
const (
maxWaitTime = 3 * time.Minute
checkIfConnectedInterval = time.Second
)
maxWaitC := time.After(maxWaitTime)
connectedCheckTicker := time.NewTicker(checkIfConnectedInterval)
defer connectedCheckTicker.Stop()
for {
select {
case <-maxWaitC:
2024-04-27 15:24:58 +02:00
log.Info("Closing sync ws after timeout")
return nil
case <-connectedCheckTicker.C:
if syncConfirmed := s.isSyncConfirmed(r.Context(), fcmToken); syncConfirmed {
if err := s.sendTokenAndCloseConn(fcmToken, conn); err != nil {
log.Errorf("Failed to send token: %v", err)
return nil
}
2024-04-27 15:24:58 +02:00
log.Infof("Sync ws finished")
return nil
}
}
}
}
func (s *Syncing) isSyncConfirmed(ctx context.Context, fcmToken string) bool {
return s.store.IsSyncConfirmed(fcmToken)
}
func (s *Syncing) requestSync(ctx context.Context, fcmToken string) {
s.store.RequestSync(fcmToken)
}
type WaitForSyncResponse struct {
BrowserExtensionProxyToken string `json:"browser_extension_proxy_token"`
Status string `json:"status"`
}
func (s *Syncing) sendTokenAndCloseConn(fcmToken string, conn *websocket.Conn) error {
extProxyToken, err := s.signSvc.SignAndEncode(sign.Message{
ConnectionID: fcmToken,
ExpiresAt: time.Now().Add(syncTokenValidityDuration),
ConnectionType: sign.ConnectionTypeBrowserExtensionSync,
})
if err != nil {
return fmt.Errorf("failed to generate ext proxy token: %v", err)
}
if err := conn.WriteJSON(WaitForSyncResponse{
BrowserExtensionProxyToken: extProxyToken,
Status: "ok",
}); err != nil {
return fmt.Errorf("failed to write to extension: %v", err)
}
return conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
}
type ConfirmSyncResponse struct {
ProxyToken string `json:"proxy_token"`
}
var noSyncRequestErr = errors.New("sync request was not created")
func (s *Syncing) confirmSync(ctx context.Context, fcmToken string) (ConfirmSyncResponse, error) {
logging.Infof("Starting sync confirm for %q", fcmToken)
mobileProxyToken, err := s.signSvc.SignAndEncode(sign.Message{
ConnectionID: fcmToken,
ExpiresAt: time.Now().Add(syncTokenValidityDuration),
ConnectionType: sign.ConnectionTypeMobileSyncConfirm,
})
if err != nil {
return ConfirmSyncResponse{}, fmt.Errorf("failed to generate ext proxy token: %v", err)
}
if ok := s.store.ConfirmSync(fcmToken); !ok {
return ConfirmSyncResponse{}, noSyncRequestErr
}
return ConfirmSyncResponse{ProxyToken: mobileProxyToken}, nil
}
type RequestSyncResponse struct {
BrowserExtensionWaitToken string `json:"browser_extension_wait_token"`
MobileConfirmToken string `json:"mobile_confirm_token"`
}
func (s *Syncing) RequestSync(ctx *gin.Context, token string) (RequestSyncResponse, error) {
mobileConfirmToken, err := s.signSvc.SignAndEncode(sign.Message{
ConnectionID: token,
ExpiresAt: time.Now().Add(syncTokenValidityDuration),
ConnectionType: sign.ConnectionTypeMobileSyncConfirm,
})
if err != nil {
return RequestSyncResponse{}, fmt.Errorf("failed to generate mobile confirm token: %v", err)
}
browserExtensionWaitToken, err := s.signSvc.SignAndEncode(sign.Message{
ConnectionID: token,
ExpiresAt: time.Now().Add(syncTokenValidityDuration),
ConnectionType: sign.ConnectionTypeBrowserExtensionSyncWait,
})
if err != nil {
return RequestSyncResponse{}, fmt.Errorf("failed to generate browser extension wait token: %v", err)
}
return RequestSyncResponse{
BrowserExtensionWaitToken: browserExtensionWaitToken,
MobileConfirmToken: mobileConfirmToken,
}, nil
}
func (s *Syncing) SendPush(ctx *gin.Context, token string, body map[string]string) (fcm.Response, error) {
msg := &messaging.Message{
Token: token,
Android: &messaging.AndroidConfig{
Data: body,
},
}
return s.fcmClient.Send(ctx, msg)
}