Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add submit-challenge command, inform user about ratelimiting #510

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ func (br *SignalBridge) RegisterCommands() {
cmdInvite,
cmdListInvited,
cmdRevokeInvite,
cmdSubmitChallenge,
)
}

Expand Down Expand Up @@ -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")
}
4 changes: 4 additions & 0 deletions messagetracking.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"maunium.net/go/mautrix/id"

"go.mau.fi/mautrix-signal/msgconv"
"go.mau.fi/mautrix-signal/pkg/signalmeow"
)

var (
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions pkg/signalmeow/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type Client struct {
cdAuthLock sync.Mutex
cdAuth *basicExpiringCredentials
cdToken []byte
ChallengeToken string
}

func (cli *Client) handleEvent(evt events.SignalEvent) {
Expand Down
66 changes: 64 additions & 2 deletions pkg/signalmeow/sending.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Loading