diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 62dd1d8..4942a0e 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -15,31 +15,18 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v3 - with: - go-version: 1.17 - uses: actions/checkout@v3 - - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + - uses: actions/setup-go@v4 with: - # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: v1.29 - - # Optional: working directory, useful for monorepos - # working-directory: somedir - - # Optional: golangci-lint command line arguments. - # args: --issues-exit-code=0 - - # Optional: show only new issues if it's a pull request. The default value is `false`. - # only-new-issues: true - - # Optional: if set to true then the all caching functionality will be complete disabled, - # takes precedence over all other caching options. - # skip-cache: true - - # Optional: if set to true then the action don't cache or restore ~/go/pkg. - # skip-pkg-cache: true - - # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. - # skip-build-cache: true \ No newline at end of file + go-version-file: go.mod + # The builtin cache feature ensures that installing golangci-lint + # is consistently fast. + cache: true + cache-dependency-path: go.sum + - name: install-golangci-lint + # Install golangci-lint from source instead of using + # golangci-lint-action to ensure the golangci-lint binary is built with + # the same Go version we're targeting. + run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.47.3 + - name: golangci-lint + run: golangci-lint run --version --verbose --out-format=github-actions \ No newline at end of file diff --git a/go.mod b/go.mod index 4280ef6..5f3a534 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,9 @@ go 1.18 require github.com/google/uuid v1.3.0 -require github.com/go-resty/resty/v2 v2.7.0 +require ( + github.com/go-resty/resty/v2 v2.7.0 + github.com/golang-jwt/jwt/v5 v5.0.0 +) require golang.org/x/net v0.7.0 // indirect diff --git a/go.sum b/go.sum index 9ce2fce..06c4566 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= diff --git a/lago.go b/lago.go index 91dac5f..807d28f 100644 --- a/lago.go +++ b/lago.go @@ -63,11 +63,18 @@ func (c *Client) SetDebug(debug bool) *Client { } func (c *Client) Get(ctx context.Context, cr *ClientRequest) (interface{}, *Error) { - resp, err := c.HttpClient.R(). + hasResult := cr.Result != nil + + request := c.HttpClient.R(). SetContext(ctx). SetError(&Error{}). - SetResult(cr.Result). - SetQueryParams(cr.QueryParams). + SetQueryParams(cr.QueryParams) + + if hasResult { + request.SetResult(cr.Result) + } + + resp, err := request. Get(cr.Path) if err != nil { return nil, &Error{Err: err} @@ -87,7 +94,11 @@ func (c *Client) Get(ctx context.Context, cr *ClientRequest) (interface{}, *Erro return nil, err } - return resp.Result(), nil + if hasResult { + return resp.Result(), nil + } + + return resp.String(), nil } func (c *Client) Post(ctx context.Context, cr *ClientRequest) (interface{}, *Error) { @@ -97,6 +108,7 @@ func (c *Client) Post(ctx context.Context, cr *ClientRequest) (interface{}, *Err SetResult(cr.Result). SetBody(cr.Body). Post(cr.Path) + if err != nil { return nil, &Error{Err: err} } diff --git a/webhook.go b/webhook.go new file mode 100644 index 0000000..ec4c10a --- /dev/null +++ b/webhook.go @@ -0,0 +1,110 @@ +package lago + +import ( + "context" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + "net/http" + + jwt "github.com/golang-jwt/jwt/v5" +) + +type WebhookRequest struct { + client *Client +} + +func (c *Client) Webhook() *WebhookRequest { + return &WebhookRequest{ + client: c, + } +} + +func (wr *WebhookRequest) GetPublicKey(ctx context.Context) (*rsa.PublicKey, *Error) { + clientRequest := &ClientRequest{ + Path: "webhooks/public_key", + } + + result, err := wr.client.Get(ctx, clientRequest) + if err != nil { + return nil, err + } + + validatedResult, ok := result.(string) + if !ok { + return nil, &Error{ + Err: errors.New("response is not a string"), + HTTPStatusCode: http.StatusInternalServerError, + Msg: "response is not a string", + } + } + + // Decode the base64-encoded key + bytesResult, decodeErr := base64.StdEncoding.DecodeString(validatedResult) + if err != nil { + return nil, &Error{ + Err: decodeErr, + HTTPStatusCode: http.StatusInternalServerError, + Msg: "cannot decode the key", + } + } + + // Parse the PEM block + block, _ := pem.Decode(bytesResult) + if block == nil || block.Type != "PUBLIC KEY" { + return nil, &Error{ + Err: errors.New("Failed to decode PEM block containing public key"), + HTTPStatusCode: http.StatusInternalServerError, + Msg: "Failed to decode PEM block containing public key", + } + } + + // Parse the DER-encoded public key + publicKey, parseErr := x509.ParsePKIXPublicKey(block.Bytes) + if parseErr != nil { + return nil, &Error{ + Err: parseErr, + HTTPStatusCode: http.StatusInternalServerError, + Msg: "Failed to to parse the public key", + } + } + + rsaPublicKey, ok := publicKey.(*rsa.PublicKey) + if !ok { + return nil, &Error{ + Err: errors.New("Unexpected type of public key"), + HTTPStatusCode: http.StatusInternalServerError, + Msg: "Unexpected type of public key", + } + } + + return rsaPublicKey, nil +} + +func (wr *WebhookRequest) ValidateSignature(ctx context.Context, signature string) (bool, *Error) { + publicKey, err := wr.GetPublicKey(ctx) + if err != nil { + return false, err + } + + token, parseErr := jwt.Parse(signature, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + return publicKey, nil + }) + if parseErr != nil { + return false, &Error{ + Err: parseErr, + HTTPStatusCode: http.StatusInternalServerError, + Msg: "cannot parse token", + } + } + + println(fmt.Printf("token: %+v", token)) + + return token.Valid, nil +}