2024-04-05 11:31:40 +02:00
|
|
|
package sync
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
|
2024-05-27 16:56:05 +02:00
|
|
|
"firebase.google.com/go/v4/messaging"
|
2024-05-07 14:04:44 +02:00
|
|
|
"github.com/gin-gonic/gin"
|
2024-04-05 11:31:40 +02:00
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
|
|
|
|
"github.com/twofas/2fas-server/internal/common/logging"
|
|
|
|
"github.com/twofas/2fas-server/internal/pass/connection"
|
2024-05-27 16:56:05 +02:00
|
|
|
"github.com/twofas/2fas-server/internal/pass/fcm"
|
2024-04-05 11:31:40 +02:00
|
|
|
"github.com/twofas/2fas-server/internal/pass/sign"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Syncing struct {
|
2024-05-27 16:56:05 +02:00
|
|
|
store store
|
|
|
|
signSvc *sign.Service
|
|
|
|
fcmClient fcm.Client
|
2024-04-05 11:31:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type store interface {
|
2024-05-27 16:56:05 +02:00
|
|
|
RequestSync(fcmToken string)
|
|
|
|
ConfirmSync(fcmToken string) bool
|
|
|
|
IsSyncConfirmed(fcmToken string) bool
|
2024-04-05 11:31:40 +02:00
|
|
|
}
|
|
|
|
|
2024-05-27 16:56:05 +02:00
|
|
|
func NewApp(signService *sign.Service, fcmClient fcm.Client) *Syncing {
|
2024-04-05 11:31:40 +02:00
|
|
|
return &Syncing{
|
2024-05-27 16:56:05 +02:00
|
|
|
store: NewMemoryStore(),
|
|
|
|
signSvc: signService,
|
|
|
|
fcmClient: fcmClient,
|
2024-04-05 11:31:40 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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")
|
2024-04-05 11:31:40 +02:00
|
|
|
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")
|
2024-04-05 11:31:40 +02:00
|
|
|
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")
|
2024-04-05 11:31:40 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Syncing) isSyncConfirmed(ctx context.Context, fcmToken string) bool {
|
2024-05-27 16:56:05 +02:00
|
|
|
return s.store.IsSyncConfirmed(fcmToken)
|
2024-04-05 11:31:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Syncing) requestSync(ctx context.Context, fcmToken string) {
|
|
|
|
s.store.RequestSync(fcmToken)
|
|
|
|
}
|
|
|
|
|
2024-05-07 14:04:44 +02:00
|
|
|
type WaitForSyncResponse struct {
|
|
|
|
BrowserExtensionProxyToken string `json:"browser_extension_proxy_token"`
|
|
|
|
Status string `json:"status"`
|
|
|
|
}
|
|
|
|
|
2024-04-05 11:31:40 +02:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2024-05-07 14:04:44 +02:00
|
|
|
if err := conn.WriteJSON(WaitForSyncResponse{
|
2024-04-05 11:31:40 +02:00
|
|
|
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
|
|
|
|
}
|
2024-05-07 14:04:44 +02:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2024-05-27 16:56:05 +02:00
|
|
|
|
2024-06-05 07:32:34 +02:00
|
|
|
func (s *Syncing) SendPush(ctx *gin.Context, token string, body map[string]string) (fcm.Response, error) {
|
2024-05-27 16:56:05 +02:00
|
|
|
msg := &messaging.Message{
|
|
|
|
Token: token,
|
|
|
|
Android: &messaging.AndroidConfig{
|
2024-06-05 07:32:34 +02:00
|
|
|
Data: body,
|
2024-05-27 16:56:05 +02:00
|
|
|
},
|
|
|
|
}
|
|
|
|
return s.fcmClient.Send(ctx, msg)
|
|
|
|
}
|