package tmobile import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "sync" "time" "github.com/fiskerinc/cloud-services/pkg/grpc/sms" "github.com/fiskerinc/cloud-services/pkg/logger" "github.com/fiskerinc/cloud-services/pkg/utils/randomvalues" "github.com/pkg/errors" errorsO "errors" tmtg "github.com/fiskerinc/cloud-services/pkg/tmobtokengen" ) const ( failedToParseRequest = "failed to encode request: %v" payloadMsg = "payload: %s" contentType = "Content-Type" failedGeneratePod = "failed to generate pop token: %v" startingTMobileTimeout = time.Millisecond * 10 maximumTMobileTimeout = time.Minute Endpoint = "https://adn.t-mobile.com" ) var ( fakeIDGenerator = randomvalues.NewNonCryptoGenerator("1234567890", 0) currentTMobileTimeout = startingTMobileTimeout ) type httpClienter interface { Do(req *http.Request) (*http.Response, error) } type TMobClienter interface { AccessToken(ctx context.Context) (out *AccessTokenResponse, err error) SetAccessToken(accessToken string) SendSMS(ctx context.Context, in *SendSMSRequest) (out *SendSMSResponse, err error) Details(ctx context.Context, ID string) (out *SMSDetailsResponse, err error) ChangeRatePlan(context.Context, *ChangeRatePlanRequest) (*ChangeRatePlanResponse, error) CustomAttributes(context.Context, *CustomAtributesRequest) (*CustomAtributesResponse, error) GetProducts(context.Context, *sms.GetAvailableProductsRequest) (*sms.GetAvailableProductsResponse, error) DeviceDetails(context.Context, *DeviceDetailsRequest) (*DeviceDetailsResponse, error) ChangeDeviceStatus(ctx context.Context, cda ChangeDeviceActivation) (err error) SetFilter(filter []string) } type TMobClient struct { tg tmtg.Generator client httpClienter accessToken string baseURL *url.URL lock sync.RWMutex // Using a read-write mutex and anyone can send a sms at any time, but if the token is do for a renew, we should stop sending sms for a second toRefresh <-chan time.Time iccidFilter ICCIDFilter } var _ TMobClienter = &TMobClient{} const ( Authorization = "Authorization" XAuthorization = "X-Authorization" XAuthOriginator = "x-auth-originator" failedToDoRequest = "failed to do request" applicationJSON = "application/json" ) // baseURLstr: in the url to tmobile i.e. "https://core.saas.api.t-mobile.com" // tg: should have been initiated with the correct keys. I believe this service should do it itself given the correct input // timeout: The amount of time the http client will do a request before it gives up doing the request func NewTMobileClient(baseURLstr string, tg tmtg.Generator, timeout time.Duration) (*TMobClient, error) { u, err := url.Parse(baseURLstr) if err != nil { return nil, errors.WithMessage(err, "failed to parse base URL") } tmb := &TMobClient{ baseURL: u, tg: tg, client: &http.Client{ Timeout: timeout, }, toRefresh: make(<-chan time.Time), } tmb.refresh(context.Background()) tmb.iccidFilter = InitFilter() return tmb, nil } func (c *TMobClient) do(req *http.Request, out interface{}) error { resp, err := c.client.Do(req) if err != nil { return errors.WithMessagef(ErrDoRequest, "failed to do request: %v", err) } defer resp.Body.Close() if resp.StatusCode >= 300 { var body []byte body, err = io.ReadAll(resp.Body) if err != nil { return errors.WithMessagef(ErrReadResponseBody, "failed to read response body: %v", err) } toWrapErr := ErrBadStatusCode if resp.StatusCode == http.StatusUnauthorized { toWrapErr = ErrAccessTokenExpired } else if resp.StatusCode == http.StatusBadGateway { toWrapErr = ErrBadGatewayCode } else if resp.StatusCode == http.StatusGatewayTimeout { toWrapErr = ErrBadGatewayCode } return errors.WithMessagef(toWrapErr, "request failed with status code %d, body: %s", resp.StatusCode, string(body)) } err = json.NewDecoder(resp.Body).Decode(out) if err != nil { return errors.WithMessagef(ErrJSONMarshal, "failed to decode response: %v", err) } return nil } // Must call setAccessToken after calling this function to set the access token func (c *TMobClient) AccessToken(ctx context.Context) (out *AccessTokenResponse, err error) { path := "/oauth2/v1/tokens" c.lock.Lock() defer c.lock.Unlock() emap := make(tmtg.EHTSMap).SetAuthorization(c.tg.ClientSecretAsAuthVal()). SetURI(path). SetHTTPMethod(http.MethodPost) token, err := c.tg.Generate(emap) if err != nil { return nil, err } fullPath := c.baseURL.String() + path req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, nil) if err != nil { return nil, errors.WithMessagef(ErrCreateRequest, "failed to create request") } req.Header.Set(Authorization, c.tg.ClientSecretAsAuthVal()) req.Header.Set(XAuthorization, token) err = c.do(req, &out) if err != nil { return nil, errors.WithMessage(err, failedToDoRequest) } return } func (c *TMobClient) SetAccessToken(accessToken string) { c.accessToken = "Bearer " + accessToken } func (c *TMobClient) SendSMS(ctx context.Context, in *SendSMSRequest) (out *SendSMSResponse, err error) { if !c.iccidFilter.ShouldSend(in.ICCID) { out = &SendSMSResponse{ SmsMessageID: fakeSMSID(), } return } path := "/eitcsr-iotcp-notifications-v2/prd02/iotcp/v2/notifications/sms/messages" payload := new(bytes.Buffer) if err = json.NewEncoder(payload).Encode(in); err != nil { return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err) } payloadStr := payload.String() emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken). SetURI(path). SetHTTPMethod(http.MethodPost). SetContentType(applicationJSON). SetBody(payloadStr) logger.Debug().Msgf(payloadMsg, payloadStr) token, err := c.tg.Generate(emap) if err != nil { return nil, errors.WithMessagef(ErrTokenGen, "failed to generate token: %v", err) } fullPath := c.baseURL.String() + path req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, bytes.NewBufferString(payloadStr)) if err != nil { return nil, err } c.lock.RLock() defer c.lock.RUnlock() req.Header.Set(Authorization, c.accessToken) req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken)) req.Header.Set(XAuthorization, token) req.Header.Set(contentType, applicationJSON) err = c.do(req, &out) if err != nil { return nil, errors.WithMessage(err, failedToDoRequest) } return } func (c *TMobClient) Details(ctx context.Context, ID string) (out *SMSDetailsResponse, err error) { path := fmt.Sprintf("/eitcsr-iotcp-notifications-v2/prd02/iotcp/v2/notifications/sms/messages/%s", ID) emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken). SetURI(path). SetHTTPMethod(http.MethodGet) token, err := c.tg.Generate(emap) if err != nil { return nil, errors.WithMessagef(ErrTokenGen, "failed to generate token: %v", err) } fullPath := c.baseURL.String() + path req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullPath, nil) if err != nil { return nil, errors.WithMessagef(ErrCreateRequest, "failed to create request: %v", err) } c.lock.RLock() defer c.lock.RUnlock() req.Header.Set(Authorization, c.accessToken) req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken)) req.Header.Set(XAuthorization, token) req.Header.Set(contentType, applicationJSON) err = c.do(req, &out) if err != nil { return nil, errors.WithMessage(err, failedToDoRequest) } return } func InitTokenGen(pkVal, clientId, secret string) (*tmtg.PopTokenGenerator, error) { // No reason to take a variable and write it to a text file and then read it from the text file tg, err := tmtg.NewTokenGenerator( clientId, secret, time.Minute*2, pkVal, // path to public key file ) if err != nil { return nil, errors.WithMessage(err, "failed to create token generator") } return tg, nil } func (c *TMobClient) ChangeRatePlan(ctx context.Context, in *ChangeRatePlanRequest) (*ChangeRatePlanResponse, error) { path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/change-rate-plan" fullPath := c.baseURL.String() + path // The in body is so small, no use in using the encode method payload := new(bytes.Buffer) if err := json.NewEncoder(payload).Encode(in); err != nil { return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err) } payloadStr := payload.String() emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken). SetURI(path). SetHTTPMethod(http.MethodPost). SetContentType(applicationJSON). SetBody(payloadStr) logger.Debug().Msgf(payloadMsg, payloadStr) popToken, err := c.tg.Generate(emap) if err != nil { return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload) if err != nil { return nil, err } c.lock.RLock() defer c.lock.RUnlock() req.Header.Set(Authorization, c.accessToken) req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken)) req.Header.Set(XAuthorization, popToken) req.Header.Set(contentType, applicationJSON) out := &ChangeRatePlanResponse{} err = c.do(req, out) if err != nil { return nil, errors.WithMessage(err, failedToDoRequest) } return out, nil } func (c *TMobClient) CustomAttributes(ctx context.Context, in *CustomAtributesRequest) (*CustomAtributesResponse, error) { path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/device-attributes" fullPath := c.baseURL.String() + path payload := new(bytes.Buffer) if err := json.NewEncoder(payload).Encode(in); err != nil { return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err) } payloadStr := payload.String() emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken). SetURI(path). SetHTTPMethod(http.MethodPost). SetContentType(applicationJSON). SetBody(payloadStr) logger.Debug().Msgf(payloadMsg, payloadStr) popToken, err := c.tg.Generate(emap) if err != nil { return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload) if err != nil { return nil, err } c.lock.RLock() defer c.lock.RUnlock() req.Header.Set(Authorization, c.accessToken) req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken)) req.Header.Set(XAuthorization, popToken) req.Header.Set(contentType, applicationJSON) out := &CustomAtributesResponse{} err = c.do(req, out) if err != nil { return nil, errors.WithMessage(err, failedToDoRequest) } return out, nil } func (c *TMobClient) GetProducts(ctx context.Context, in *sms.GetAvailableProductsRequest) (*sms.GetAvailableProductsResponse, error) { path := "/eitcsr-iot-product-service-v1/prd02/iotcp/v1/iot-product-service/products/query" fullPath := c.baseURL.String() + path payload := new(bytes.Buffer) if err := json.NewEncoder(payload).Encode(in); err != nil { return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err) } payloadStr := payload.String() emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken). SetURI(path). SetHTTPMethod(http.MethodPost). SetContentType(applicationJSON). SetBody(payloadStr) logger.Debug().Msgf(payloadMsg, payloadStr) popToken, err := c.tg.Generate(emap) if err != nil { return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload) if err != nil { return nil, err } c.lock.RLock() defer c.lock.RUnlock() req.Header.Set(Authorization, c.accessToken) req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken)) req.Header.Set(XAuthorization, popToken) req.Header.Set(contentType, applicationJSON) out := &sms.GetAvailableProductsResponse{} err = c.do(req, &out.AvailableProducts) if err != nil { return nil, errors.WithMessage(err, failedToDoRequest) } return out, nil } func (c *TMobClient) DeviceDetails(ctx context.Context, in *DeviceDetailsRequest) (*DeviceDetailsResponse, error) { path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/details" fullPath := c.baseURL.String() + path payload := new(bytes.Buffer) if err := json.NewEncoder(payload).Encode(in); err != nil { return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err) } payloadStr := payload.String() emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken). SetURI(path). SetHTTPMethod(http.MethodPost). SetContentType(applicationJSON). SetBody(payloadStr) logger.Debug().Msgf(payloadMsg, payloadStr) popToken, err := c.tg.Generate(emap) if err != nil { return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload) if err != nil { return nil, err } c.lock.RLock() defer c.lock.RUnlock() req.Header.Set(Authorization, c.accessToken) req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken)) req.Header.Set(XAuthorization, popToken) req.Header.Set(contentType, applicationJSON) out := &DeviceDetailsResponse{} err = c.do(req, &out) if err != nil { return nil, errors.WithMessage(err, failedToDoRequest) } return out, nil } func (c *TMobClient) refresh(ctx context.Context) { var expiresIn time.Duration at, err := c.AccessToken(ctx) if err != nil { logger.Error().Msgf("failed to get access token: %v", err) expiresIn = currentTMobileTimeout currentTMobileTimeout = currentTMobileTimeout * currentTMobileTimeout if currentTMobileTimeout > maximumTMobileTimeout { currentTMobileTimeout = maximumTMobileTimeout } } else { expiresIn = time.Duration(at.ExpiresIn) * time.Second expiresIn -= time.Minute * 1 // Expire 1 minute earlier than needed. Previous code was refreshing in half the time c.SetAccessToken(at.AccessToken) logger.Info().Msgf("Refreshed access token, expires in %s", expiresIn) // Reset the increasing timeout currentTMobileTimeout = startingTMobileTimeout } time.AfterFunc(expiresIn, func() { c.refresh(context.Background()) }) } // SetFilter implements TMobClienter. func (c *TMobClient) SetFilter(filter []string) { c.iccidFilter = ICCIDFilter{ filter: filter, } } // SmsMessageID is a string consisting of numbers, will return a marker showing its not real func fakeSMSID() (fakeID string) { return fmt.Sprintf("FAKE_SMS_ID:%s", fakeIDGenerator.GetString(10)) } func IsRealSMSID(smsID string) (isReal bool) { return !strings.HasPrefix(smsID, "FAKE_SMS_ID:") } // HandleChangeDeviceStatus implements TMobClienter. func (c *TMobClient) ChangeDeviceStatus(ctx context.Context, cda ChangeDeviceActivation) (err error) { var fn func(ctx context.Context, in *ICCIDBody)(out *ICCIDBody, err error) if cda.Enabled { fn = c.handleServiceRestore }else { fn = c.handleServiceCancel } for _, iccid := range cda.ICCIDs { iccid = strings.TrimSuffix(strings.TrimSuffix(iccid, "F"), "f") temp := ICCIDBody{ ICCID: iccid, } _, tErr := fn(ctx, &temp) if tErr != nil { err = errorsO.Join(err, tErr) } } return err } // sets status as ACTIVATED func (c *TMobClient) handleServiceRestore(ctx context.Context, in *ICCIDBody)(out *ICCIDBody, err error){ path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/service-restore" fullPath := c.baseURL.String() + path payload := new(bytes.Buffer) err = json.NewEncoder(payload).Encode(in) if err != nil { return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err) } payloadStr := payload.String() emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken). SetURI(path). SetHTTPMethod(http.MethodPost). SetContentType(applicationJSON). SetBody(payloadStr) popToken, err := c.tg.Generate(emap) if err != nil { return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload) if err != nil { return nil, err } c.lock.RLock() defer c.lock.RUnlock() req.Header.Set(Authorization, c.accessToken) req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken)) req.Header.Set(XAuthorization, popToken) req.Header.Set(contentType, applicationJSON) out = &ICCIDBody{} err = c.do(req, &out) if err != nil { return nil, errors.WithMessage(err, failedToDoRequest) } return } // sets status as DEACTIVATED func (c *TMobClient) handleServiceCancel(ctx context.Context, in *ICCIDBody)(out *ICCIDBody, err error){ path := "/eitcsr-iotcp-line-of-service-v1/prd02/iotcp/v1/line-of-service/devices/service-suspend" fullPath := c.baseURL.String() + path payload := new(bytes.Buffer) err = json.NewEncoder(payload).Encode(in) if err != nil { return nil, errors.WithMessagef(ErrJSONMarshal, failedToParseRequest, err) } payloadStr := payload.String() emap := make(tmtg.EHTSMap).SetAuthorization(c.accessToken). SetURI(path). SetHTTPMethod(http.MethodPost). SetContentType(applicationJSON). SetBody(payloadStr) popToken, err := c.tg.Generate(emap) if err != nil { return nil, errors.WithMessagef(ErrTokenGen, failedGeneratePod, err) } req, err := http.NewRequestWithContext(ctx, http.MethodPost, fullPath, payload) if err != nil { return nil, err } c.lock.RLock() defer c.lock.RUnlock() req.Header.Set(Authorization, c.accessToken) req.Header.Set(XAuthOriginator, ToXAuthOriginator(c.accessToken)) req.Header.Set(XAuthorization, popToken) req.Header.Set(contentType, applicationJSON) out = &ICCIDBody{} err = c.do(req, &out) if err != nil { return nil, errors.WithMessage(err, failedToDoRequest) } return }