From 8037f51b1326523973158ca3d0d68533391134d2 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Fri, 11 Sep 2026 23:52:53 -0400 Subject: [PATCH 1/6] gcm: fix instance ID registration field --- gcm.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gcm.go b/gcm.go index 6d83a02..2e4eeb4 100644 --- a/gcm.go +++ b/gcm.go @@ -38,7 +38,7 @@ func RegisterGCM(ctx context.Context, authorizationEntity string, creds GCMCrede } if opts != nil && opts.InstanceID != "" { - values.Set("appId", opts.InstanceID) + values.Set("appid", opts.InstanceID) } if opts != nil && opts.Expiry != 0 { From a65dba2fc05ed6466c200a0ff233d22404dda2bf Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Sat, 12 Sep 2026 07:39:07 -0400 Subject: [PATCH 2/6] gcm: support numeric sender registrations --- errors.go | 8 +++++++ events.go | 6 ++++- gcm.go | 69 +++++++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 67 insertions(+), 16 deletions(-) diff --git a/errors.go b/errors.go index 5d856b1..286f838 100644 --- a/errors.go +++ b/errors.go @@ -9,6 +9,14 @@ package pushreceiver import "github.com/pkg/errors" +type GCMError string + +func (err GCMError) Error() string { + return "GCM error: " + string(err) +} + +const ErrGCMDeviceRegistration = GCMError("PHONE_REGISTRATION_ERROR") + // ErrGcmAuthorization is authorization error of GCM. var ErrGcmAuthorization = errors.New("GCM authorization error") diff --git a/events.go b/events.go index e895759..c08a2d8 100644 --- a/events.go +++ b/events.go @@ -55,6 +55,10 @@ type MessageEvent struct { } func newMessageEvent(data *pb.DataMessageStanza) *MessageEvent { + appID := data.GetAppID() + if appID == "" { + appID = data.GetCategory() + } return &MessageEvent{ PersistentID: data.GetPersistentId(), From: data.GetFrom(), @@ -65,7 +69,7 @@ func newMessageEvent(data *pb.DataMessageStanza) *MessageEvent { Token: data.GetToken(), RegID: data.GetRegId(), RawData: data.GetRawData(), - AppID: data.GetAppID(), + AppID: appID, } } diff --git a/gcm.go b/gcm.go index 2e4eeb4..822c049 100644 --- a/gcm.go +++ b/gcm.go @@ -27,6 +27,28 @@ type GCMRegistrationOpts struct { Expiry time.Duration } +func setGCMAppID(values url.Values, appID string) { + if len(appID) == 32 && strings.Trim(appID, "abcdefghijklmnop") == "" { + values.Set("app", appID) + } else { + values.Set("app", "org.chromium.linux") + values.Set("X-subtype", appID) + } +} + +func NewGCMAppID(authorizationEntity string) string { + if _, err := strconv.ParseUint(authorizationEntity, 10, 64); err != nil { + return "wp:" + strings.ToUpper(uuid.New().String()) + } + id := uuid.New() + var encoded [32]byte + for i, b := range id { + encoded[i*2] = 'a' + b>>4 + encoded[i*2+1] = 'a' + b&15 + } + return string(encoded[:]) +} + func RegisterGCM(ctx context.Context, authorizationEntity string, creds GCMCredentials, opts *GCMRegistrationOpts) (*FCMCredentials, error) { values := url.Values{} @@ -34,7 +56,7 @@ func RegisterGCM(ctx context.Context, authorizationEntity string, creds GCMCrede if opts != nil && opts.AppID != "" { appID = opts.AppID } else { - appID = "wp:" + strings.ToUpper(uuid.New().String()) + appID = NewGCMAppID(authorizationEntity) } if opts != nil && opts.InstanceID != "" { @@ -46,10 +68,9 @@ func RegisterGCM(ctx context.Context, authorizationEntity string, creds GCMCrede values.Set("ttl", ttl) } - values.Set("app", "org.chromium.linux") + setGCMAppID(values, appID) values.Set("scope", "GCM") values.Set("X-scope", "GCM") - values.Set("X-subtype", appID) values.Set("device", fmt.Sprint(creds.AndroidID)) values.Set("gmsv", strings.Split(chromeVersion, ".")[0]) values.Set("sender", authorizationEntity) @@ -61,18 +82,14 @@ func RegisterGCM(ctx context.Context, authorizationEntity string, creds GCMCrede if err != nil { return nil, errors.Wrap(err, "request GCM register") } - defer closeResponse(res) - - data, err := io.ReadAll(res.Body) + subscription, err := parseGCMResponse(res) if err != nil { return nil, errors.Wrap(err, "read GCM register response") } - - subscription, err := url.ParseQuery(string(data)) - if err != nil { - return nil, errors.Wrap(err, "parse GCM register URL") - } token := subscription.Get("token") + if token == "" { + return nil, errors.New("GCM register response missing token") + } return &FCMCredentials{ GCM: creds, @@ -83,10 +100,9 @@ func RegisterGCM(ctx context.Context, authorizationEntity string, creds GCMCrede func UnregisterGCM(ctx context.Context, authorizationEntity string, creds GCMCredentials, appID string) error { values := url.Values{} - values.Set("app", "org.chromium.linux") + setGCMAppID(values, appID) values.Set("scope", "GCM") values.Set("X-scope", "GCM") - values.Set("X-subtype", appID) values.Set("device", fmt.Sprint(creds.AndroidID)) values.Set("gmsv", strings.Split(chromeVersion, ".")[0]) values.Set("sender", authorizationEntity) @@ -99,9 +115,32 @@ func UnregisterGCM(ctx context.Context, authorizationEntity string, creds GCMCre return errors.Wrap(err, "failed to unregister with GCM") } - if res.StatusCode != http.StatusOK { - return errors.New("failed to unregister with GCM") + response, err := parseGCMResponse(res) + if err != nil { + return errors.Wrap(err, "read GCM unregister response") + } + if response.Get("token") == "" && response.Get("deleted") != appID { + return errors.New("GCM unregister response missing confirmation") } return nil } + +func parseGCMResponse(res *http.Response) (url.Values, error) { + defer closeResponse(res) + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected HTTP status %d", res.StatusCode) + } + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + values, err := url.ParseQuery(string(data)) + if err != nil { + return nil, err + } + if message := values.Get("Error"); message != "" { + return nil, GCMError(message) + } + return values, nil +} From fbda2c206fc98d9a83371b98e9d33adb6ed4456b Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Sat, 12 Sep 2026 19:40:30 -0400 Subject: [PATCH 3/6] fcm: support Android application registrations --- android.go | 144 ++++++++++++++++ checkin.go | 93 ++++++++-- fcm.go | 1 + firebase.go | 191 +++++++++++++++++++++ gcm.go | 142 +++++++++++----- pb/checkin/android_checkin.pb.go | 284 +++++++++++++++++++++++++------ proto/android_checkin.proto | 16 ++ 7 files changed, 773 insertions(+), 98 deletions(-) create mode 100644 android.go create mode 100644 firebase.go diff --git a/android.go b/android.go new file mode 100644 index 0000000..eeb5217 --- /dev/null +++ b/android.go @@ -0,0 +1,144 @@ +package pushreceiver + +import ( + "crypto/sha1" + "encoding/base64" + "encoding/hex" + "fmt" + "net/url" + "strconv" + "strings" + "time" +) + +type AndroidBuild struct { + Fingerprint string `json:"fingerprint"` + Hardware string `json:"hardware"` + Brand string `json:"brand"` + Bootloader string `json:"bootloader"` + Timestamp uint64 `json:"timestamp"` + GMSVersion uint64 `json:"gms_version"` + Device string `json:"device"` + SDKVersion uint32 `json:"sdk_version"` + Model string `json:"model"` + Manufacturer string `json:"manufacturer"` + Product string `json:"product"` + SecurityPatch string `json:"security_patch"` +} + +type AndroidCheckInConfig struct { + Build AndroidBuild `json:"build"` + Locale string `json:"locale"` + TimeZone string `json:"time_zone"` +} + +type AndroidFCMConfig struct { + PackageName string `json:"package_name"` + CertificateSHA1 string `json:"certificate_sha1"` + APIKey string `json:"api_key"` + FirebaseAppID string `json:"firebase_app_id"` + FirebaseProjectID string `json:"firebase_project_id"` + FirebaseAppName string `json:"firebase_app_name,omitempty"` + FirebaseInstallationSDKVersion string `json:"firebase_installation_sdk_version"` + FirebaseClientVersion string `json:"firebase_client_version"` + FirebaseClient string `json:"firebase_client,omitempty"` + FirebaseClientLogType string `json:"firebase_client_log_type,omitempty"` + AppVersionCode uint64 `json:"app_version_code"` + AppVersionName string `json:"app_version_name"` + TargetSDKVersion uint32 `json:"target_sdk_version"` + CheckIn AndroidCheckInConfig `json:"checkin"` +} + +func validMetadata(value string, required bool, maxLength int) bool { + if required && value == "" || len(value) > maxLength { + return false + } + return !strings.ContainsAny(value, "\x00\r\n") +} + +func (config AndroidCheckInConfig) Validate() error { + for _, value := range []string{ + config.Build.Fingerprint, config.Build.Hardware, config.Build.Brand, + config.Build.Bootloader, config.Build.Device, config.Build.Model, + config.Build.Manufacturer, config.Build.Product, config.Build.SecurityPatch, + config.Locale, config.TimeZone, + } { + if !validMetadata(value, false, 1024) { + return fmt.Errorf("invalid Android checkin metadata") + } + } + return nil +} + +func (config AndroidFCMConfig) Validate() error { + for _, value := range []string{ + config.PackageName, config.APIKey, config.FirebaseAppID, config.FirebaseProjectID, + config.FirebaseInstallationSDKVersion, config.FirebaseClientVersion, + } { + if !validMetadata(value, true, 1024) { + return fmt.Errorf("invalid Android FCM application metadata") + } + } + certificate, err := hex.DecodeString(config.CertificateSHA1) + if err != nil || len(certificate) != sha1.Size { + return fmt.Errorf("invalid Android application certificate fingerprint") + } + if strings.ContainsAny(config.FirebaseProjectID, "/\\?#%") || config.AppVersionCode == 0 || + config.TargetSDKVersion == 0 || config.CheckIn.Build.GMSVersion == 0 || config.CheckIn.Build.SDKVersion == 0 { + return fmt.Errorf("invalid Android FCM project or version") + } + if !validMetadata(config.AppVersionName, false, 1024) || !validMetadata(config.FirebaseAppName, false, 1024) || + !validMetadata(config.FirebaseClient, false, 16384) || + !validMetadata(config.FirebaseClientLogType, false, 16) { + return fmt.Errorf("invalid Android Firebase client metadata") + } + return config.CheckIn.Validate() +} + +func androidRegistrationValues(sender string, creds GCMCredentials, config *AndroidFCMConfig, installation *FirebaseInstallation, requireAuth bool) (url.Values, error) { + if err := config.Validate(); err != nil { + return nil, err + } + if value, err := strconv.ParseUint(sender, 10, 64); err != nil || value == 0 { + return nil, fmt.Errorf("invalid Android FCM sender") + } + if creds.AndroidID == 0 || creds.SecurityToken == 0 || !validMetadata(creds.VersionInfo, true, 16384) { + return nil, fmt.Errorf("incomplete Android checkin credentials") + } + if installation == nil || !validFirebaseInstallationID(installation.FID) || requireAuth && !installation.AuthTokenValid() { + return nil, fmt.Errorf("Firebase installation authorization is missing or expiring") + } + appName := config.FirebaseAppName + if appName == "" { + appName = "[DEFAULT]" + } + appNameHash := sha1.Sum([]byte(appName)) + appVersion := strconv.FormatUint(config.AppVersionCode, 10) + gmsVersion := strconv.FormatUint(config.CheckIn.Build.GMSVersion, 10) + values := url.Values{ + "sender": {sender}, "X-subtype": {sender}, "X-scope": {"*"}, + "X-appid": {installation.FID}, + "app": {config.PackageName}, "cert": {strings.ToLower(config.CertificateSHA1)}, + "device": {strconv.FormatUint(creds.AndroidID, 10)}, "info": {creds.VersionInfo}, + "app_ver": {appVersion}, "X-app_ver": {appVersion}, "X-app_ver_name": {config.AppVersionName}, + "gcm_ver": {gmsVersion}, "X-gmsv": {gmsVersion}, "plat": {"0"}, + "target_ver": {strconv.FormatUint(uint64(config.TargetSDKVersion), 10)}, + "X-osv": {strconv.FormatUint(uint64(config.CheckIn.Build.SDKVersion), 10)}, + "X-cliv": {config.FirebaseClientVersion}, "X-gmp_app_id": {config.FirebaseAppID}, + "X-firebase-app-name-hash": {base64.RawURLEncoding.EncodeToString(appNameHash[:])}, + } + if installation.AuthTokenValid() { + values.Set("X-Goog-Firebase-Installations-Auth", installation.AuthToken) + } + if config.FirebaseClient != "" { + values.Set("X-Firebase-Client", config.FirebaseClient) + } + if config.FirebaseClientLogType != "" { + values.Set("X-Firebase-Client-Log-Type", config.FirebaseClientLogType) + } + return values, nil +} + +func (installation FirebaseInstallation) AuthTokenValid() bool { + return validMetadata(installation.AuthToken, true, 16384) && time.Until(installation.AuthTokenExpiresAt) >= time.Hour +} diff --git a/checkin.go b/checkin.go index 05df29d..1d31bae 100644 --- a/checkin.go +++ b/checkin.go @@ -3,6 +3,7 @@ package pushreceiver import ( "bytes" "context" + "fmt" "io" "net/http" @@ -11,21 +12,45 @@ import ( "google.golang.org/protobuf/proto" ) -func CheckIn(ctx context.Context, opt *GCMCredentials) (*GCMCredentials, error) { - resp, err := checkin(ctx, opt) +func CheckIn(ctx context.Context, opt *GCMCredentials, options ...*AndroidCheckInConfig) (*GCMCredentials, error) { + if opt == nil { + opt = &GCMCredentials{} + } + if len(options) > 1 { + return nil, fmt.Errorf("multiple Android checkin configurations") + } + var config *AndroidCheckInConfig + if len(options) == 1 { + config = options[0] + } + if config != nil { + if err := config.Validate(); err != nil { + return nil, err + } + } + resp, err := checkin(ctx, opt, config) if err != nil { return nil, err } - creds := &GCMCredentials{ - AndroidID: *resp.AndroidId, - SecurityToken: *resp.SecurityToken, + creds := *opt + if resp.AndroidId != nil { + creds.AndroidID = resp.GetAndroidId() + } + if resp.SecurityToken != nil { + creds.SecurityToken = resp.GetSecurityToken() + } + if resp.VersionInfo != nil { + creds.VersionInfo = resp.GetVersionInfo() + } + if creds.AndroidID == 0 || creds.SecurityToken == 0 || config != nil && creds.VersionInfo == "" { + return nil, fmt.Errorf("incomplete GCM checkin response") } - return creds, nil + return &creds, nil } -func checkin(ctx context.Context, opt *GCMCredentials) (*pb.AndroidCheckinResponse, error) { +func checkin(ctx context.Context, opt *GCMCredentials, config *AndroidCheckInConfig) (*pb.AndroidCheckinResponse, error) { id := int64(opt.AndroidID) r := &pb.AndroidCheckinRequest{ Checkin: &pb.AndroidCheckinProto{ @@ -43,31 +68,79 @@ func checkin(ctx context.Context, opt *GCMCredentials) (*pb.AndroidCheckinRespon Id: &id, SecurityToken: &opt.SecurityToken, } + endpoint := checkinURL + post := postRequest + if config != nil { + build := config.Build + optionalString := func(value string) *string { + if value == "" { + return nil + } + return &value + } + r.Checkin = &pb.AndroidCheckinProto{ + UserNumber: proto.Int32(0), + Build: &pb.AndroidBuildProto{ + Fingerprint: optionalString(build.Fingerprint), Hardware: optionalString(build.Hardware), + Brand: optionalString(build.Brand), Bootloader: optionalString(build.Bootloader), + Device: optionalString(build.Device), Model: optionalString(build.Model), + Manufacturer: optionalString(build.Manufacturer), Product: optionalString(build.Product), + SecurityPatch: optionalString(build.SecurityPatch), + }, + } + if build.Timestamp != 0 { + r.Checkin.Build.Timestamp = proto.Uint64(build.Timestamp) + } + if build.GMSVersion != 0 { + r.Checkin.Build.GmsVersion = proto.Uint64(build.GMSVersion) + } + if build.SDKVersion != 0 { + r.Checkin.Build.SdkVersion = proto.Uint32(build.SDKVersion) + } + r.Locale = optionalString(config.Locale) + r.TimeZone = optionalString(config.TimeZone) + endpoint = "https://android.googleapis.com/checkin" + post = postAndroidRequest + } message, err := proto.Marshal(r) if err != nil { return nil, errors.Wrap(err, "marshal GCM checkin request") } - res, err := postRequest(ctx, checkinURL, bytes.NewReader(message), func(header *http.Header) { + res, err := post(ctx, endpoint, bytes.NewReader(message), func(header *http.Header) { header.Set("Content-Type", "application/x-protobuf") }) if err != nil { return nil, errors.Wrap(err, "request GCM checkin") } - defer closeResponse(res) + if config != nil { + defer res.Body.Close() + } else { + defer closeResponse(res) + } // unauthorized error if res.StatusCode == http.StatusUnauthorized { return nil, ErrGcmAuthorization } if res.StatusCode < 200 || res.StatusCode > 299 { + if config != nil { + return nil, fmt.Errorf("Android checkin HTTP status %d", res.StatusCode) + } return nil, errors.Errorf("server error: %s", res.Status) } - data, err := io.ReadAll(res.Body) + var body io.Reader = res.Body + if config != nil { + body = io.LimitReader(body, 4*1024*1024+1) + } + data, err := io.ReadAll(body) if err != nil { return nil, errors.Wrap(err, "read GCM checkin response") } + if config != nil && len(data) > 4*1024*1024 { + return nil, fmt.Errorf("GCM checkin response too large") + } var responseProto pb.AndroidCheckinResponse err = proto.Unmarshal(data, &responseProto) diff --git a/fcm.go b/fcm.go index 581a55e..0b925be 100644 --- a/fcm.go +++ b/fcm.go @@ -17,4 +17,5 @@ type FCMCredentials struct { type GCMCredentials struct { AndroidID uint64 `json:"androidId"` SecurityToken uint64 `json:"securityToken"` + VersionInfo string `json:"versionInfo,omitempty"` } diff --git a/firebase.go b/firebase.go new file mode 100644 index 0000000..b12823c --- /dev/null +++ b/firebase.go @@ -0,0 +1,191 @@ +package pushreceiver + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/google/uuid" +) + +type FirebaseInstallation struct { + FID string `json:"fid"` + RefreshToken string `json:"refresh_token"` + AuthToken string `json:"auth_token"` + AuthTokenExpiresAt time.Time `json:"auth_token_expires_at"` +} + +type FirebaseError int + +func (err FirebaseError) Error() string { + return fmt.Sprintf("Firebase installation HTTP status %d", int(err)) +} + +var ErrFirebaseInstallationAuth = errors.New("Firebase installation authorization rejected") + +func NewFirebaseInstallationID() (string, error) { + id, err := uuid.NewRandom() + if err != nil { + return "", err + } + var data [17]byte + copy(data[:16], id[:]) + data[16] = data[0] + data[0] = data[0]&15 | 112 + return base64.RawURLEncoding.EncodeToString(data[:])[:22], nil +} + +func validFirebaseInstallationID(fid string) bool { + return len(fid) == 22 && strings.Trim(fid, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") == "" +} + +type firebaseAuthToken struct { + Token string `json:"token"` + ExpiresIn string `json:"expiresIn"` +} + +func (token firebaseAuthToken) expiration(issuedAt time.Time) (time.Time, error) { + seconds, err := strconv.ParseUint(strings.TrimSuffix(token.ExpiresIn, "s"), 10, 32) + if err != nil || !strings.HasSuffix(token.ExpiresIn, "s") || seconds == 0 || !validMetadata(token.Token, true, 16384) { + return time.Time{}, fmt.Errorf("invalid Firebase installation auth response") + } + return issuedAt.Add(time.Duration(seconds) * time.Second), nil +} + +func CreateFirebaseInstallation(ctx context.Context, config *AndroidFCMConfig, fid string) (*FirebaseInstallation, error) { + if config == nil { + return nil, fmt.Errorf("missing Android FCM configuration") + } + if err := config.Validate(); err != nil { + return nil, err + } + if !validFirebaseInstallationID(fid) { + return nil, fmt.Errorf("invalid Firebase installation ID") + } + body := map[string]string{ + "fid": fid, "appId": config.FirebaseAppID, "authVersion": "FIS_v2", "sdkVersion": config.FirebaseInstallationSDKVersion, + } + var result struct { + FID string `json:"fid"` + RefreshToken string `json:"refreshToken"` + AuthToken firebaseAuthToken `json:"authToken"` + } + issuedAt := time.Now() + if err := firebaseRequest(ctx, config, "", "", body, &result); err != nil { + return nil, err + } + if !validFirebaseInstallationID(result.FID) || !validMetadata(result.RefreshToken, true, 16384) { + return nil, fmt.Errorf("incomplete Firebase installation response") + } + expiration, err := result.AuthToken.expiration(issuedAt) + if err != nil { + return nil, err + } + return &FirebaseInstallation{result.FID, result.RefreshToken, result.AuthToken.Token, expiration}, nil +} + +func RefreshFirebaseInstallation(ctx context.Context, config *AndroidFCMConfig, installation FirebaseInstallation) (*FirebaseInstallation, error) { + if config == nil { + return nil, fmt.Errorf("missing Android FCM configuration") + } + if err := config.Validate(); err != nil { + return nil, err + } + if !validFirebaseInstallationID(installation.FID) || !validMetadata(installation.RefreshToken, true, 16384) { + return nil, fmt.Errorf("incomplete Firebase installation credentials") + } + body := map[string]any{"installation": map[string]string{"sdkVersion": config.FirebaseInstallationSDKVersion}} + var token firebaseAuthToken + issuedAt := time.Now() + if err := firebaseRequest(ctx, config, installation.FID, installation.RefreshToken, body, &token); err != nil { + var status FirebaseError + if errors.As(err, &status) && (status == http.StatusUnauthorized || status == http.StatusNotFound) { + return nil, fmt.Errorf("%w: %w", ErrFirebaseInstallationAuth, err) + } + return nil, err + } + expiration, err := token.expiration(issuedAt) + if err != nil { + return nil, err + } + installation.AuthToken = token.Token + installation.AuthTokenExpiresAt = expiration + return &installation, nil +} + +func firebaseRequest(ctx context.Context, config *AndroidFCMConfig, fid, refreshToken string, body, result any) error { + data, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("encode Firebase installation request") + } + var compressed bytes.Buffer + writer := gzip.NewWriter(&compressed) + if _, err = writer.Write(data); err != nil { + return err + } + if err = writer.Close(); err != nil { + return err + } + endpoint := "https://firebaseinstallations.googleapis.com/v1/projects/" + url.PathEscape(config.FirebaseProjectID) + "/installations" + if fid != "" { + endpoint += "/" + url.PathEscape(fid) + "/authTokens:generate" + } + response, err := postAndroidRequest(ctx, endpoint, &compressed, func(header *http.Header) { + header.Set("Content-Type", "application/json") + header.Set("Accept", "application/json") + header.Set("Content-Encoding", "gzip") + header.Set("Cache-Control", "no-cache") + header.Set("X-Android-Package", config.PackageName) + header.Set("X-Android-Cert", strings.ToUpper(config.CertificateSHA1)) + header.Set("X-Goog-Api-Key", config.APIKey) + if refreshToken != "" { + header.Set("Authorization", "FIS_v2 "+refreshToken) + } + }) + if err != nil { + return err + } + defer response.Body.Close() + if response.StatusCode < 200 || response.StatusCode >= 300 { + return FirebaseError(response.StatusCode) + } + data, err = io.ReadAll(io.LimitReader(response.Body, 65537)) + if err != nil { + return fmt.Errorf("read Firebase installation response: %w", err) + } + if len(data) > 65536 || json.Unmarshal(data, result) != nil { + return fmt.Errorf("invalid Firebase installation response") + } + return nil +} + +func postAndroidRequest(ctx context.Context, endpoint string, body io.Reader, headerSetter func(*http.Header)) (*http.Response, error) { + request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body) + if err != nil { + return nil, fmt.Errorf("create Android registration request") + } + headerSetter(&request.Header) + client := &http.Client{ + Timeout: 30 * time.Second, + CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }, + } + response, err := client.Do(request) + if err != nil { + var urlError *url.Error + if errors.As(err, &urlError) { + err = urlError.Err + } + return nil, fmt.Errorf("Android registration request failed: %w", err) + } + return response, nil +} diff --git a/gcm.go b/gcm.go index 822c049..8ddc357 100644 --- a/gcm.go +++ b/gcm.go @@ -22,9 +22,11 @@ import ( ) type GCMRegistrationOpts struct { - AppID string - InstanceID string - Expiry time.Duration + AppID string + InstanceID string + Expiry time.Duration + Android *AndroidFCMConfig + Installation *FirebaseInstallation } func setGCMAppID(values url.Values, appID string) { @@ -59,30 +61,40 @@ func RegisterGCM(ctx context.Context, authorizationEntity string, creds GCMCrede appID = NewGCMAppID(authorizationEntity) } - if opts != nil && opts.InstanceID != "" { - values.Set("appid", opts.InstanceID) - } - - if opts != nil && opts.Expiry != 0 { - ttl := strconv.Itoa(int(opts.Expiry.Seconds())) - values.Set("ttl", ttl) - } - - setGCMAppID(values, appID) - values.Set("scope", "GCM") - values.Set("X-scope", "GCM") - values.Set("device", fmt.Sprint(creds.AndroidID)) - values.Set("gmsv", strings.Split(chromeVersion, ".")[0]) - values.Set("sender", authorizationEntity) - - res, err := postRequest(ctx, registerURL, strings.NewReader(values.Encode()), func(header *http.Header) { + endpoint := registerURL + post := postRequest + if opts != nil && opts.Android != nil { + var err error + values, err = androidRegistrationValues(authorizationEntity, creds, opts.Android, opts.Installation, true) + if err != nil { + return nil, err + } + endpoint = "https://android.apis.google.com/c2dm/register3" + post = postAndroidRequest + } else { + if opts != nil && opts.InstanceID != "" { + values.Set("appid", opts.InstanceID) + } + if opts != nil && opts.Expiry != 0 { + ttl := strconv.Itoa(int(opts.Expiry.Seconds())) + values.Set("ttl", ttl) + } + setGCMAppID(values, appID) + values.Set("scope", "GCM") + values.Set("X-scope", "GCM") + values.Set("device", fmt.Sprint(creds.AndroidID)) + values.Set("gmsv", strings.Split(chromeVersion, ".")[0]) + values.Set("sender", authorizationEntity) + } + + res, err := post(ctx, endpoint, strings.NewReader(values.Encode()), func(header *http.Header) { header.Set("Content-Type", "application/x-www-form-urlencoded") header.Set("Authorization", fmt.Sprintf("AidLogin %d:%d", creds.AndroidID, creds.SecurityToken)) }) if err != nil { return nil, errors.Wrap(err, "request GCM register") } - subscription, err := parseGCMResponse(res) + subscription, err := parseGCMResponse(res, opts != nil && opts.Android != nil) if err != nil { return nil, errors.Wrap(err, "read GCM register response") } @@ -98,16 +110,37 @@ func RegisterGCM(ctx context.Context, authorizationEntity string, creds GCMCrede }, nil } -func UnregisterGCM(ctx context.Context, authorizationEntity string, creds GCMCredentials, appID string) error { +func UnregisterGCM(ctx context.Context, authorizationEntity string, creds GCMCredentials, appID string, options ...*GCMRegistrationOpts) error { + if len(options) > 1 { + return errors.New("multiple GCM unregistration options") + } + var opts *GCMRegistrationOpts + if len(options) == 1 { + opts = options[0] + } + android := opts != nil && opts.Android != nil values := url.Values{} - setGCMAppID(values, appID) - values.Set("scope", "GCM") - values.Set("X-scope", "GCM") - values.Set("device", fmt.Sprint(creds.AndroidID)) - values.Set("gmsv", strings.Split(chromeVersion, ".")[0]) - values.Set("sender", authorizationEntity) - values.Set("delete", "true") - res, err := postRequest(ctx, registerURL, strings.NewReader(values.Encode()), func(header *http.Header) { + endpoint := registerURL + post := postRequest + if android { + var err error + values, err = androidRegistrationValues(authorizationEntity, creds, opts.Android, opts.Installation, false) + if err != nil { + return err + } + values.Set("X-delete", "1") + endpoint = "https://android.apis.google.com/c2dm/register3" + post = postAndroidRequest + } else { + setGCMAppID(values, appID) + values.Set("scope", "GCM") + values.Set("X-scope", "GCM") + values.Set("device", fmt.Sprint(creds.AndroidID)) + values.Set("gmsv", strings.Split(chromeVersion, ".")[0]) + values.Set("sender", authorizationEntity) + values.Set("delete", "true") + } + res, err := post(ctx, endpoint, strings.NewReader(values.Encode()), func(header *http.Header) { header.Set("Content-Type", "application/x-www-form-urlencoded") header.Set("Authorization", fmt.Sprintf("AidLogin %d:%d", creds.AndroidID, creds.SecurityToken)) }) @@ -115,31 +148,64 @@ func UnregisterGCM(ctx context.Context, authorizationEntity string, creds GCMCre return errors.Wrap(err, "failed to unregister with GCM") } - response, err := parseGCMResponse(res) + response, err := parseGCMResponse(res, android) if err != nil { return errors.Wrap(err, "read GCM unregister response") } - if response.Get("token") == "" && response.Get("deleted") != appID { + if android { + if !response.Has("deleted") { + return errors.New("Android FCM unregister response missing confirmation") + } + } else if response.Get("token") == "" && response.Get("deleted") != appID { return errors.New("GCM unregister response missing confirmation") } return nil } -func parseGCMResponse(res *http.Response) (url.Values, error) { - defer closeResponse(res) +func parseGCMResponse(res *http.Response, android bool) (url.Values, error) { + if android { + defer res.Body.Close() + } else { + defer closeResponse(res) + } if res.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected HTTP status %d", res.StatusCode) } - data, err := io.ReadAll(res.Body) - if err != nil { - return nil, err + var body io.Reader = res.Body + if android { + body = io.LimitReader(body, 65537) } - values, err := url.ParseQuery(string(data)) + data, err := io.ReadAll(body) if err != nil { return nil, err } + if android && len(data) > 65536 { + return nil, errors.New("GCM response too large") + } + var values url.Values + if android { + values = url.Values{} + for _, line := range strings.Split(string(data), "\n") { + if line == "" { + continue + } + key, value, found := strings.Cut(line, "=") + if !found || key == "" { + return nil, errors.New("invalid Android FCM response") + } + values.Set(key, value) + } + } else { + values, err = url.ParseQuery(string(data)) + if err != nil { + return nil, err + } + } if message := values.Get("Error"); message != "" { + if android && (len(message) > 128 || strings.Trim(message, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_") != "") { + return nil, errors.New("GCM response contains an invalid error code") + } return nil, GCMError(message) } return values, nil diff --git a/pb/checkin/android_checkin.pb.go b/pb/checkin/android_checkin.pb.go index 2b01e28..8c77fb1 100644 --- a/pb/checkin/android_checkin.pb.go +++ b/pb/checkin/android_checkin.pb.go @@ -7,18 +7,17 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.31.0 -// protoc v4.24.2 +// protoc-gen-go v1.34.2 +// protoc v5.28.2 // source: android_checkin.proto package checkin_proto import ( - reflect "reflect" - sync "sync" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" ) const ( @@ -298,12 +297,148 @@ func (x *ChromeBuildProto) GetChannel() ChromeBuildProto_Channel { return ChromeBuildProto_CHANNEL_STABLE } +type AndroidBuildProto struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Fingerprint *string `protobuf:"bytes,1,opt,name=fingerprint" json:"fingerprint,omitempty"` + Hardware *string `protobuf:"bytes,2,opt,name=hardware" json:"hardware,omitempty"` + Brand *string `protobuf:"bytes,3,opt,name=brand" json:"brand,omitempty"` + Bootloader *string `protobuf:"bytes,5,opt,name=bootloader" json:"bootloader,omitempty"` + Timestamp *uint64 `protobuf:"varint,7,opt,name=timestamp" json:"timestamp,omitempty"` + GmsVersion *uint64 `protobuf:"varint,8,opt,name=gms_version,json=gmsVersion" json:"gms_version,omitempty"` + Device *string `protobuf:"bytes,9,opt,name=device" json:"device,omitempty"` + SdkVersion *uint32 `protobuf:"varint,10,opt,name=sdk_version,json=sdkVersion" json:"sdk_version,omitempty"` + Model *string `protobuf:"bytes,11,opt,name=model" json:"model,omitempty"` + Manufacturer *string `protobuf:"bytes,12,opt,name=manufacturer" json:"manufacturer,omitempty"` + Product *string `protobuf:"bytes,13,opt,name=product" json:"product,omitempty"` + SecurityPatch *string `protobuf:"bytes,19,opt,name=security_patch,json=securityPatch" json:"security_patch,omitempty"` +} + +func (x *AndroidBuildProto) Reset() { + *x = AndroidBuildProto{} + if protoimpl.UnsafeEnabled { + mi := &file_android_checkin_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AndroidBuildProto) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AndroidBuildProto) ProtoMessage() {} + +func (x *AndroidBuildProto) ProtoReflect() protoreflect.Message { + mi := &file_android_checkin_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AndroidBuildProto.ProtoReflect.Descriptor instead. +func (*AndroidBuildProto) Descriptor() ([]byte, []int) { + return file_android_checkin_proto_rawDescGZIP(), []int{1} +} + +func (x *AndroidBuildProto) GetFingerprint() string { + if x != nil && x.Fingerprint != nil { + return *x.Fingerprint + } + return "" +} + +func (x *AndroidBuildProto) GetHardware() string { + if x != nil && x.Hardware != nil { + return *x.Hardware + } + return "" +} + +func (x *AndroidBuildProto) GetBrand() string { + if x != nil && x.Brand != nil { + return *x.Brand + } + return "" +} + +func (x *AndroidBuildProto) GetBootloader() string { + if x != nil && x.Bootloader != nil { + return *x.Bootloader + } + return "" +} + +func (x *AndroidBuildProto) GetTimestamp() uint64 { + if x != nil && x.Timestamp != nil { + return *x.Timestamp + } + return 0 +} + +func (x *AndroidBuildProto) GetGmsVersion() uint64 { + if x != nil && x.GmsVersion != nil { + return *x.GmsVersion + } + return 0 +} + +func (x *AndroidBuildProto) GetDevice() string { + if x != nil && x.Device != nil { + return *x.Device + } + return "" +} + +func (x *AndroidBuildProto) GetSdkVersion() uint32 { + if x != nil && x.SdkVersion != nil { + return *x.SdkVersion + } + return 0 +} + +func (x *AndroidBuildProto) GetModel() string { + if x != nil && x.Model != nil { + return *x.Model + } + return "" +} + +func (x *AndroidBuildProto) GetManufacturer() string { + if x != nil && x.Manufacturer != nil { + return *x.Manufacturer + } + return "" +} + +func (x *AndroidBuildProto) GetProduct() string { + if x != nil && x.Product != nil { + return *x.Product + } + return "" +} + +func (x *AndroidBuildProto) GetSecurityPatch() string { + if x != nil && x.SecurityPatch != nil { + return *x.SecurityPatch + } + return "" +} + // Information sent by the device in a "checkin" request. type AndroidCheckinProto struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields + Build *AndroidBuildProto `protobuf:"bytes,1,opt,name=build" json:"build,omitempty"` // Miliseconds since the Unix epoch of the device's last successful checkin. LastCheckinMsec *int64 `protobuf:"varint,2,opt,name=last_checkin_msec,json=lastCheckinMsec" json:"last_checkin_msec,omitempty"` // The current MCC+MNC of the mobile device's current cell. @@ -338,7 +473,7 @@ const ( func (x *AndroidCheckinProto) Reset() { *x = AndroidCheckinProto{} if protoimpl.UnsafeEnabled { - mi := &file_android_checkin_proto_msgTypes[1] + mi := &file_android_checkin_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -351,7 +486,7 @@ func (x *AndroidCheckinProto) String() string { func (*AndroidCheckinProto) ProtoMessage() {} func (x *AndroidCheckinProto) ProtoReflect() protoreflect.Message { - mi := &file_android_checkin_proto_msgTypes[1] + mi := &file_android_checkin_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -364,7 +499,14 @@ func (x *AndroidCheckinProto) ProtoReflect() protoreflect.Message { // Deprecated: Use AndroidCheckinProto.ProtoReflect.Descriptor instead. func (*AndroidCheckinProto) Descriptor() ([]byte, []int) { - return file_android_checkin_proto_rawDescGZIP(), []int{1} + return file_android_checkin_proto_rawDescGZIP(), []int{2} +} + +func (x *AndroidCheckinProto) GetBuild() *AndroidBuildProto { + if x != nil { + return x.Build + } + return nil } func (x *AndroidCheckinProto) GetLastCheckinMsec() int64 { @@ -448,35 +590,63 @@ var file_android_checkin_proto_rawDesc = []byte{ 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x44, 0x45, 0x56, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x43, 0x41, 0x4e, 0x41, 0x52, 0x59, 0x10, 0x04, 0x12, 0x13, 0x0a, 0x0f, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, - 0x4f, 0x57, 0x4e, 0x10, 0x05, 0x22, 0xca, 0x02, 0x0a, 0x13, 0x41, 0x6e, 0x64, 0x72, 0x6f, 0x69, - 0x64, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x2a, 0x0a, - 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x5f, 0x6d, 0x73, - 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x43, 0x68, - 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x4d, 0x73, 0x65, 0x63, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x65, 0x6c, - 0x6c, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x63, 0x65, 0x6c, 0x6c, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x21, - 0x0a, 0x0c, 0x73, 0x69, 0x6d, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x69, 0x6d, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, - 0x72, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x6f, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x18, 0x08, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x72, 0x6f, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x1f, 0x0a, 0x0b, 0x75, - 0x73, 0x65, 0x72, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x40, 0x0a, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x63, 0x68, 0x65, - 0x63, 0x6b, 0x69, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x54, 0x79, 0x70, 0x65, 0x3a, 0x11, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x41, 0x4e, - 0x44, 0x52, 0x4f, 0x49, 0x44, 0x5f, 0x4f, 0x53, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x42, - 0x0a, 0x0c, 0x63, 0x68, 0x72, 0x6f, 0x6d, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x18, 0x0d, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x5f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x68, 0x72, 0x6f, 0x6d, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x52, 0x0b, 0x63, 0x68, 0x72, 0x6f, 0x6d, 0x65, 0x42, 0x75, 0x69, - 0x6c, 0x64, 0x2a, 0x67, 0x0a, 0x0a, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x15, 0x0a, 0x11, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x41, 0x4e, 0x44, 0x52, 0x4f, - 0x49, 0x44, 0x5f, 0x4f, 0x53, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x44, 0x45, 0x56, 0x49, 0x43, - 0x45, 0x5f, 0x49, 0x4f, 0x53, 0x5f, 0x4f, 0x53, 0x10, 0x02, 0x12, 0x19, 0x0a, 0x15, 0x44, 0x45, - 0x56, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x48, 0x52, 0x4f, 0x4d, 0x45, 0x5f, 0x42, 0x52, 0x4f, 0x57, - 0x53, 0x45, 0x52, 0x10, 0x03, 0x12, 0x14, 0x0a, 0x10, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, - 0x43, 0x48, 0x52, 0x4f, 0x4d, 0x45, 0x5f, 0x4f, 0x53, 0x10, 0x04, 0x42, 0x13, 0x48, 0x03, 0x5a, - 0x0f, 0x2e, 0x3b, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x4f, 0x57, 0x4e, 0x10, 0x05, 0x22, 0xfa, 0x02, 0x0a, 0x11, 0x41, 0x6e, 0x64, 0x72, 0x6f, 0x69, + 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x20, 0x0a, 0x0b, 0x66, + 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x12, 0x1a, 0x0a, + 0x08, 0x68, 0x61, 0x72, 0x64, 0x77, 0x61, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x68, 0x61, 0x72, 0x64, 0x77, 0x61, 0x72, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x72, 0x61, + 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x62, 0x72, 0x61, 0x6e, 0x64, 0x12, + 0x1e, 0x0a, 0x0a, 0x62, 0x6f, 0x6f, 0x74, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x72, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x62, 0x6f, 0x6f, 0x74, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x72, 0x12, + 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1f, 0x0a, + 0x0b, 0x67, 0x6d, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x0a, 0x67, 0x6d, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, + 0x0a, 0x06, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x64, 0x6b, 0x5f, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73, 0x64, 0x6b, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, + 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x22, 0x0a, + 0x0c, 0x6d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x18, 0x0c, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, + 0x72, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x18, 0x0d, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, + 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x70, 0x61, 0x74, 0x63, 0x68, 0x18, 0x13, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x50, 0x61, 0x74, + 0x63, 0x68, 0x22, 0x82, 0x03, 0x0a, 0x13, 0x41, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x43, 0x68, + 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x36, 0x0a, 0x05, 0x62, 0x75, + 0x69, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x63, 0x68, 0x65, 0x63, + 0x6b, 0x69, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x6e, 0x64, 0x72, 0x6f, 0x69, + 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x52, 0x05, 0x62, 0x75, 0x69, + 0x6c, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x63, 0x68, 0x65, 0x63, 0x6b, + 0x69, 0x6e, 0x5f, 0x6d, 0x73, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x6c, + 0x61, 0x73, 0x74, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x4d, 0x73, 0x65, 0x63, 0x12, 0x23, + 0x0a, 0x0d, 0x63, 0x65, 0x6c, 0x6c, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x65, 0x6c, 0x6c, 0x4f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x6f, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x69, 0x6d, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x6f, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x69, 0x6d, 0x4f, 0x70, + 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x6f, 0x61, 0x6d, 0x69, 0x6e, + 0x67, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x6f, 0x61, 0x6d, 0x69, 0x6e, 0x67, + 0x12, 0x1f, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x4e, 0x75, 0x6d, 0x62, 0x65, + 0x72, 0x12, 0x40, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x19, 0x2e, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x3a, 0x11, 0x44, 0x45, 0x56, 0x49, + 0x43, 0x45, 0x5f, 0x41, 0x4e, 0x44, 0x52, 0x4f, 0x49, 0x44, 0x5f, 0x4f, 0x53, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x12, 0x42, 0x0a, 0x0c, 0x63, 0x68, 0x72, 0x6f, 0x6d, 0x65, 0x5f, 0x62, 0x75, + 0x69, 0x6c, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x68, 0x65, 0x63, + 0x6b, 0x69, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x68, 0x72, 0x6f, 0x6d, 0x65, + 0x42, 0x75, 0x69, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x52, 0x0b, 0x63, 0x68, 0x72, 0x6f, + 0x6d, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x2a, 0x67, 0x0a, 0x0a, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x15, 0x0a, 0x11, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, + 0x41, 0x4e, 0x44, 0x52, 0x4f, 0x49, 0x44, 0x5f, 0x4f, 0x53, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, + 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x49, 0x4f, 0x53, 0x5f, 0x4f, 0x53, 0x10, 0x02, 0x12, + 0x19, 0x0a, 0x15, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x48, 0x52, 0x4f, 0x4d, 0x45, + 0x5f, 0x42, 0x52, 0x4f, 0x57, 0x53, 0x45, 0x52, 0x10, 0x03, 0x12, 0x14, 0x0a, 0x10, 0x44, 0x45, + 0x56, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x48, 0x52, 0x4f, 0x4d, 0x45, 0x5f, 0x4f, 0x53, 0x10, 0x04, + 0x42, 0x13, 0x48, 0x03, 0x5a, 0x0f, 0x2e, 0x3b, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x5f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, } var ( @@ -492,24 +662,26 @@ func file_android_checkin_proto_rawDescGZIP() []byte { } var file_android_checkin_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_android_checkin_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_android_checkin_proto_goTypes = []interface{}{ +var file_android_checkin_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_android_checkin_proto_goTypes = []any{ (DeviceType)(0), // 0: checkin_proto.DeviceType (ChromeBuildProto_Platform)(0), // 1: checkin_proto.ChromeBuildProto.Platform (ChromeBuildProto_Channel)(0), // 2: checkin_proto.ChromeBuildProto.Channel (*ChromeBuildProto)(nil), // 3: checkin_proto.ChromeBuildProto - (*AndroidCheckinProto)(nil), // 4: checkin_proto.AndroidCheckinProto + (*AndroidBuildProto)(nil), // 4: checkin_proto.AndroidBuildProto + (*AndroidCheckinProto)(nil), // 5: checkin_proto.AndroidCheckinProto } var file_android_checkin_proto_depIdxs = []int32{ 1, // 0: checkin_proto.ChromeBuildProto.platform:type_name -> checkin_proto.ChromeBuildProto.Platform 2, // 1: checkin_proto.ChromeBuildProto.channel:type_name -> checkin_proto.ChromeBuildProto.Channel - 0, // 2: checkin_proto.AndroidCheckinProto.type:type_name -> checkin_proto.DeviceType - 3, // 3: checkin_proto.AndroidCheckinProto.chrome_build:type_name -> checkin_proto.ChromeBuildProto - 4, // [4:4] is the sub-list for method output_type - 4, // [4:4] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] is the sub-list for field type_name + 4, // 2: checkin_proto.AndroidCheckinProto.build:type_name -> checkin_proto.AndroidBuildProto + 0, // 3: checkin_proto.AndroidCheckinProto.type:type_name -> checkin_proto.DeviceType + 3, // 4: checkin_proto.AndroidCheckinProto.chrome_build:type_name -> checkin_proto.ChromeBuildProto + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name } func init() { file_android_checkin_proto_init() } @@ -518,7 +690,7 @@ func file_android_checkin_proto_init() { return } if !protoimpl.UnsafeEnabled { - file_android_checkin_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_android_checkin_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*ChromeBuildProto); i { case 0: return &v.state @@ -530,7 +702,19 @@ func file_android_checkin_proto_init() { return nil } } - file_android_checkin_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_android_checkin_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*AndroidBuildProto); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_android_checkin_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*AndroidCheckinProto); i { case 0: return &v.state @@ -549,7 +733,7 @@ func file_android_checkin_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_android_checkin_proto_rawDesc, NumEnums: 3, - NumMessages: 2, + NumMessages: 3, NumExtensions: 0, NumServices: 0, }, diff --git a/proto/android_checkin.proto b/proto/android_checkin.proto index b68a8de..fc2c33b 100644 --- a/proto/android_checkin.proto +++ b/proto/android_checkin.proto @@ -42,8 +42,24 @@ message ChromeBuildProto { optional Channel channel = 3; } +message AndroidBuildProto { + optional string fingerprint = 1; + optional string hardware = 2; + optional string brand = 3; + optional string bootloader = 5; + optional uint64 timestamp = 7; + optional uint64 gms_version = 8; + optional string device = 9; + optional uint32 sdk_version = 10; + optional string model = 11; + optional string manufacturer = 12; + optional string product = 13; + optional string security_patch = 19; +} + // Information sent by the device in a "checkin" request. message AndroidCheckinProto { + optional AndroidBuildProto build = 1; // Miliseconds since the Unix epoch of the device's last successful checkin. optional int64 last_checkin_msec = 2; From 2282700395d3b003f9dab18bcd73edd955bb0e4b Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Sat, 12 Sep 2026 19:47:12 -0400 Subject: [PATCH 4/6] fcm: fix registration lint errors --- android.go | 2 +- checkin.go | 2 +- firebase.go | 6 +++--- gcm.go | 2 +- pb/checkin/android_checkin.pb.go | 5 +++-- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/android.go b/android.go index eeb5217..74a474d 100644 --- a/android.go +++ b/android.go @@ -106,7 +106,7 @@ func androidRegistrationValues(sender string, creds GCMCredentials, config *Andr return nil, fmt.Errorf("incomplete Android checkin credentials") } if installation == nil || !validFirebaseInstallationID(installation.FID) || requireAuth && !installation.AuthTokenValid() { - return nil, fmt.Errorf("Firebase installation authorization is missing or expiring") + return nil, fmt.Errorf("firebase installation authorization is missing or expiring") } appName := config.FirebaseAppName if appName == "" { diff --git a/checkin.go b/checkin.go index 1d31bae..a6af1c2 100644 --- a/checkin.go +++ b/checkin.go @@ -126,7 +126,7 @@ func checkin(ctx context.Context, opt *GCMCredentials, config *AndroidCheckInCon } if res.StatusCode < 200 || res.StatusCode > 299 { if config != nil { - return nil, fmt.Errorf("Android checkin HTTP status %d", res.StatusCode) + return nil, fmt.Errorf("android checkin HTTP status %d", res.StatusCode) } return nil, errors.Errorf("server error: %s", res.Status) } diff --git a/firebase.go b/firebase.go index b12823c..f50330b 100644 --- a/firebase.go +++ b/firebase.go @@ -28,10 +28,10 @@ type FirebaseInstallation struct { type FirebaseError int func (err FirebaseError) Error() string { - return fmt.Sprintf("Firebase installation HTTP status %d", int(err)) + return fmt.Sprintf("firebase installation HTTP status %d", int(err)) } -var ErrFirebaseInstallationAuth = errors.New("Firebase installation authorization rejected") +var ErrFirebaseInstallationAuth = errors.New("firebase installation authorization rejected") func NewFirebaseInstallationID() (string, error) { id, err := uuid.NewRandom() @@ -185,7 +185,7 @@ func postAndroidRequest(ctx context.Context, endpoint string, body io.Reader, he if errors.As(err, &urlError) { err = urlError.Err } - return nil, fmt.Errorf("Android registration request failed: %w", err) + return nil, fmt.Errorf("android registration request failed: %w", err) } return response, nil } diff --git a/gcm.go b/gcm.go index 8ddc357..7456fb8 100644 --- a/gcm.go +++ b/gcm.go @@ -154,7 +154,7 @@ func UnregisterGCM(ctx context.Context, authorizationEntity string, creds GCMCre } if android { if !response.Has("deleted") { - return errors.New("Android FCM unregister response missing confirmation") + return errors.New("android FCM unregister response missing confirmation") } } else if response.Get("token") == "" && response.Get("deleted") != appID { return errors.New("GCM unregister response missing confirmation") diff --git a/pb/checkin/android_checkin.pb.go b/pb/checkin/android_checkin.pb.go index 8c77fb1..b295666 100644 --- a/pb/checkin/android_checkin.pb.go +++ b/pb/checkin/android_checkin.pb.go @@ -14,10 +14,11 @@ package checkin_proto import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( From 9d387f9087b1c0d0ce424f9d400d090d7e5b4aed Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Sat, 12 Sep 2026 20:04:01 -0400 Subject: [PATCH 5/6] fcm: remove Android application registrations --- android.go | 144 ---------------- checkin.go | 93 ++--------- fcm.go | 1 - firebase.go | 191 --------------------- gcm.go | 142 +++++----------- pb/checkin/android_checkin.pb.go | 279 ++++++------------------------- proto/android_checkin.proto | 16 -- 7 files changed, 95 insertions(+), 771 deletions(-) delete mode 100644 android.go delete mode 100644 firebase.go diff --git a/android.go b/android.go deleted file mode 100644 index 74a474d..0000000 --- a/android.go +++ /dev/null @@ -1,144 +0,0 @@ -package pushreceiver - -import ( - "crypto/sha1" - "encoding/base64" - "encoding/hex" - "fmt" - "net/url" - "strconv" - "strings" - "time" -) - -type AndroidBuild struct { - Fingerprint string `json:"fingerprint"` - Hardware string `json:"hardware"` - Brand string `json:"brand"` - Bootloader string `json:"bootloader"` - Timestamp uint64 `json:"timestamp"` - GMSVersion uint64 `json:"gms_version"` - Device string `json:"device"` - SDKVersion uint32 `json:"sdk_version"` - Model string `json:"model"` - Manufacturer string `json:"manufacturer"` - Product string `json:"product"` - SecurityPatch string `json:"security_patch"` -} - -type AndroidCheckInConfig struct { - Build AndroidBuild `json:"build"` - Locale string `json:"locale"` - TimeZone string `json:"time_zone"` -} - -type AndroidFCMConfig struct { - PackageName string `json:"package_name"` - CertificateSHA1 string `json:"certificate_sha1"` - APIKey string `json:"api_key"` - FirebaseAppID string `json:"firebase_app_id"` - FirebaseProjectID string `json:"firebase_project_id"` - FirebaseAppName string `json:"firebase_app_name,omitempty"` - FirebaseInstallationSDKVersion string `json:"firebase_installation_sdk_version"` - FirebaseClientVersion string `json:"firebase_client_version"` - FirebaseClient string `json:"firebase_client,omitempty"` - FirebaseClientLogType string `json:"firebase_client_log_type,omitempty"` - AppVersionCode uint64 `json:"app_version_code"` - AppVersionName string `json:"app_version_name"` - TargetSDKVersion uint32 `json:"target_sdk_version"` - CheckIn AndroidCheckInConfig `json:"checkin"` -} - -func validMetadata(value string, required bool, maxLength int) bool { - if required && value == "" || len(value) > maxLength { - return false - } - return !strings.ContainsAny(value, "\x00\r\n") -} - -func (config AndroidCheckInConfig) Validate() error { - for _, value := range []string{ - config.Build.Fingerprint, config.Build.Hardware, config.Build.Brand, - config.Build.Bootloader, config.Build.Device, config.Build.Model, - config.Build.Manufacturer, config.Build.Product, config.Build.SecurityPatch, - config.Locale, config.TimeZone, - } { - if !validMetadata(value, false, 1024) { - return fmt.Errorf("invalid Android checkin metadata") - } - } - return nil -} - -func (config AndroidFCMConfig) Validate() error { - for _, value := range []string{ - config.PackageName, config.APIKey, config.FirebaseAppID, config.FirebaseProjectID, - config.FirebaseInstallationSDKVersion, config.FirebaseClientVersion, - } { - if !validMetadata(value, true, 1024) { - return fmt.Errorf("invalid Android FCM application metadata") - } - } - certificate, err := hex.DecodeString(config.CertificateSHA1) - if err != nil || len(certificate) != sha1.Size { - return fmt.Errorf("invalid Android application certificate fingerprint") - } - if strings.ContainsAny(config.FirebaseProjectID, "/\\?#%") || config.AppVersionCode == 0 || - config.TargetSDKVersion == 0 || config.CheckIn.Build.GMSVersion == 0 || config.CheckIn.Build.SDKVersion == 0 { - return fmt.Errorf("invalid Android FCM project or version") - } - if !validMetadata(config.AppVersionName, false, 1024) || !validMetadata(config.FirebaseAppName, false, 1024) || - !validMetadata(config.FirebaseClient, false, 16384) || - !validMetadata(config.FirebaseClientLogType, false, 16) { - return fmt.Errorf("invalid Android Firebase client metadata") - } - return config.CheckIn.Validate() -} - -func androidRegistrationValues(sender string, creds GCMCredentials, config *AndroidFCMConfig, installation *FirebaseInstallation, requireAuth bool) (url.Values, error) { - if err := config.Validate(); err != nil { - return nil, err - } - if value, err := strconv.ParseUint(sender, 10, 64); err != nil || value == 0 { - return nil, fmt.Errorf("invalid Android FCM sender") - } - if creds.AndroidID == 0 || creds.SecurityToken == 0 || !validMetadata(creds.VersionInfo, true, 16384) { - return nil, fmt.Errorf("incomplete Android checkin credentials") - } - if installation == nil || !validFirebaseInstallationID(installation.FID) || requireAuth && !installation.AuthTokenValid() { - return nil, fmt.Errorf("firebase installation authorization is missing or expiring") - } - appName := config.FirebaseAppName - if appName == "" { - appName = "[DEFAULT]" - } - appNameHash := sha1.Sum([]byte(appName)) - appVersion := strconv.FormatUint(config.AppVersionCode, 10) - gmsVersion := strconv.FormatUint(config.CheckIn.Build.GMSVersion, 10) - values := url.Values{ - "sender": {sender}, "X-subtype": {sender}, "X-scope": {"*"}, - "X-appid": {installation.FID}, - "app": {config.PackageName}, "cert": {strings.ToLower(config.CertificateSHA1)}, - "device": {strconv.FormatUint(creds.AndroidID, 10)}, "info": {creds.VersionInfo}, - "app_ver": {appVersion}, "X-app_ver": {appVersion}, "X-app_ver_name": {config.AppVersionName}, - "gcm_ver": {gmsVersion}, "X-gmsv": {gmsVersion}, "plat": {"0"}, - "target_ver": {strconv.FormatUint(uint64(config.TargetSDKVersion), 10)}, - "X-osv": {strconv.FormatUint(uint64(config.CheckIn.Build.SDKVersion), 10)}, - "X-cliv": {config.FirebaseClientVersion}, "X-gmp_app_id": {config.FirebaseAppID}, - "X-firebase-app-name-hash": {base64.RawURLEncoding.EncodeToString(appNameHash[:])}, - } - if installation.AuthTokenValid() { - values.Set("X-Goog-Firebase-Installations-Auth", installation.AuthToken) - } - if config.FirebaseClient != "" { - values.Set("X-Firebase-Client", config.FirebaseClient) - } - if config.FirebaseClientLogType != "" { - values.Set("X-Firebase-Client-Log-Type", config.FirebaseClientLogType) - } - return values, nil -} - -func (installation FirebaseInstallation) AuthTokenValid() bool { - return validMetadata(installation.AuthToken, true, 16384) && time.Until(installation.AuthTokenExpiresAt) >= time.Hour -} diff --git a/checkin.go b/checkin.go index a6af1c2..05df29d 100644 --- a/checkin.go +++ b/checkin.go @@ -3,7 +3,6 @@ package pushreceiver import ( "bytes" "context" - "fmt" "io" "net/http" @@ -12,45 +11,21 @@ import ( "google.golang.org/protobuf/proto" ) -func CheckIn(ctx context.Context, opt *GCMCredentials, options ...*AndroidCheckInConfig) (*GCMCredentials, error) { - if opt == nil { - opt = &GCMCredentials{} - } - if len(options) > 1 { - return nil, fmt.Errorf("multiple Android checkin configurations") - } - var config *AndroidCheckInConfig - if len(options) == 1 { - config = options[0] - } - if config != nil { - if err := config.Validate(); err != nil { - return nil, err - } - } - resp, err := checkin(ctx, opt, config) +func CheckIn(ctx context.Context, opt *GCMCredentials) (*GCMCredentials, error) { + resp, err := checkin(ctx, opt) if err != nil { return nil, err } - creds := *opt - if resp.AndroidId != nil { - creds.AndroidID = resp.GetAndroidId() - } - if resp.SecurityToken != nil { - creds.SecurityToken = resp.GetSecurityToken() - } - if resp.VersionInfo != nil { - creds.VersionInfo = resp.GetVersionInfo() - } - if creds.AndroidID == 0 || creds.SecurityToken == 0 || config != nil && creds.VersionInfo == "" { - return nil, fmt.Errorf("incomplete GCM checkin response") + creds := &GCMCredentials{ + AndroidID: *resp.AndroidId, + SecurityToken: *resp.SecurityToken, } - return &creds, nil + return creds, nil } -func checkin(ctx context.Context, opt *GCMCredentials, config *AndroidCheckInConfig) (*pb.AndroidCheckinResponse, error) { +func checkin(ctx context.Context, opt *GCMCredentials) (*pb.AndroidCheckinResponse, error) { id := int64(opt.AndroidID) r := &pb.AndroidCheckinRequest{ Checkin: &pb.AndroidCheckinProto{ @@ -68,79 +43,31 @@ func checkin(ctx context.Context, opt *GCMCredentials, config *AndroidCheckInCon Id: &id, SecurityToken: &opt.SecurityToken, } - endpoint := checkinURL - post := postRequest - if config != nil { - build := config.Build - optionalString := func(value string) *string { - if value == "" { - return nil - } - return &value - } - r.Checkin = &pb.AndroidCheckinProto{ - UserNumber: proto.Int32(0), - Build: &pb.AndroidBuildProto{ - Fingerprint: optionalString(build.Fingerprint), Hardware: optionalString(build.Hardware), - Brand: optionalString(build.Brand), Bootloader: optionalString(build.Bootloader), - Device: optionalString(build.Device), Model: optionalString(build.Model), - Manufacturer: optionalString(build.Manufacturer), Product: optionalString(build.Product), - SecurityPatch: optionalString(build.SecurityPatch), - }, - } - if build.Timestamp != 0 { - r.Checkin.Build.Timestamp = proto.Uint64(build.Timestamp) - } - if build.GMSVersion != 0 { - r.Checkin.Build.GmsVersion = proto.Uint64(build.GMSVersion) - } - if build.SDKVersion != 0 { - r.Checkin.Build.SdkVersion = proto.Uint32(build.SDKVersion) - } - r.Locale = optionalString(config.Locale) - r.TimeZone = optionalString(config.TimeZone) - endpoint = "https://android.googleapis.com/checkin" - post = postAndroidRequest - } message, err := proto.Marshal(r) if err != nil { return nil, errors.Wrap(err, "marshal GCM checkin request") } - res, err := post(ctx, endpoint, bytes.NewReader(message), func(header *http.Header) { + res, err := postRequest(ctx, checkinURL, bytes.NewReader(message), func(header *http.Header) { header.Set("Content-Type", "application/x-protobuf") }) if err != nil { return nil, errors.Wrap(err, "request GCM checkin") } - if config != nil { - defer res.Body.Close() - } else { - defer closeResponse(res) - } + defer closeResponse(res) // unauthorized error if res.StatusCode == http.StatusUnauthorized { return nil, ErrGcmAuthorization } if res.StatusCode < 200 || res.StatusCode > 299 { - if config != nil { - return nil, fmt.Errorf("android checkin HTTP status %d", res.StatusCode) - } return nil, errors.Errorf("server error: %s", res.Status) } - var body io.Reader = res.Body - if config != nil { - body = io.LimitReader(body, 4*1024*1024+1) - } - data, err := io.ReadAll(body) + data, err := io.ReadAll(res.Body) if err != nil { return nil, errors.Wrap(err, "read GCM checkin response") } - if config != nil && len(data) > 4*1024*1024 { - return nil, fmt.Errorf("GCM checkin response too large") - } var responseProto pb.AndroidCheckinResponse err = proto.Unmarshal(data, &responseProto) diff --git a/fcm.go b/fcm.go index 0b925be..581a55e 100644 --- a/fcm.go +++ b/fcm.go @@ -17,5 +17,4 @@ type FCMCredentials struct { type GCMCredentials struct { AndroidID uint64 `json:"androidId"` SecurityToken uint64 `json:"securityToken"` - VersionInfo string `json:"versionInfo,omitempty"` } diff --git a/firebase.go b/firebase.go deleted file mode 100644 index f50330b..0000000 --- a/firebase.go +++ /dev/null @@ -1,191 +0,0 @@ -package pushreceiver - -import ( - "bytes" - "compress/gzip" - "context" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" - "strconv" - "strings" - "time" - - "github.com/google/uuid" -) - -type FirebaseInstallation struct { - FID string `json:"fid"` - RefreshToken string `json:"refresh_token"` - AuthToken string `json:"auth_token"` - AuthTokenExpiresAt time.Time `json:"auth_token_expires_at"` -} - -type FirebaseError int - -func (err FirebaseError) Error() string { - return fmt.Sprintf("firebase installation HTTP status %d", int(err)) -} - -var ErrFirebaseInstallationAuth = errors.New("firebase installation authorization rejected") - -func NewFirebaseInstallationID() (string, error) { - id, err := uuid.NewRandom() - if err != nil { - return "", err - } - var data [17]byte - copy(data[:16], id[:]) - data[16] = data[0] - data[0] = data[0]&15 | 112 - return base64.RawURLEncoding.EncodeToString(data[:])[:22], nil -} - -func validFirebaseInstallationID(fid string) bool { - return len(fid) == 22 && strings.Trim(fid, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") == "" -} - -type firebaseAuthToken struct { - Token string `json:"token"` - ExpiresIn string `json:"expiresIn"` -} - -func (token firebaseAuthToken) expiration(issuedAt time.Time) (time.Time, error) { - seconds, err := strconv.ParseUint(strings.TrimSuffix(token.ExpiresIn, "s"), 10, 32) - if err != nil || !strings.HasSuffix(token.ExpiresIn, "s") || seconds == 0 || !validMetadata(token.Token, true, 16384) { - return time.Time{}, fmt.Errorf("invalid Firebase installation auth response") - } - return issuedAt.Add(time.Duration(seconds) * time.Second), nil -} - -func CreateFirebaseInstallation(ctx context.Context, config *AndroidFCMConfig, fid string) (*FirebaseInstallation, error) { - if config == nil { - return nil, fmt.Errorf("missing Android FCM configuration") - } - if err := config.Validate(); err != nil { - return nil, err - } - if !validFirebaseInstallationID(fid) { - return nil, fmt.Errorf("invalid Firebase installation ID") - } - body := map[string]string{ - "fid": fid, "appId": config.FirebaseAppID, "authVersion": "FIS_v2", "sdkVersion": config.FirebaseInstallationSDKVersion, - } - var result struct { - FID string `json:"fid"` - RefreshToken string `json:"refreshToken"` - AuthToken firebaseAuthToken `json:"authToken"` - } - issuedAt := time.Now() - if err := firebaseRequest(ctx, config, "", "", body, &result); err != nil { - return nil, err - } - if !validFirebaseInstallationID(result.FID) || !validMetadata(result.RefreshToken, true, 16384) { - return nil, fmt.Errorf("incomplete Firebase installation response") - } - expiration, err := result.AuthToken.expiration(issuedAt) - if err != nil { - return nil, err - } - return &FirebaseInstallation{result.FID, result.RefreshToken, result.AuthToken.Token, expiration}, nil -} - -func RefreshFirebaseInstallation(ctx context.Context, config *AndroidFCMConfig, installation FirebaseInstallation) (*FirebaseInstallation, error) { - if config == nil { - return nil, fmt.Errorf("missing Android FCM configuration") - } - if err := config.Validate(); err != nil { - return nil, err - } - if !validFirebaseInstallationID(installation.FID) || !validMetadata(installation.RefreshToken, true, 16384) { - return nil, fmt.Errorf("incomplete Firebase installation credentials") - } - body := map[string]any{"installation": map[string]string{"sdkVersion": config.FirebaseInstallationSDKVersion}} - var token firebaseAuthToken - issuedAt := time.Now() - if err := firebaseRequest(ctx, config, installation.FID, installation.RefreshToken, body, &token); err != nil { - var status FirebaseError - if errors.As(err, &status) && (status == http.StatusUnauthorized || status == http.StatusNotFound) { - return nil, fmt.Errorf("%w: %w", ErrFirebaseInstallationAuth, err) - } - return nil, err - } - expiration, err := token.expiration(issuedAt) - if err != nil { - return nil, err - } - installation.AuthToken = token.Token - installation.AuthTokenExpiresAt = expiration - return &installation, nil -} - -func firebaseRequest(ctx context.Context, config *AndroidFCMConfig, fid, refreshToken string, body, result any) error { - data, err := json.Marshal(body) - if err != nil { - return fmt.Errorf("encode Firebase installation request") - } - var compressed bytes.Buffer - writer := gzip.NewWriter(&compressed) - if _, err = writer.Write(data); err != nil { - return err - } - if err = writer.Close(); err != nil { - return err - } - endpoint := "https://firebaseinstallations.googleapis.com/v1/projects/" + url.PathEscape(config.FirebaseProjectID) + "/installations" - if fid != "" { - endpoint += "/" + url.PathEscape(fid) + "/authTokens:generate" - } - response, err := postAndroidRequest(ctx, endpoint, &compressed, func(header *http.Header) { - header.Set("Content-Type", "application/json") - header.Set("Accept", "application/json") - header.Set("Content-Encoding", "gzip") - header.Set("Cache-Control", "no-cache") - header.Set("X-Android-Package", config.PackageName) - header.Set("X-Android-Cert", strings.ToUpper(config.CertificateSHA1)) - header.Set("X-Goog-Api-Key", config.APIKey) - if refreshToken != "" { - header.Set("Authorization", "FIS_v2 "+refreshToken) - } - }) - if err != nil { - return err - } - defer response.Body.Close() - if response.StatusCode < 200 || response.StatusCode >= 300 { - return FirebaseError(response.StatusCode) - } - data, err = io.ReadAll(io.LimitReader(response.Body, 65537)) - if err != nil { - return fmt.Errorf("read Firebase installation response: %w", err) - } - if len(data) > 65536 || json.Unmarshal(data, result) != nil { - return fmt.Errorf("invalid Firebase installation response") - } - return nil -} - -func postAndroidRequest(ctx context.Context, endpoint string, body io.Reader, headerSetter func(*http.Header)) (*http.Response, error) { - request, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body) - if err != nil { - return nil, fmt.Errorf("create Android registration request") - } - headerSetter(&request.Header) - client := &http.Client{ - Timeout: 30 * time.Second, - CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }, - } - response, err := client.Do(request) - if err != nil { - var urlError *url.Error - if errors.As(err, &urlError) { - err = urlError.Err - } - return nil, fmt.Errorf("android registration request failed: %w", err) - } - return response, nil -} diff --git a/gcm.go b/gcm.go index 7456fb8..822c049 100644 --- a/gcm.go +++ b/gcm.go @@ -22,11 +22,9 @@ import ( ) type GCMRegistrationOpts struct { - AppID string - InstanceID string - Expiry time.Duration - Android *AndroidFCMConfig - Installation *FirebaseInstallation + AppID string + InstanceID string + Expiry time.Duration } func setGCMAppID(values url.Values, appID string) { @@ -61,40 +59,30 @@ func RegisterGCM(ctx context.Context, authorizationEntity string, creds GCMCrede appID = NewGCMAppID(authorizationEntity) } - endpoint := registerURL - post := postRequest - if opts != nil && opts.Android != nil { - var err error - values, err = androidRegistrationValues(authorizationEntity, creds, opts.Android, opts.Installation, true) - if err != nil { - return nil, err - } - endpoint = "https://android.apis.google.com/c2dm/register3" - post = postAndroidRequest - } else { - if opts != nil && opts.InstanceID != "" { - values.Set("appid", opts.InstanceID) - } - if opts != nil && opts.Expiry != 0 { - ttl := strconv.Itoa(int(opts.Expiry.Seconds())) - values.Set("ttl", ttl) - } - setGCMAppID(values, appID) - values.Set("scope", "GCM") - values.Set("X-scope", "GCM") - values.Set("device", fmt.Sprint(creds.AndroidID)) - values.Set("gmsv", strings.Split(chromeVersion, ".")[0]) - values.Set("sender", authorizationEntity) - } - - res, err := post(ctx, endpoint, strings.NewReader(values.Encode()), func(header *http.Header) { + if opts != nil && opts.InstanceID != "" { + values.Set("appid", opts.InstanceID) + } + + if opts != nil && opts.Expiry != 0 { + ttl := strconv.Itoa(int(opts.Expiry.Seconds())) + values.Set("ttl", ttl) + } + + setGCMAppID(values, appID) + values.Set("scope", "GCM") + values.Set("X-scope", "GCM") + values.Set("device", fmt.Sprint(creds.AndroidID)) + values.Set("gmsv", strings.Split(chromeVersion, ".")[0]) + values.Set("sender", authorizationEntity) + + res, err := postRequest(ctx, registerURL, strings.NewReader(values.Encode()), func(header *http.Header) { header.Set("Content-Type", "application/x-www-form-urlencoded") header.Set("Authorization", fmt.Sprintf("AidLogin %d:%d", creds.AndroidID, creds.SecurityToken)) }) if err != nil { return nil, errors.Wrap(err, "request GCM register") } - subscription, err := parseGCMResponse(res, opts != nil && opts.Android != nil) + subscription, err := parseGCMResponse(res) if err != nil { return nil, errors.Wrap(err, "read GCM register response") } @@ -110,37 +98,16 @@ func RegisterGCM(ctx context.Context, authorizationEntity string, creds GCMCrede }, nil } -func UnregisterGCM(ctx context.Context, authorizationEntity string, creds GCMCredentials, appID string, options ...*GCMRegistrationOpts) error { - if len(options) > 1 { - return errors.New("multiple GCM unregistration options") - } - var opts *GCMRegistrationOpts - if len(options) == 1 { - opts = options[0] - } - android := opts != nil && opts.Android != nil +func UnregisterGCM(ctx context.Context, authorizationEntity string, creds GCMCredentials, appID string) error { values := url.Values{} - endpoint := registerURL - post := postRequest - if android { - var err error - values, err = androidRegistrationValues(authorizationEntity, creds, opts.Android, opts.Installation, false) - if err != nil { - return err - } - values.Set("X-delete", "1") - endpoint = "https://android.apis.google.com/c2dm/register3" - post = postAndroidRequest - } else { - setGCMAppID(values, appID) - values.Set("scope", "GCM") - values.Set("X-scope", "GCM") - values.Set("device", fmt.Sprint(creds.AndroidID)) - values.Set("gmsv", strings.Split(chromeVersion, ".")[0]) - values.Set("sender", authorizationEntity) - values.Set("delete", "true") - } - res, err := post(ctx, endpoint, strings.NewReader(values.Encode()), func(header *http.Header) { + setGCMAppID(values, appID) + values.Set("scope", "GCM") + values.Set("X-scope", "GCM") + values.Set("device", fmt.Sprint(creds.AndroidID)) + values.Set("gmsv", strings.Split(chromeVersion, ".")[0]) + values.Set("sender", authorizationEntity) + values.Set("delete", "true") + res, err := postRequest(ctx, registerURL, strings.NewReader(values.Encode()), func(header *http.Header) { header.Set("Content-Type", "application/x-www-form-urlencoded") header.Set("Authorization", fmt.Sprintf("AidLogin %d:%d", creds.AndroidID, creds.SecurityToken)) }) @@ -148,64 +115,31 @@ func UnregisterGCM(ctx context.Context, authorizationEntity string, creds GCMCre return errors.Wrap(err, "failed to unregister with GCM") } - response, err := parseGCMResponse(res, android) + response, err := parseGCMResponse(res) if err != nil { return errors.Wrap(err, "read GCM unregister response") } - if android { - if !response.Has("deleted") { - return errors.New("android FCM unregister response missing confirmation") - } - } else if response.Get("token") == "" && response.Get("deleted") != appID { + if response.Get("token") == "" && response.Get("deleted") != appID { return errors.New("GCM unregister response missing confirmation") } return nil } -func parseGCMResponse(res *http.Response, android bool) (url.Values, error) { - if android { - defer res.Body.Close() - } else { - defer closeResponse(res) - } +func parseGCMResponse(res *http.Response) (url.Values, error) { + defer closeResponse(res) if res.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected HTTP status %d", res.StatusCode) } - var body io.Reader = res.Body - if android { - body = io.LimitReader(body, 65537) - } - data, err := io.ReadAll(body) + data, err := io.ReadAll(res.Body) if err != nil { return nil, err } - if android && len(data) > 65536 { - return nil, errors.New("GCM response too large") - } - var values url.Values - if android { - values = url.Values{} - for _, line := range strings.Split(string(data), "\n") { - if line == "" { - continue - } - key, value, found := strings.Cut(line, "=") - if !found || key == "" { - return nil, errors.New("invalid Android FCM response") - } - values.Set(key, value) - } - } else { - values, err = url.ParseQuery(string(data)) - if err != nil { - return nil, err - } + values, err := url.ParseQuery(string(data)) + if err != nil { + return nil, err } if message := values.Get("Error"); message != "" { - if android && (len(message) > 128 || strings.Trim(message, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_") != "") { - return nil, errors.New("GCM response contains an invalid error code") - } return nil, GCMError(message) } return values, nil diff --git a/pb/checkin/android_checkin.pb.go b/pb/checkin/android_checkin.pb.go index b295666..2b01e28 100644 --- a/pb/checkin/android_checkin.pb.go +++ b/pb/checkin/android_checkin.pb.go @@ -7,8 +7,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.34.2 -// protoc v5.28.2 +// protoc-gen-go v1.31.0 +// protoc v4.24.2 // source: android_checkin.proto package checkin_proto @@ -298,148 +298,12 @@ func (x *ChromeBuildProto) GetChannel() ChromeBuildProto_Channel { return ChromeBuildProto_CHANNEL_STABLE } -type AndroidBuildProto struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Fingerprint *string `protobuf:"bytes,1,opt,name=fingerprint" json:"fingerprint,omitempty"` - Hardware *string `protobuf:"bytes,2,opt,name=hardware" json:"hardware,omitempty"` - Brand *string `protobuf:"bytes,3,opt,name=brand" json:"brand,omitempty"` - Bootloader *string `protobuf:"bytes,5,opt,name=bootloader" json:"bootloader,omitempty"` - Timestamp *uint64 `protobuf:"varint,7,opt,name=timestamp" json:"timestamp,omitempty"` - GmsVersion *uint64 `protobuf:"varint,8,opt,name=gms_version,json=gmsVersion" json:"gms_version,omitempty"` - Device *string `protobuf:"bytes,9,opt,name=device" json:"device,omitempty"` - SdkVersion *uint32 `protobuf:"varint,10,opt,name=sdk_version,json=sdkVersion" json:"sdk_version,omitempty"` - Model *string `protobuf:"bytes,11,opt,name=model" json:"model,omitempty"` - Manufacturer *string `protobuf:"bytes,12,opt,name=manufacturer" json:"manufacturer,omitempty"` - Product *string `protobuf:"bytes,13,opt,name=product" json:"product,omitempty"` - SecurityPatch *string `protobuf:"bytes,19,opt,name=security_patch,json=securityPatch" json:"security_patch,omitempty"` -} - -func (x *AndroidBuildProto) Reset() { - *x = AndroidBuildProto{} - if protoimpl.UnsafeEnabled { - mi := &file_android_checkin_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *AndroidBuildProto) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AndroidBuildProto) ProtoMessage() {} - -func (x *AndroidBuildProto) ProtoReflect() protoreflect.Message { - mi := &file_android_checkin_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AndroidBuildProto.ProtoReflect.Descriptor instead. -func (*AndroidBuildProto) Descriptor() ([]byte, []int) { - return file_android_checkin_proto_rawDescGZIP(), []int{1} -} - -func (x *AndroidBuildProto) GetFingerprint() string { - if x != nil && x.Fingerprint != nil { - return *x.Fingerprint - } - return "" -} - -func (x *AndroidBuildProto) GetHardware() string { - if x != nil && x.Hardware != nil { - return *x.Hardware - } - return "" -} - -func (x *AndroidBuildProto) GetBrand() string { - if x != nil && x.Brand != nil { - return *x.Brand - } - return "" -} - -func (x *AndroidBuildProto) GetBootloader() string { - if x != nil && x.Bootloader != nil { - return *x.Bootloader - } - return "" -} - -func (x *AndroidBuildProto) GetTimestamp() uint64 { - if x != nil && x.Timestamp != nil { - return *x.Timestamp - } - return 0 -} - -func (x *AndroidBuildProto) GetGmsVersion() uint64 { - if x != nil && x.GmsVersion != nil { - return *x.GmsVersion - } - return 0 -} - -func (x *AndroidBuildProto) GetDevice() string { - if x != nil && x.Device != nil { - return *x.Device - } - return "" -} - -func (x *AndroidBuildProto) GetSdkVersion() uint32 { - if x != nil && x.SdkVersion != nil { - return *x.SdkVersion - } - return 0 -} - -func (x *AndroidBuildProto) GetModel() string { - if x != nil && x.Model != nil { - return *x.Model - } - return "" -} - -func (x *AndroidBuildProto) GetManufacturer() string { - if x != nil && x.Manufacturer != nil { - return *x.Manufacturer - } - return "" -} - -func (x *AndroidBuildProto) GetProduct() string { - if x != nil && x.Product != nil { - return *x.Product - } - return "" -} - -func (x *AndroidBuildProto) GetSecurityPatch() string { - if x != nil && x.SecurityPatch != nil { - return *x.SecurityPatch - } - return "" -} - // Information sent by the device in a "checkin" request. type AndroidCheckinProto struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Build *AndroidBuildProto `protobuf:"bytes,1,opt,name=build" json:"build,omitempty"` // Miliseconds since the Unix epoch of the device's last successful checkin. LastCheckinMsec *int64 `protobuf:"varint,2,opt,name=last_checkin_msec,json=lastCheckinMsec" json:"last_checkin_msec,omitempty"` // The current MCC+MNC of the mobile device's current cell. @@ -474,7 +338,7 @@ const ( func (x *AndroidCheckinProto) Reset() { *x = AndroidCheckinProto{} if protoimpl.UnsafeEnabled { - mi := &file_android_checkin_proto_msgTypes[2] + mi := &file_android_checkin_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -487,7 +351,7 @@ func (x *AndroidCheckinProto) String() string { func (*AndroidCheckinProto) ProtoMessage() {} func (x *AndroidCheckinProto) ProtoReflect() protoreflect.Message { - mi := &file_android_checkin_proto_msgTypes[2] + mi := &file_android_checkin_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -500,14 +364,7 @@ func (x *AndroidCheckinProto) ProtoReflect() protoreflect.Message { // Deprecated: Use AndroidCheckinProto.ProtoReflect.Descriptor instead. func (*AndroidCheckinProto) Descriptor() ([]byte, []int) { - return file_android_checkin_proto_rawDescGZIP(), []int{2} -} - -func (x *AndroidCheckinProto) GetBuild() *AndroidBuildProto { - if x != nil { - return x.Build - } - return nil + return file_android_checkin_proto_rawDescGZIP(), []int{1} } func (x *AndroidCheckinProto) GetLastCheckinMsec() int64 { @@ -591,63 +448,35 @@ var file_android_checkin_proto_rawDesc = []byte{ 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x44, 0x45, 0x56, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x43, 0x41, 0x4e, 0x41, 0x52, 0x59, 0x10, 0x04, 0x12, 0x13, 0x0a, 0x0f, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, - 0x4f, 0x57, 0x4e, 0x10, 0x05, 0x22, 0xfa, 0x02, 0x0a, 0x11, 0x41, 0x6e, 0x64, 0x72, 0x6f, 0x69, - 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x20, 0x0a, 0x0b, 0x66, - 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x12, 0x1a, 0x0a, - 0x08, 0x68, 0x61, 0x72, 0x64, 0x77, 0x61, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x68, 0x61, 0x72, 0x64, 0x77, 0x61, 0x72, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x72, 0x61, - 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x62, 0x72, 0x61, 0x6e, 0x64, 0x12, - 0x1e, 0x0a, 0x0a, 0x62, 0x6f, 0x6f, 0x74, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x72, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0a, 0x62, 0x6f, 0x6f, 0x74, 0x6c, 0x6f, 0x61, 0x64, 0x65, 0x72, 0x12, - 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1f, 0x0a, - 0x0b, 0x67, 0x6d, 0x73, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x0a, 0x67, 0x6d, 0x73, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, - 0x0a, 0x06, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x64, 0x6b, 0x5f, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73, 0x64, 0x6b, - 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, - 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x12, 0x22, 0x0a, - 0x0c, 0x6d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, 0x72, 0x18, 0x0c, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d, 0x61, 0x6e, 0x75, 0x66, 0x61, 0x63, 0x74, 0x75, 0x72, 0x65, - 0x72, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x18, 0x0d, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x73, - 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x70, 0x61, 0x74, 0x63, 0x68, 0x18, 0x13, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x50, 0x61, 0x74, - 0x63, 0x68, 0x22, 0x82, 0x03, 0x0a, 0x13, 0x41, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x43, 0x68, - 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x36, 0x0a, 0x05, 0x62, 0x75, - 0x69, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x63, 0x68, 0x65, 0x63, - 0x6b, 0x69, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x6e, 0x64, 0x72, 0x6f, 0x69, - 0x64, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x52, 0x05, 0x62, 0x75, 0x69, - 0x6c, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x63, 0x68, 0x65, 0x63, 0x6b, - 0x69, 0x6e, 0x5f, 0x6d, 0x73, 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x6c, - 0x61, 0x73, 0x74, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x4d, 0x73, 0x65, 0x63, 0x12, 0x23, - 0x0a, 0x0d, 0x63, 0x65, 0x6c, 0x6c, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x65, 0x6c, 0x6c, 0x4f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x6f, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x69, 0x6d, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, - 0x74, 0x6f, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x69, 0x6d, 0x4f, 0x70, - 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x6f, 0x61, 0x6d, 0x69, 0x6e, - 0x67, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x6f, 0x61, 0x6d, 0x69, 0x6e, 0x67, - 0x12, 0x1f, 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, - 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x4e, 0x75, 0x6d, 0x62, 0x65, - 0x72, 0x12, 0x40, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x19, 0x2e, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x3a, 0x11, 0x44, 0x45, 0x56, 0x49, - 0x43, 0x45, 0x5f, 0x41, 0x4e, 0x44, 0x52, 0x4f, 0x49, 0x44, 0x5f, 0x4f, 0x53, 0x52, 0x04, 0x74, - 0x79, 0x70, 0x65, 0x12, 0x42, 0x0a, 0x0c, 0x63, 0x68, 0x72, 0x6f, 0x6d, 0x65, 0x5f, 0x62, 0x75, - 0x69, 0x6c, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x68, 0x65, 0x63, - 0x6b, 0x69, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x68, 0x72, 0x6f, 0x6d, 0x65, - 0x42, 0x75, 0x69, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x52, 0x0b, 0x63, 0x68, 0x72, 0x6f, - 0x6d, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x2a, 0x67, 0x0a, 0x0a, 0x44, 0x65, 0x76, 0x69, 0x63, - 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x15, 0x0a, 0x11, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, - 0x41, 0x4e, 0x44, 0x52, 0x4f, 0x49, 0x44, 0x5f, 0x4f, 0x53, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, - 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x49, 0x4f, 0x53, 0x5f, 0x4f, 0x53, 0x10, 0x02, 0x12, - 0x19, 0x0a, 0x15, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x48, 0x52, 0x4f, 0x4d, 0x45, - 0x5f, 0x42, 0x52, 0x4f, 0x57, 0x53, 0x45, 0x52, 0x10, 0x03, 0x12, 0x14, 0x0a, 0x10, 0x44, 0x45, - 0x56, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x48, 0x52, 0x4f, 0x4d, 0x45, 0x5f, 0x4f, 0x53, 0x10, 0x04, - 0x42, 0x13, 0x48, 0x03, 0x5a, 0x0f, 0x2e, 0x3b, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x5f, - 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x4f, 0x57, 0x4e, 0x10, 0x05, 0x22, 0xca, 0x02, 0x0a, 0x13, 0x41, 0x6e, 0x64, 0x72, 0x6f, 0x69, + 0x64, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x2a, 0x0a, + 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x5f, 0x6d, 0x73, + 0x65, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x43, 0x68, + 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x4d, 0x73, 0x65, 0x63, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x65, 0x6c, + 0x6c, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x63, 0x65, 0x6c, 0x6c, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x21, + 0x0a, 0x0c, 0x73, 0x69, 0x6d, 0x5f, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x69, 0x6d, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, + 0x72, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x6f, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x72, 0x6f, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x1f, 0x0a, 0x0b, 0x75, + 0x73, 0x65, 0x72, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0a, 0x75, 0x73, 0x65, 0x72, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x40, 0x0a, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x63, 0x68, 0x65, + 0x63, 0x6b, 0x69, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x44, 0x65, 0x76, 0x69, 0x63, + 0x65, 0x54, 0x79, 0x70, 0x65, 0x3a, 0x11, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x41, 0x4e, + 0x44, 0x52, 0x4f, 0x49, 0x44, 0x5f, 0x4f, 0x53, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x42, + 0x0a, 0x0c, 0x63, 0x68, 0x72, 0x6f, 0x6d, 0x65, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x18, 0x0d, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x5f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x68, 0x72, 0x6f, 0x6d, 0x65, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x52, 0x0b, 0x63, 0x68, 0x72, 0x6f, 0x6d, 0x65, 0x42, 0x75, 0x69, + 0x6c, 0x64, 0x2a, 0x67, 0x0a, 0x0a, 0x44, 0x65, 0x76, 0x69, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x15, 0x0a, 0x11, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, 0x41, 0x4e, 0x44, 0x52, 0x4f, + 0x49, 0x44, 0x5f, 0x4f, 0x53, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x44, 0x45, 0x56, 0x49, 0x43, + 0x45, 0x5f, 0x49, 0x4f, 0x53, 0x5f, 0x4f, 0x53, 0x10, 0x02, 0x12, 0x19, 0x0a, 0x15, 0x44, 0x45, + 0x56, 0x49, 0x43, 0x45, 0x5f, 0x43, 0x48, 0x52, 0x4f, 0x4d, 0x45, 0x5f, 0x42, 0x52, 0x4f, 0x57, + 0x53, 0x45, 0x52, 0x10, 0x03, 0x12, 0x14, 0x0a, 0x10, 0x44, 0x45, 0x56, 0x49, 0x43, 0x45, 0x5f, + 0x43, 0x48, 0x52, 0x4f, 0x4d, 0x45, 0x5f, 0x4f, 0x53, 0x10, 0x04, 0x42, 0x13, 0x48, 0x03, 0x5a, + 0x0f, 0x2e, 0x3b, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x69, 0x6e, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, } var ( @@ -663,26 +492,24 @@ func file_android_checkin_proto_rawDescGZIP() []byte { } var file_android_checkin_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_android_checkin_proto_msgTypes = make([]protoimpl.MessageInfo, 3) -var file_android_checkin_proto_goTypes = []any{ +var file_android_checkin_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_android_checkin_proto_goTypes = []interface{}{ (DeviceType)(0), // 0: checkin_proto.DeviceType (ChromeBuildProto_Platform)(0), // 1: checkin_proto.ChromeBuildProto.Platform (ChromeBuildProto_Channel)(0), // 2: checkin_proto.ChromeBuildProto.Channel (*ChromeBuildProto)(nil), // 3: checkin_proto.ChromeBuildProto - (*AndroidBuildProto)(nil), // 4: checkin_proto.AndroidBuildProto - (*AndroidCheckinProto)(nil), // 5: checkin_proto.AndroidCheckinProto + (*AndroidCheckinProto)(nil), // 4: checkin_proto.AndroidCheckinProto } var file_android_checkin_proto_depIdxs = []int32{ 1, // 0: checkin_proto.ChromeBuildProto.platform:type_name -> checkin_proto.ChromeBuildProto.Platform 2, // 1: checkin_proto.ChromeBuildProto.channel:type_name -> checkin_proto.ChromeBuildProto.Channel - 4, // 2: checkin_proto.AndroidCheckinProto.build:type_name -> checkin_proto.AndroidBuildProto - 0, // 3: checkin_proto.AndroidCheckinProto.type:type_name -> checkin_proto.DeviceType - 3, // 4: checkin_proto.AndroidCheckinProto.chrome_build:type_name -> checkin_proto.ChromeBuildProto - 5, // [5:5] is the sub-list for method output_type - 5, // [5:5] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 0, // 2: checkin_proto.AndroidCheckinProto.type:type_name -> checkin_proto.DeviceType + 3, // 3: checkin_proto.AndroidCheckinProto.chrome_build:type_name -> checkin_proto.ChromeBuildProto + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name } func init() { file_android_checkin_proto_init() } @@ -691,7 +518,7 @@ func file_android_checkin_proto_init() { return } if !protoimpl.UnsafeEnabled { - file_android_checkin_proto_msgTypes[0].Exporter = func(v any, i int) any { + file_android_checkin_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ChromeBuildProto); i { case 0: return &v.state @@ -703,19 +530,7 @@ func file_android_checkin_proto_init() { return nil } } - file_android_checkin_proto_msgTypes[1].Exporter = func(v any, i int) any { - switch v := v.(*AndroidBuildProto); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_android_checkin_proto_msgTypes[2].Exporter = func(v any, i int) any { + file_android_checkin_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*AndroidCheckinProto); i { case 0: return &v.state @@ -734,7 +549,7 @@ func file_android_checkin_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_android_checkin_proto_rawDesc, NumEnums: 3, - NumMessages: 3, + NumMessages: 2, NumExtensions: 0, NumServices: 0, }, diff --git a/proto/android_checkin.proto b/proto/android_checkin.proto index fc2c33b..b68a8de 100644 --- a/proto/android_checkin.proto +++ b/proto/android_checkin.proto @@ -42,24 +42,8 @@ message ChromeBuildProto { optional Channel channel = 3; } -message AndroidBuildProto { - optional string fingerprint = 1; - optional string hardware = 2; - optional string brand = 3; - optional string bootloader = 5; - optional uint64 timestamp = 7; - optional uint64 gms_version = 8; - optional string device = 9; - optional uint32 sdk_version = 10; - optional string model = 11; - optional string manufacturer = 12; - optional string product = 13; - optional string security_patch = 19; -} - // Information sent by the device in a "checkin" request. message AndroidCheckinProto { - optional AndroidBuildProto build = 1; // Miliseconds since the Unix epoch of the device's last successful checkin. optional int64 last_checkin_msec = 2; From 2a21d84042d6776cd31bfcc368080fe9d4274906 Mon Sep 17 00:00:00 2001 From: Gerardo Rodriguez Date: Sat, 12 Sep 2026 20:05:12 -0400 Subject: [PATCH 6/6] gcm: use UUID subtypes for numeric senders --- gcm.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/gcm.go b/gcm.go index 822c049..b43de72 100644 --- a/gcm.go +++ b/gcm.go @@ -40,13 +40,7 @@ func NewGCMAppID(authorizationEntity string) string { if _, err := strconv.ParseUint(authorizationEntity, 10, 64); err != nil { return "wp:" + strings.ToUpper(uuid.New().String()) } - id := uuid.New() - var encoded [32]byte - for i, b := range id { - encoded[i*2] = 'a' + b>>4 - encoded[i*2+1] = 'a' + b&15 - } - return string(encoded[:]) + return uuid.NewString() } func RegisterGCM(ctx context.Context, authorizationEntity string, creds GCMCredentials, opts *GCMRegistrationOpts) (*FCMCredentials, error) {