@@ -21,6 +21,7 @@ import (
2121
2222 "maunium.net/go/mautrix"
2323 "maunium.net/go/mautrix/bridgev2"
24+ "maunium.net/go/mautrix/bridgev2/networkid"
2425 "maunium.net/go/mautrix/bridgev2/status"
2526)
2627
@@ -30,13 +31,18 @@ type ProvLogin struct {
3031 PrevStep * bridgev2.LoginStep
3132 NextStep * bridgev2.LoginStep
3233 Override * bridgev2.UserLogin
33- Lock sync.Mutex
34+ // ChallengeFor is set to the login this process is resolving a challenge
35+ // for, if it was created via PostLoginChallenge. At most one challenge
36+ // process may be outstanding per login.
37+ ChallengeFor networkid.UserLoginID
38+ Lock sync.Mutex
3439
3540 Ctx context.Context
3641 CancelCtx context.CancelFunc
3742}
3843
3944var ErrNilStep = errors .New ("bridge returned nil step with no error" )
45+ var ErrNoPendingChallenge = bridgev2.RespError {ErrCode : "FI.MAU.BRIDGE.NO_PENDING_CHALLENGE" , Err : "No pending challenge to resolve" , StatusCode : http .StatusNotFound }
4046var ErrTooManyLogins = bridgev2.RespError {ErrCode : "FI.MAU.BRIDGE.TOO_MANY_LOGINS" , Err : "Maximum number of logins exceeded" }
4147var ErrLoginCancelled = bridgev2.RespError {ErrCode : "FI.MAU.BRIDGE.LOGIN_CANCELLED" , Err : "Login process was cancelled" }
4248var ErrLoginTimedOut = bridgev2.RespError {ErrCode : "FI.MAU.BRIDGE.LOGIN_TIMED_OUT" , Err : "Login process timed out" }
@@ -97,6 +103,84 @@ func (prov *ProvisioningAPI) PostLoginStart(w http.ResponseWriter, r *http.Reque
97103 exhttp .WriteJSONResponse (w , http .StatusOK , & RespSubmitLogin {LoginID : loginID , LoginStep : firstStep })
98104}
99105
106+ func (prov * ProvisioningAPI ) PostLoginChallenge (w http.ResponseWriter , r * http.Request ) {
107+ // A challenge always targets a specific existing login, so require an
108+ // explicit login_id.
109+ userLogin , failed := prov .GetExplicitLoginForRequest (w , r )
110+ if failed {
111+ return
112+ } else if userLogin == nil {
113+ ErrNotLoggedIn .Write (w )
114+ return
115+ }
116+ challengeable , ok := userLogin .Client .(bridgev2.ChallengeProvidingNetworkAPI )
117+ if ! ok {
118+ ErrNoPendingChallenge .Write (w )
119+ return
120+ }
121+ // Check for an existing challenge and re-attach the caller if we have one.
122+ prov .loginsLock .RLock ()
123+ existing := prov .findChallengeLogin (userLogin .ID )
124+ prov .loginsLock .RUnlock ()
125+ if existing != nil {
126+ prov .respondWithExistingChallenge (w , r , existing )
127+ return
128+ }
129+ login , err := challengeable .Challenge (r .Context ())
130+ if err != nil {
131+ zerolog .Ctx (r .Context ()).Err (err ).Msg ("Failed to create challenge login process" )
132+ RespondWithError (w , err , "Internal error starting challenge" )
133+ return
134+ } else if login == nil {
135+ ErrNoPendingChallenge .Write (w )
136+ return
137+ }
138+ firstStep , err := login .Start (r .Context ())
139+ if err == nil && firstStep == nil {
140+ err = ErrNilStep
141+ }
142+ if err != nil {
143+ zerolog .Ctx (r .Context ()).Err (err ).Msg ("Failed to start challenge login process" )
144+ RespondWithError (w , err , "Internal error starting challenge" )
145+ return
146+ }
147+ loginID := xid .New ().String ()
148+ ctx , cancel := context .WithTimeout (prov .br .Bridge .BackgroundCtx , 30 * time .Minute )
149+ ctx = userLogin .Log .With ().
150+ Str ("login_id" , loginID ).
151+ Logger ().WithContext (ctx )
152+ provLogin := & ProvLogin {
153+ ID : loginID ,
154+ Process : login ,
155+ NextStep : firstStep ,
156+ Override : userLogin ,
157+ ChallengeFor : userLogin .ID ,
158+ Ctx : ctx ,
159+ CancelCtx : cancel ,
160+ }
161+ prov .loginsLock .Lock ()
162+ existing = prov .findChallengeLogin (userLogin .ID )
163+ if existing == nil {
164+ prov .logins [loginID ] = provLogin
165+ }
166+ prov .loginsLock .Unlock ()
167+ if existing != nil {
168+ // A concurrent request registered its process first. Drop ours (it was
169+ // never handed out, so nothing can be driving it) and use theirs.
170+ login .Cancel ()
171+ cancel ()
172+ prov .respondWithExistingChallenge (w , r , existing )
173+ return
174+ }
175+ go prov .handleLoginTimeout (provLogin )
176+ zerolog .Ctx (r .Context ()).Info ().
177+ Str ("login_id" , loginID ).
178+ Str ("override_login_id" , string (userLogin .ID )).
179+ Any ("first_step" , firstStep ).
180+ Msg ("Created challenge login process" )
181+ exhttp .WriteJSONResponse (w , http .StatusOK , & RespSubmitLogin {LoginID : loginID , LoginStep : firstStep })
182+ }
183+
100184func (prov * ProvisioningAPI ) PostLoginStep (w http.ResponseWriter , r * http.Request ) {
101185 loginID := r .PathValue ("loginProcessID" )
102186 prov .loginsLock .RLock ()
@@ -263,6 +347,15 @@ func (prov *ProvisioningAPI) handleCompleteStep(login *ProvLogin, step *bridgev2
263347 if login .Override == nil || login .Override .ID == step .CompleteParams .UserLoginID {
264348 return
265349 }
350+ if login .ChallengeFor != "" {
351+ // Challenges should never adversely affect existing logins; refuse
352+ // to delete it.
353+ zerolog .Ctx (ctx ).Error ().
354+ Str ("challenge_for" , string (login .ChallengeFor )).
355+ Str ("new_login_id" , string (step .CompleteParams .UserLoginID )).
356+ Msg ("Challenge completed with a different login ID, not deleting the original login" )
357+ return
358+ }
266359 zerolog .Ctx (ctx ).Info ().
267360 Str ("old_login_id" , string (login .Override .ID )).
268361 Str ("new_login_id" , string (step .CompleteParams .UserLoginID )).
@@ -284,6 +377,35 @@ func (prov *ProvisioningAPI) handleLoginTimeout(login *ProvLogin) {
284377 }
285378}
286379
380+ // findChallengeLogin returns the outstanding challenge process for the given
381+ // login, or nil if there isn't one.
382+ //
383+ // loginsLock must be held.
384+ func (prov * ProvisioningAPI ) findChallengeLogin (loginID networkid.UserLoginID ) * ProvLogin {
385+ for _ , login := range prov .logins {
386+ if login .ChallengeFor == loginID && login .Ctx .Err () == nil {
387+ return login
388+ }
389+ }
390+ return nil
391+ }
392+
393+ // respondWithExistingChallenge re-attaches the caller to a challenge process
394+ // that is already running.
395+ func (prov * ProvisioningAPI ) respondWithExistingChallenge (w http.ResponseWriter , r * http.Request , login * ProvLogin ) {
396+ login .Lock .Lock ()
397+ nextStep := login .NextStep
398+ login .Lock .Unlock ()
399+ zerolog .Ctx (r .Context ()).Info ().
400+ Str ("login_id" , login .ID ).
401+ Str ("challenge_for" , string (login .ChallengeFor )).
402+ Msg ("Re-attaching to existing challenge login process" )
403+ exhttp .WriteJSONResponse (w , http .StatusOK , & RespSubmitLogin {
404+ LoginID : login .ID ,
405+ LoginStep : nextStep ,
406+ })
407+ }
408+
287409func (prov * ProvisioningAPI ) deleteLogin (login * ProvLogin , cancel bool ) {
288410 if cancel {
289411 login .Process .Cancel ()
0 commit comments