2024-04-05 11:31:40 +02:00
|
|
|
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)
|
|
|
|
}
|
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 {
|
|
|
|
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
|
|
|
|
}
|