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

submit: add support for checking imat tokens #1

Merged
merged 2 commits into from
Nov 13, 2023
Merged
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
138 changes: 102 additions & 36 deletions submit.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,13 @@ import (
"bytes"
"compress/gzip"
"context"
"crypto/rand"
"encoding/base32"
"encoding/json"
"fmt"
"html"
"io"
"io/ioutil"
"log"
"math/rand"
"mime"
"mime/multipart"
"net/http"
Expand Down Expand Up @@ -77,7 +76,8 @@ type parsedPayload struct {
Files []string `json:"files"`
FileErrors []string `json:"file_errors"`

Whoami *whoamiResponse `json:"-"`
MatrixWhoami *matrixWhoamiResponse `json:"-"`
IMAWhoami *imaWhoamiResponse `json:"-"`

IsInternal bool `json:"-"`

Expand Down Expand Up @@ -215,7 +215,7 @@ type whoamiBridgeInfo struct {
//RemoteState map[string]whoamiRemoteState `json:"remoteState"`
}

type whoamiResponse struct {
type matrixWhoamiResponse struct {
UserInfo struct {
Hungryserv bool `json:"useHungryserv"`
Channel string `json:"channel"`
Expand All @@ -232,7 +232,59 @@ type whoamiResponse struct {
} `json:"matrix"`
}

func (s *submitServer) verifyAccessToken(ctx context.Context, auth, userID string) (*whoamiResponse, error) {
type imaWhoamiResponse struct {
IMAUserToken string `json:"ima_user_token"`
AnalyticsID string `json:"analytics_id"`
Email string `json:"email"`
Subscription struct {
ExpiresAt string `json:"expires_at"`
Active bool `json:"active"`
} `json:"subscription"`
}

func (s *submitServer) verifyIMAToken(ctx context.Context, auth, userID string) (*imaWhoamiResponse, error) {
if len(auth) == 0 {
return nil, fmt.Errorf("missing authorization header")
} else if !strings.HasPrefix(auth, "Bearer ") {
return nil, fmt.Errorf("invalid authorization header")
}

// The user ID in this case should be an email
atIndex := strings.IndexRune(userID, '@')
if atIndex <= 0 {
return nil, fmt.Errorf("invalid user ID")
}

// All of iMessage on Android is on beeper.com
apiServerURL, ok := s.cfg.APIServerURLs["beeper.com"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's strictly true, this should probably use the current env api server (then dev/staging booper can be pointed at dev/staging rageshake)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, we might not be telling rageshake which env it is in 🤔 I guess hardcoding beeper.com for now is easier

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do dev/staging booper even exist? As far as I can tell, everyone is on the same app.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if there's a switcher, but booper used to be pointed at staging until a week ago or something

if !ok {
return nil, fmt.Errorf("beeper.com server API server URL not configured")
}

baseURL, _ := url.Parse(apiServerURL)
baseURL.Path = "/ima/whoami"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL.String(), nil)
if err != nil {
return nil, fmt.Errorf("failed to create http request: %w", err)
}
req.Header.Set("Authorization", auth)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make whoami request: %w", err)
}
defer resp.Body.Close()
var respData imaWhoamiResponse
if respBytes, err := io.ReadAll(resp.Body); err != nil {
return nil, fmt.Errorf("failed to read whoami response body (status %d): %w", resp.StatusCode, err)
} else if resp.StatusCode != 200 {
return nil, fmt.Errorf("whoami returned non-200 status code %d (data: %s)", resp.StatusCode, respBytes)
} else if err = json.Unmarshal(respBytes, &respData); err != nil {
return nil, fmt.Errorf("failed to parse success whoami response body: %w", err)
}
return &respData, nil
}

func (s *submitServer) verifyMatrixAccessToken(ctx context.Context, auth, userID string) (*matrixWhoamiResponse, error) {
if len(auth) == 0 {
return nil, fmt.Errorf("missing authorization header")
} else if !strings.HasPrefix(auth, "Bearer ") {
Expand All @@ -258,7 +310,7 @@ func (s *submitServer) verifyAccessToken(ctx context.Context, auth, userID strin
return nil, fmt.Errorf("failed to create http request: %w", err)
}
req.Header.Set("Authorization", auth)
var respData whoamiResponse
var respData matrixWhoamiResponse
resp, err := http.DefaultClient.Do(req)
if resp != nil {
defer resp.Body.Close()
Expand Down Expand Up @@ -289,12 +341,12 @@ func (s *submitServer) parseRequest(w http.ResponseWriter, req *http.Request, re
length, err := strconv.Atoi(req.Header.Get("Content-Length"))
if err != nil {
log.Println("Couldn't parse content-length", err)
http.Error(w, "Bad content-length", 400)
http.Error(w, "Bad content-length", http.StatusBadRequest)
return nil
}
if length > maxPayloadSize {
log.Println("Content-length", length, "too large")
http.Error(w, fmt.Sprintf("Content too large (max %d)", maxPayloadSize), 413)
http.Error(w, fmt.Sprintf("Content too large (max %d)", maxPayloadSize), http.StatusRequestEntityTooLarge)
return nil
}

Expand All @@ -303,40 +355,51 @@ func (s *submitServer) parseRequest(w http.ResponseWriter, req *http.Request, re
p, err = parseMultipartRequest(w, req, reportDir)
if err != nil {
log.Println("Error parsing multipart data:", err)
http.Error(w, "Bad multipart data", 400)
http.Error(w, "Bad multipart data", http.StatusBadRequest)
return nil
}
} else {
p, err = parseJSONRequest(w, req, reportDir)
if err != nil {
log.Println("Error parsing JSON body", err)
http.Error(w, fmt.Sprintf("Could not decode payload: %s", err.Error()), 400)
http.Error(w, fmt.Sprintf("Could not decode payload: %s", err.Error()), http.StatusBadRequest)
return nil
}
}

if p.AppName == "booper" {
userID, hasUserID := p.Data["user_id"]
delete(p.Data, "user_id")
delete(p.Data, "verified_device_id")
if !hasUserID {
return p
} else if p.AppName == "booper" {
if gplaySpamEmailRegex.MatchString(p.Data["user_id"]) {
log.Println("Dropping report from", p.Data["user_id"])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we still need this filter

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? It seems like a bad heuristic. What if someone just happens to have a bunch of numbers at the end of their gmail account?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to be spammed by the google play testers in linear, they're still sending over a dozen bug reports per day

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used to have a gmail account with 7 numbers at the end.

I went ahead and added this check back in, but this might be something we want to revisit.

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(200)
_, _ = w.Write([]byte("{}"))
return nil
}
// Don't verify tokens for booper
return p
}

userID, ok := p.Data["user_id"]
delete(p.Data, "user_id")
delete(p.Data, "verified_device_id")
if ok {
whoami, err := s.verifyAccessToken(req.Context(), req.Header.Get("Authorization"), userID)
whoami, err := s.verifyIMAToken(req.Context(), req.Header.Get("Authorization"), userID)
if err != nil {
log.Printf("Error verifying user ID (%s): %v", userID, err)
p.Data["unverified_user_id"] = userID
} else {
p.IMAWhoami = whoami
p.VerifiedUserID = whoami.Email
if p.VerifiedUserID != userID {
log.Printf("Mismatching user ID (verified: %s, input: %s), overriding...", p.VerifiedUserID, userID)
}
p.Data["user_id"] = p.VerifiedUserID
}
} else {
whoami, err := s.verifyMatrixAccessToken(req.Context(), req.Header.Get("Authorization"), userID)
if err != nil {
log.Printf("Error verifying user ID (%s): %v", userID, err)
p.Data["unverified_user_id"] = userID
} else {
p.Whoami = whoami
p.MatrixWhoami = whoami
p.VerifiedUserID = whoami.Matrix.UserID
p.VerifiedDeviceID = whoami.Matrix.DeviceID
if p.VerifiedUserID != userID {
Expand Down Expand Up @@ -485,7 +548,7 @@ func parseFormPart(part *multipart.Part, p *parsedPayload, reportDir string) err
return nil
}

b, err := ioutil.ReadAll(partReader)
b, err := io.ReadAll(partReader)
if err != nil {
return err
}
Expand Down Expand Up @@ -602,10 +665,8 @@ func (s *submitServer) saveReportBackground(p parsedPayload, reportDir, listingU
return err
}

if p.AppName != "booper" {
if err := s.submitWebhook(context.Background(), p, listingURL, &resp); err != nil {
return err
}
if err := s.submitWebhook(context.Background(), p, listingURL, &resp); err != nil {
return err
}

return nil
Expand Down Expand Up @@ -642,32 +703,37 @@ func (s *submitServer) submitLinearIssue(p parsedPayload, listingURL string, res

labelIDs := []string{labelRageshake}
subscriberIDs := make([]string, 0)
if p.Whoami != nil && p.Whoami.UserInfo.Email != "" {
linearID := getLinearID(p.Whoami.UserInfo.Email, s.cfg.LinearToken)
if linearID != "" {

// Determine if the user has a Linear ID and add them to the subscriber
// list if they do have an ID.
var email string
if p.MatrixWhoami != nil && p.MatrixWhoami.UserInfo.Email != "" {
email = p.MatrixWhoami.UserInfo.Email
} else if p.IMAWhoami != nil && p.IMAWhoami.Email != "" {
email = p.IMAWhoami.Email
}
if email != "" {
if linearID := getLinearID(email, s.cfg.LinearToken); linearID != "" {
subscriberIDs = []string{linearID}
}
}

if p.AppName == "booper" {
labelIDs = append(labelIDs, labelBooperApp)

linearID := getLinearID(p.Data["user_id"], s.cfg.LinearToken)
if linearID != "" {
subscriberIDs = []string{linearID}
}
}

isInternal := len(subscriberIDs) > 0 || strings.HasSuffix(p.VerifiedUserID, ":beeper-dev.com") || strings.HasSuffix(p.VerifiedUserID, ":beeper-staging.com")
if isInternal {
p.IsInternal = true
labelIDs = append(labelIDs, labelInternalUser)
} else if p.AppName != "booper" {
labelIDs = append(labelIDs, labelSupportReview)
if p.Whoami != nil && p.Whoami.UserInfo.Channel == "NIGHTLY" {
if p.MatrixWhoami != nil && p.MatrixWhoami.UserInfo.Channel == "NIGHTLY" {
labelIDs = append(labelIDs, labelNightlyUser)
}
}
if p.Whoami != nil {
if !isInternal && p.Whoami.UserInfo.CreatedAt.Add(24*time.Hour).After(time.Now()) {
if p.MatrixWhoami != nil {
if !isInternal && p.MatrixWhoami.UserInfo.CreatedAt.Add(24*time.Hour).After(time.Now()) {
labelIDs = append(labelIDs, labelNewUser)
}
}
Expand Down
Loading