From 644b3b1e99292cd6728b3706c1232f8d7f524ce5 Mon Sep 17 00:00:00 2001 From: Malte E Date: Thu, 9 May 2024 18:48:27 +0200 Subject: [PATCH] Add submit-challenge command, inform user about ratelimiting --- commands.go | 26 +++++++++++++++ messagetracking.go | 4 +++ pkg/signalmeow/client.go | 1 + pkg/signalmeow/sending.go | 66 +++++++++++++++++++++++++++++++++++++-- 4 files changed, 95 insertions(+), 2 deletions(-) diff --git a/commands.go b/commands.go index 728f2d62..53280331 100644 --- a/commands.go +++ b/commands.go @@ -74,6 +74,7 @@ func (br *SignalBridge) RegisterCommands() { cmdInvite, cmdListInvited, cmdRevokeInvite, + cmdSubmitChallenge, ) } @@ -1128,3 +1129,28 @@ func fnCreate(ce *WrappedCommandEvent) { portal.UpdateBridgeInfo(ce.Ctx) ce.Reply("Successfully created Signal group %s", gid.String()) } + +var cmdSubmitChallenge = &commands.FullHandler{ + Func: wrapCommand(fnSubmitChallenge), + Name: "submit-challenge", + Help: commands.HelpMeta{ + Section: HelpSectionMiscellaneous, + Description: "Submit a captcha challenge when getting rate limited", + Args: "<_captcha token_>", + }, + RequiresLogin: true, +} + +func fnSubmitChallenge(ce *WrappedCommandEvent) { + if len(ce.Args) == 0 { + ce.Reply("**Usage:** `submit-challenge <_captcha token_>`") + } + captcha := ce.Args[0] + captcha = strings.TrimPrefix(captcha, "signalcaptcha://") + err := ce.User.Client.SubmitRateLimitRecaptchaChallenge(ce.Ctx, captcha) + if err != nil { + ce.Reply("Failed to submit challenge: %v", err) + return + } + ce.Reply("Captcha challenge submitted successfully") +} diff --git a/messagetracking.go b/messagetracking.go index e95abb4e..646bb481 100644 --- a/messagetracking.go +++ b/messagetracking.go @@ -30,6 +30,7 @@ import ( "maunium.net/go/mautrix/id" "go.mau.fi/mautrix-signal/msgconv" + "go.mau.fi/mautrix-signal/pkg/signalmeow" ) var ( @@ -115,6 +116,9 @@ func (portal *Portal) sendErrorMessage(ctx context.Context, evt *event.Event, er if errors.Is(err, errMessageTakingLong) { msg = fmt.Sprintf("\u26a0 Bridging your %s is taking longer than usual", msgType) } + if errors.Is(err, signalmeow.ErrCaptchaChallengeRequired) { + msg = "\u26a0 You have been rate limited. Follow the instructions at https://docs.mau.fi/bridges/go/signal/captcha.html on how to complete a captcha challenge." + } content := &event.MessageEventContent{ MsgType: event.MsgNotice, Body: msg, diff --git a/pkg/signalmeow/client.go b/pkg/signalmeow/client.go index f311e9a8..76631208 100644 --- a/pkg/signalmeow/client.go +++ b/pkg/signalmeow/client.go @@ -55,6 +55,7 @@ type Client struct { cdAuthLock sync.Mutex cdAuth *basicExpiringCredentials cdToken []byte + ChallengeToken string } func (cli *Client) handleEvent(evt events.SignalEvent) { diff --git a/pkg/signalmeow/sending.go b/pkg/signalmeow/sending.go index 11211e00..cf08a83c 100644 --- a/pkg/signalmeow/sending.go +++ b/pkg/signalmeow/sending.go @@ -994,7 +994,6 @@ func (cli *Client) handle410(ctx context.Context, recipient libsignalgo.ServiceI // We got rate limited. // We ~~will~~ could try sending a "pushChallenge" response, but if that doesn't work we just gotta wait. -// TODO: explore captcha response func (cli *Client) handle428(ctx context.Context, recipient libsignalgo.ServiceID, response *signalpb.WebSocketResponseMessage) error { log := zerolog.Ctx(ctx) // Decode json body @@ -1005,7 +1004,6 @@ func (cli *Client) handle428(ctx context.Context, recipient libsignalgo.ServiceI log.Err(err).Msg("Unmarshal error") return err } - // Sample response: //id:25 status:428 message:"Precondition Required" headers:"Retry-After:86400" //headers:"Content-Type:application/json" headers:"Content-Length:88" @@ -1024,6 +1022,13 @@ func (cli *Client) handle428(ctx context.Context, recipient libsignalgo.ServiceI if retryAfterSeconds > 0 { log.Warn().Uint64("retry_after_seconds", retryAfterSeconds).Msg("Got rate limited") } + + token := body["token"] + s, ok := token.(string) + if ok { + cli.ChallengeToken = s + } + // TODO: responding to a pushChallenge this way doesn't work, server just returns 422 // Luckily challenges seem rare when sending with sealed sender //if body["options"] != nil { @@ -1053,5 +1058,62 @@ func (cli *Client) handle428(ctx context.Context, recipient libsignalgo.ServiceI // } // } //} + return ErrCaptchaChallengeRequired +} + +var ErrCaptchaChallengeRequired = errors.New("captcha challenge required") + +type ChallengeType string + +const ( + ChallengeTypeCaptcha ChallengeType = "captcha" + ChallengeTypePush ChallengeType = "rateLimitPushChallenge" +) + +type SubmitChallengePayload struct { + Type ChallengeType `json:"type"` + Token string `json:"token"` + Captcha string `json:"captcha,omitempty"` +} + +func (cli *Client) SubmitRateLimitRecaptchaChallenge(ctx context.Context, captcha string) error { + log := zerolog.Ctx(ctx).With(). + Str("action", "submit rate limit recaptcha challenge"). + Logger() + if cli.ChallengeToken == "" { + return fmt.Errorf("no pending challenge") + } + payload, err := json.Marshal(SubmitChallengePayload{ + Type: ChallengeTypeCaptcha, + Token: cli.ChallengeToken, + Captcha: captcha, + }) + if err != nil { + log.Err(err).Msg("Error marshalling request") + return err + } + username, password := cli.Store.BasicAuthCreds() + request := web.CreateWSRequest(http.MethodPut, "/v1/challenge", payload, &username, &password) + response, err := cli.AuthedWS.SendRequest(ctx, request) + if err != nil { + log.Err(err).Msg("Error sending request") + return err + } + responseCode := *response.Status + log.Debug().Uint32("Response Code", responseCode).Msg("Sent recaptcha challenge") + if responseCode == 428 { + return fmt.Errorf("Error submitting captcha challenge. Your captcha may have expired") + } + if responseCode != http.StatusOK && responseCode != http.StatusAccepted && responseCode != http.StatusNoContent && responseCode != http.StatusMultiStatus { + var body map[string]interface{} + err = json.Unmarshal(response.Body, &body) + if err != nil { + log.Err(err).Msg("Error unmarshalling message body") + return err + } + return fmt.Errorf("failed to submit captcha challenge. response code: %d, message: %s", responseCode, body["message"]) + } else { + cli.ChallengeToken = "" + } return nil }