From 3313a9995767a13ef009364a5b437021b3148af3 Mon Sep 17 00:00:00 2001 From: Mark Smith Date: Mon, 17 Apr 2017 15:28:37 +0100 Subject: [PATCH] Early prototype code. --- auth.go | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++ insight.go | 81 ++++++++++++++++++++++++++++++++++ nexmo.go | 34 +++++++++++++++ 3 files changed, 240 insertions(+) create mode 100644 auth.go create mode 100644 insight.go create mode 100644 nexmo.go diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..852ded8 --- /dev/null +++ b/auth.go @@ -0,0 +1,125 @@ +package nexmo + +import ( + "crypto/rsa" + "math/rand" + "strconv" + "time" + + "fmt" + + "github.com/dghubble/sling" + "github.com/dgrijalva/jwt-go" +) + +type AuthType uint8 + +type RandomProvider interface { + Int31() int32 +} + +const ( + ApiSecretAuth AuthType = iota + 1 + ApiSecretPathAuth + JwtAuth +) + +type AuthSet struct { + apiSecret *apiSecretAuth + appAuth *applicationAuth +} + +func NewAuthSet() *AuthSet { + return new(AuthSet) +} + +func (a *AuthSet) SetAPISecret(apiKey, apiSecret string) { + a.apiSecret = &apiSecretAuth{ + apiKey: apiKey, + apiSecret: apiSecret, + } +} + +func (a *AuthSet) GenerateToken() (string, error) { + if a.appAuth == nil { + return "", fmt.Errorf("must call SetApplicationAuth before calling GenerateToken") + } + return a.appAuth.generateToken() +} + +func (a *AuthSet) SetApplicationAuth(appID string, key []byte) error { + privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(key) + if err != nil { + return err + } + a.appAuth = &applicationAuth{ + appID: appID, + privateKey: privateKey, + r: rand.New(rand.NewSource(time.Now().UnixNano())), + } + return nil +} + +func (a *AuthSet) ApplyAuth(acceptableAuths []AuthType, sling *sling.Sling, request apiSecretRequest) error { + for _, acceptableAuth := range acceptableAuths { + switch acceptableAuth { + case ApiSecretAuth: + if a.apiSecret != nil { + return a.apiSecret.apply(request) + } + case ApiSecretPathAuth: + if a.apiSecret != nil { + sling.Path(fmt.Sprintf("%s/%s", a.apiSecret.apiKey, a.apiSecret.apiSecret)) + } + case JwtAuth: + token, err := a.appAuth.generateToken() + if err != nil { + return err + } + if a.appAuth != nil { + sling.Set("Authorization", fmt.Sprintf("Bearer %v", token)) + } + } + } + return fmt.Errorf("AuthSet not configured with credentials for %x", acceptableAuths) +} + +type apiSecretRequest interface { + applyAPISecret(apiKey, apiSecret string) +} + +type apiSecretAuth struct { + apiKey string + apiSecret string +} + +func (a apiSecretAuth) apply(request apiSecretRequest) error { + if request == nil { + return fmt.Errorf("cannot apply api_key and api_secret to a nil request") + } + request.applyAPISecret(a.apiKey, a.apiSecret) + return nil +} + +type applicationAuth struct { + appID string + privateKey *rsa.PrivateKey + r RandomProvider +} + +type jwtClaims struct { + ApplicationID string `json:"application_id"` + jwt.StandardClaims +} + +func (a applicationAuth) generateToken() (string, error) { + claims := jwtClaims{ + a.appID, + jwt.StandardClaims{ + Id: strconv.Itoa(int(a.r.Int31())), + IssuedAt: time.Now().UTC().Unix(), + }, + } + token := jwt.NewWithClaims(jwt.GetSigningMethod("RS256"), claims) + return token.SignedString(a.privateKey) +} diff --git a/insight.go b/insight.go new file mode 100644 index 0000000..4f4cc39 --- /dev/null +++ b/insight.go @@ -0,0 +1,81 @@ +package nexmo + +import ( + "net/http" + + "fmt" + + "github.com/dghubble/sling" +) + +type InsightService struct { + sling *sling.Sling + authSet *AuthSet +} + +type BasicInsightRequest struct { + Number string + Country string + Cnam *bool +} + +type basicInsightJSONRequest struct { + APIKey string `json:"api_key"` + APISecret string `json:"api_secret"` + Number string `json:"number,omitempty"` + Country string `json:"country,omitempty"` +} + +type BasicInsightResponse struct { + Status int64 `json:"status"` + StatusMessage string `json:"status_message,omitempty"` + ErrorText string `json:"error_text,omitempty"` + InternationalFormatNumber string `json:"international_format_number,omitempty"` + NationalFormatNumber string `json:"national_format_number,omitempty"` + CountryCode string `json:"country_code,omitempty"` + CountryCodeIso3 string `json:"country_code_iso3,omitempty"` + CountryName string `json:"country_name,omitempty"` + CountryPrefix string `json:"country_prefix,omitempty"` +} + +func (r *BasicInsightResponse) responseError() error { + if r.Status != 0 { + if r.StatusMessage != "" { + return fmt.Errorf("%d: %s", r.Status, r.StatusMessage) + } + return fmt.Errorf("%d: %s", r.Status, r.ErrorText) + } + return nil +} + +func newInsightService(base *sling.Sling, authSet *AuthSet) *InsightService { + sling := base.Base("https://api.nexmo.com/ni/") + return &InsightService{ + sling: sling, + authSet: authSet, + } +} + +func (c *InsightService) GetBasicInsight(request BasicInsightRequest) (BasicInsightResponse, *http.Response, error) { + if c.authSet.apiSecret == nil { + return BasicInsightResponse{}, nil, fmt.Errorf("Cannot call GetBasicInsight without providing APISecretAuth") + } + jsonRequest := basicInsightJSONRequest{ + APIKey: c.authSet.apiSecret.apiKey, + APISecret: c.authSet.apiSecret.apiSecret, + Number: request.Number, + Country: request.Country, + } + + insightResponse := new(BasicInsightResponse) + resp, err := c.sling.New().Post("basic/json").BodyJSON(jsonRequest).ReceiveSuccess(insightResponse) + if err != nil { + return *insightResponse, resp, err + } + err = insightResponse.responseError() + return *insightResponse, resp, err +} + +func (c *InsightService) SetBaseURL(baseURL string) { + c.sling.Base(baseURL) +} diff --git a/nexmo.go b/nexmo.go new file mode 100644 index 0000000..ee5175c --- /dev/null +++ b/nexmo.go @@ -0,0 +1,34 @@ +package nexmo + +import ( + "fmt" + "net/http" + + "github.com/dghubble/sling" +) + +type Client struct { + sling *sling.Sling + Insight *InsightService + //SMS *SMSService +} + +func NewClient(httpClient *http.Client, authSet *AuthSet) *Client { + base := sling.New(). + Client(httpClient). + Set("User-Agent", "nexmo-go/2.0 (judy2k)") + return &Client{ + sling: base, + Insight: newInsightService(base.New(), authSet), + //SMS: newSMSService(base.New(), authSet), + } +} + +type APIError struct { + Status int64 + ErrorMessage string +} + +func (a APIError) Error() string { + return fmt.Sprintf("%d: %s", a.Status, a.ErrorMessage) +}