2fas-server/internal/pass/sync/sync.go
2024-04-30 15:58:48 +02:00

181 lines
5.0 KiB
Go

package sync
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"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/sign"
)
type Syncing struct {
store store
signSvc *sign.Service
}
type store interface {
RequestSync(fmtToken string)
ConfirmSync(fmtToken string) bool
IsSyncCofirmed(fmtToken string) bool
}
func NewApp(signService *sign.Service, fakeMobilePush bool) *Syncing {
return &Syncing{
store: NewMemoryStore(),
signSvc: signService,
}
}
const (
syncTokenValidityDuration = 3 * time.Minute
)
type ConfigureBrowserExtensionRequest struct {
ExtensionID string `json:"extension_id"`
}
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 RequestSyncResponse struct {
BrowserExtensionProxyToken string `json:"browser_extension_proxy_token"`
Status string `json:"status"`
}
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)
}
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:
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
}
log.Infof("Sync ws finished")
return nil
}
}
}
}
func (s *Syncing) isSyncConfirmed(ctx context.Context, fcmToken string) bool {
return s.store.IsSyncCofirmed(fcmToken)
}
func (s *Syncing) requestSync(ctx context.Context, fcmToken string) {
s.store.RequestSync(fcmToken)
}
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(RequestSyncResponse{
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, ""))
}
func (s *Syncing) sendMobileToken(fcmToken string, resp http.ResponseWriter) error {
extProxyToken, err := s.signSvc.SignAndEncode(sign.Message{
ConnectionID: fcmToken,
ExpiresAt: time.Now().Add(syncTokenValidityDuration),
ConnectionType: sign.ConnectionTypeMobileSyncConfirm,
})
if err != nil {
return fmt.Errorf("failed to generate ext proxy token: %v", err)
}
bb, err := json.Marshal(struct {
MobileSyncConfirmToken string `json:"mobile_sync_confirm_token"`
}{
MobileSyncConfirmToken: extProxyToken,
})
if err != nil {
return fmt.Errorf("failed to marshal the response: %v", err)
}
if _, err := resp.Write(bb); err != nil {
return fmt.Errorf("failed to write the response: %v", err)
}
return nil
}
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
}