2024-01-12 19:19:52 +01:00
|
|
|
package pairing
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/twofas/2fas-server/internal/common/logging"
|
2024-01-24 20:57:31 +01:00
|
|
|
"github.com/twofas/2fas-server/internal/pass/sign"
|
2024-01-12 19:19:52 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
type Pairing struct {
|
2024-01-24 20:57:31 +01:00
|
|
|
store store
|
|
|
|
signSvc *sign.Service
|
2024-01-12 19:19:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
type store interface {
|
|
|
|
AddExtension(ctx context.Context, extensionID string)
|
|
|
|
ExtensionExists(ctx context.Context, extensionID string) bool
|
|
|
|
GetPairingInfo(ctx context.Context, extensionID string) (PairingInfo, error)
|
|
|
|
SetPairingInfo(ctx context.Context, extensionID string, pi PairingInfo) error
|
|
|
|
}
|
|
|
|
|
2024-01-24 20:57:31 +01:00
|
|
|
func NewPairingApp(signService *sign.Service) *Pairing {
|
2024-01-12 19:19:52 +01:00
|
|
|
return &Pairing{
|
2024-01-24 20:57:31 +01:00
|
|
|
store: NewMemoryStore(),
|
|
|
|
signSvc: signService,
|
2024-01-12 19:19:52 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-24 20:57:31 +01:00
|
|
|
const (
|
|
|
|
pairingTokenValidityDuration = 3 * time.Minute
|
|
|
|
)
|
|
|
|
|
2024-01-12 19:19:52 +01:00
|
|
|
type ConfigureBrowserExtensionRequest struct {
|
|
|
|
ExtensionID string `json:"extension_id"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type ConfigureBrowserExtensionResponse struct {
|
|
|
|
BrowserExtensionPairingToken string `json:"browser_extension_pairing_token"`
|
|
|
|
ConnectionToken string `json:"connection_token"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Pairing) ConfigureBrowserExtension(ctx context.Context, req ConfigureBrowserExtensionRequest) (ConfigureBrowserExtensionResponse, error) {
|
|
|
|
p.store.AddExtension(ctx, req.ExtensionID)
|
|
|
|
|
2024-01-24 20:57:31 +01:00
|
|
|
pairingToken, err := p.signSvc.SignAndEncode(sign.Message{
|
|
|
|
ConnectionID: req.ExtensionID,
|
|
|
|
ExpiresAt: time.Now().Add(pairingTokenValidityDuration),
|
|
|
|
ConnectionType: sign.ConnectionTypeBrowserExtensionWait,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return ConfigureBrowserExtensionResponse{}, fmt.Errorf("failed to generate pairing token: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
mobileToken, err := p.signSvc.SignAndEncode(sign.Message{
|
|
|
|
ConnectionID: req.ExtensionID,
|
|
|
|
ExpiresAt: time.Now().Add(pairingTokenValidityDuration),
|
|
|
|
ConnectionType: sign.ConnectionTypeMobileConfirm,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return ConfigureBrowserExtensionResponse{}, fmt.Errorf("Failed to generate mobile confirm token: %v", err)
|
|
|
|
}
|
2024-01-12 19:19:52 +01:00
|
|
|
return ConfigureBrowserExtensionResponse{
|
2024-01-24 20:57:31 +01:00
|
|
|
ConnectionToken: mobileToken,
|
2024-01-12 19:19:52 +01:00
|
|
|
BrowserExtensionPairingToken: pairingToken,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type ExtensionWaitForConnectionInput struct {
|
|
|
|
ResponseWriter http.ResponseWriter
|
|
|
|
HttpReq *http.Request
|
|
|
|
}
|
|
|
|
|
|
|
|
type WaitForConnectionResponse struct {
|
|
|
|
BrowserExtensionProxyToken string `json:"browser_extension_proxy_token"`
|
|
|
|
Status string `json:"status"`
|
|
|
|
DeviceID string `json:"device_id"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Pairing) ServePairingWS(w http.ResponseWriter, r *http.Request, extID string) {
|
|
|
|
log := logging.WithField("extension_id", extID)
|
2024-01-21 10:25:12 +01:00
|
|
|
upgrader, err := wsUpgraderForProtocol(r)
|
|
|
|
if err != nil {
|
|
|
|
log.Error(err)
|
|
|
|
return
|
|
|
|
}
|
2024-01-12 19:19:52 +01:00
|
|
|
conn, err := upgrader.Upgrade(w, r, nil)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("Failed to upgrade on ServePairingWS: %v", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defer conn.Close()
|
|
|
|
|
|
|
|
log.Info("Starting pairing WS")
|
|
|
|
|
|
|
|
if deviceID, pairingDone := p.isExtensionPaired(r.Context(), extID, log); pairingDone {
|
|
|
|
if err := p.sendTokenAndCloseConn(extID, deviceID, conn); err != nil {
|
|
|
|
log.Errorf("Failed to send token: %v", err)
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
maxWaitTime = 3 * time.Minute
|
|
|
|
checkIfConnectedInterval = time.Second
|
|
|
|
)
|
|
|
|
maxWaitC := time.After(maxWaitTime)
|
|
|
|
// TODO: consider returning event from store on change.
|
|
|
|
connectedCheckTicker := time.NewTicker(checkIfConnectedInterval)
|
|
|
|
defer connectedCheckTicker.Stop()
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-maxWaitC:
|
|
|
|
log.Info("Closing paring ws after timeout")
|
|
|
|
return
|
|
|
|
case <-connectedCheckTicker.C:
|
|
|
|
if deviceID, pairingDone := p.isExtensionPaired(r.Context(), extID, log); pairingDone {
|
|
|
|
if err := p.sendTokenAndCloseConn(extID, deviceID, conn); err != nil {
|
|
|
|
log.Errorf("Failed to send token: %v", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
log.WithField("device_id", deviceID).Infof("Paring ws finished")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Pairing) isExtensionPaired(ctx context.Context, extID string, log *logrus.Entry) (string, bool) {
|
|
|
|
pairingInfo, err := p.store.GetPairingInfo(ctx, extID)
|
|
|
|
if err != nil {
|
|
|
|
log.Warn("Failed to get pairing info")
|
|
|
|
return "", false
|
|
|
|
}
|
|
|
|
return pairingInfo.Device.DeviceID, pairingInfo.IsPaired()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Pairing) sendTokenAndCloseConn(extID, deviceID string, conn *websocket.Conn) error {
|
2024-01-24 20:57:31 +01:00
|
|
|
extProxyToken, err := p.signSvc.SignAndEncode(sign.Message{
|
|
|
|
ConnectionID: extID,
|
|
|
|
ExpiresAt: time.Now().Add(pairingTokenValidityDuration),
|
|
|
|
ConnectionType: sign.ConnectionTypeBrowserExtensionProxy,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Failed to generate ext proxy token: %v", err)
|
|
|
|
}
|
|
|
|
|
2024-01-12 19:19:52 +01:00
|
|
|
if err := conn.WriteJSON(WaitForConnectionResponse{
|
2024-01-24 20:57:31 +01:00
|
|
|
BrowserExtensionProxyToken: extProxyToken,
|
2024-01-12 19:19:52 +01:00
|
|
|
Status: "ok",
|
|
|
|
DeviceID: deviceID,
|
|
|
|
}); err != nil {
|
|
|
|
return fmt.Errorf("failed to write to extension: %v", err)
|
|
|
|
}
|
|
|
|
return conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetPairingInfo returns paired device and information if pairing was done.
|
|
|
|
func (p *Pairing) GetPairingInfo(ctx context.Context, extensionID string) (PairingInfo, error) {
|
|
|
|
return p.store.GetPairingInfo(ctx, extensionID)
|
|
|
|
}
|
|
|
|
|
|
|
|
type ConfirmPairingRequest struct {
|
|
|
|
FCMToken string `json:"fcm_token"`
|
|
|
|
DeviceID string `json:"device_id"`
|
|
|
|
}
|
|
|
|
|
2024-01-24 20:57:31 +01:00
|
|
|
type ConfirmPairingResponse struct {
|
|
|
|
ProxyToken string `json:"proxy_token"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Pairing) ConfirmPairing(ctx context.Context, req ConfirmPairingRequest, extensionID string) (ConfirmPairingResponse, error) {
|
|
|
|
mobileProxyToken, err := p.signSvc.SignAndEncode(sign.Message{
|
|
|
|
ConnectionID: extensionID,
|
|
|
|
ExpiresAt: time.Now().Add(pairingTokenValidityDuration),
|
|
|
|
ConnectionType: sign.ConnectionTypeMobileProxy,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return ConfirmPairingResponse{}, fmt.Errorf("Failed to generate ext proxy token: %v", err)
|
|
|
|
}
|
|
|
|
if err := p.store.SetPairingInfo(ctx, extensionID, PairingInfo{
|
2024-01-12 19:19:52 +01:00
|
|
|
Device: MobileDevice{
|
|
|
|
DeviceID: req.DeviceID,
|
|
|
|
FCMToken: req.FCMToken,
|
|
|
|
},
|
|
|
|
PairedAt: time.Now().UTC(),
|
2024-01-24 20:57:31 +01:00
|
|
|
}); err != nil {
|
|
|
|
return ConfirmPairingResponse{}, err
|
|
|
|
}
|
|
|
|
return ConfirmPairingResponse{ProxyToken: mobileProxyToken}, nil
|
2024-01-12 19:19:52 +01:00
|
|
|
}
|