Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.out
.vscode/
.idea/
.vs/
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ The logger package provides logic related to logging. It offers the Facade Desig
| [`Request Logger Configuration`](logger/requestLoggerConfiguration.go) | Provides request logging configurations for the Sdk Logger. |
| [`Sdk Logger`](logger/sdkLogger.go) | Provides default and null implementation of [` Sdk Logger Interface`](logger/sdkLogger.go) to log API requests and responses. |

### Security
The security package provides logic related to request authentication and verification. It includes digest encoding/decoding, signature verification, and HMAC-based request validation.

| File Name | Description |
|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`DigestCodec`](security/digest_codec.go) | Provides digest encoding/decoding interfaces and implementations for Hex, Base64, and Base64URL formats. |
| [`SignatureVerifier`](security/hmac_signature_verifier.go) | Provides interfaces for verifying signatures for HTTP requests. |
| [`HmacSignatureVerifier`](security/hmac_signature_verifier.go) | Provides an implementation of [`SignatureVerifier`](security/hmac_signature_verifier.go) for HMAC-based signature verification for HTTP requests, supporting configurable algorithms and templates. |
| [`VerificationResult`](security/verification_result.go) | Defines the structure for representing signature verification outcomes with success/failure states and error details. |


### Test Helper
Package testHelper provides helper functions for testing purposes.

Expand Down
61 changes: 60 additions & 1 deletion https/httpClient_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package https

import (
"bytes"
"io"
"net/http"
"reflect"
"strconv"
"testing"
)

Expand All @@ -19,7 +22,7 @@ func TestNewHttpClient(t *testing.T) {

func TestHttpClientExecute(t *testing.T) {
client := NewHttpClient(NewHttpConfiguration())
request, _ := http.NewRequest(http.MethodGet, "https://apimatic-goo.free.beeceptor.com", nil)
request, _ := http.NewRequest(http.MethodGet, "https://apimatic-go.free.beeceptor.com", nil)
response, _ := client.Execute(request)

if response == nil || response.StatusCode != 200 {
Expand All @@ -35,3 +38,59 @@ func TestHttpClientExecuteError(t *testing.T) {
t.Errorf("Failed: Response not okay!\n %v", response)
}
}

func TestHttpClientInterfaceExecuteFailure(t *testing.T) {
req, err := FromFastHttpRequest(
[]byte("INVALID_METHOD"),
"://invalid-url",
nil,
nil,
[]byte{},
nil,
)

if err == nil || req != nil {
t.Errorf("Failed: Response not okay!\n %v", req)
}
}

func TestHttpClientInterfaceExecute(t *testing.T) {
fastReq, _ := http.NewRequest(
http.MethodGet,
"https://apimatic-go.free.beeceptor.com",
io.NopCloser(bytes.NewBuffer([]byte("This is data"))))
bodyBytes, _ := ReadRequestBody(fastReq)

fastReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
fastReq.Header.Set("Content-Length", strconv.Itoa(len(bodyBytes)))

fastReqVisitAll := func(setFunc func(k, v []byte)) {
for k, v := range fastReq.Header {
for _, hv := range v {
setFunc([]byte(k), []byte(hv))
}
}
}

fastReq.AddCookie(&http.Cookie{Name: API_KEY, Value: "12345"})
fastReq.AddCookie(&http.Cookie{Name: API_TOKEN, Value: "token-123456"})

fastReqVisitAllCookie := func(setFunc func(key, value []byte)) {
for _, cookie := range fastReq.Cookies() {
setFunc([]byte(cookie.Name), []byte(cookie.Value))
}
}

req, err := FromFastHttpRequest(
[]byte(fastReq.Method),
fastReq.URL.String(),
bodyBytes,
fastReqVisitAll,
[]byte(fastReq.RequestURI),
fastReqVisitAllCookie,
)

if err != nil || req == nil {
t.Errorf("Failed: Response not okay!\n %v", req)
}
}
34 changes: 34 additions & 0 deletions https/httpContext.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package https

import (
"bytes"
"fmt"
"io"
"net/http"
)
Expand All @@ -23,3 +24,36 @@ func (ctx *HttpContext) GetResponseBody() ([]byte, error) {
ctx.Response.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
return bodyBytes, err
}

func ReadRequestBody(req *http.Request) ([]byte, error) {
bodyBytes, err := io.ReadAll(req.Body)
req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
return bodyBytes, err
}

func FromFastHttpRequest(
methodBytes []byte,
url string,
bodyBytes []byte,
headerFunc func(setFunc func(k, v []byte)),
requestURIBytes []byte,
visitAllCookieFunc func(func(key, value []byte)),
) (*http.Request, error) {
req, err := http.NewRequest(string(methodBytes), url, io.NopCloser(bytes.NewReader(bodyBytes)))
if err != nil {
return nil, fmt.Errorf("failed to create http.Request: %w", err)
}
// Copy headers
if headerFunc != nil {
headerFunc(func(k, v []byte) { req.Header.Add(string(k), string(v)) })
}
// Set RequestURI (useful if mimicking server request)
req.RequestURI = string(requestURIBytes)
// Handle cookies explicitly
if visitAllCookieFunc != nil {
visitAllCookieFunc(func(k, v []byte) {
req.AddCookie(&http.Cookie{Name: string(k), Value: string(v)})
})
}
return req, nil
}
18 changes: 18 additions & 0 deletions https/httpContext_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,21 @@ func TestGetResponseBody(t *testing.T) {
t.Errorf("Failed:\nExpected: %v\nGot: %v", bodyBytes, newBodyBytes)
}
}

func TestReadRequestBody(t *testing.T) {
bodyBytes := []byte(`{"invalidJson"}`)
respBody := io.NopCloser(bytes.NewReader(bodyBytes))
ctx := HttpContext{
Request: &http.Request{
Body: respBody,
},
}

newBodyBytes, err := ReadRequestBody(ctx.Request)
if err != nil {
t.Error(err)
}
if !reflect.DeepEqual(newBodyBytes, bodyBytes) {
t.Errorf("Failed:\nExpected: %v\nGot: %v", bodyBytes, newBodyBytes)
}
}
2 changes: 1 addition & 1 deletion https/interceptors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func TestPassThroughInterceptorNilNext(t *testing.T) {
func TestCallHttpInterceptors(t *testing.T) {
client := NewHttpClient(NewHttpConfiguration())
callBuilder := CreateCallBuilderFactory(
func(server string) string { return "https://apimatic-goo.free.beeceptor.com" }, nil, client, NewRetryConfiguration(), Indexed)
func(server string) string { return "https://apimatic-go.free.beeceptor.com" }, nil, client, NewRetryConfiguration(), Indexed)

req := callBuilder(context.Background(), "POST", "/interceptors")
interceptor1 := func(request *http.Request, next HttpCallExecutor) HttpContext {
Expand Down
63 changes: 63 additions & 0 deletions security/digest_codec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package security

import (
"encoding/base64"
"encoding/hex"
)

// DigestCodec is an interface for encoding and decoding digest values.
type DigestCodec interface {
// Encode encodes a byte slice digest into a string representation.
Encode(bytes []byte) string

// Decode decodes a string representation back into a byte slice.
Decode(encoded string) ([]byte, error)
}

// NewHexDigestCodec returns a DigestCodec for Hex encoding/decoding.
func NewHexDigestCodec() DigestCodec {
return hexDigestCodec{}
}

// NewBase64DigestCodec returns a DigestCodec for Base64 encoding/decoding.
func NewBase64DigestCodec() DigestCodec {
return base64DigestCodec{}
}

// NewBase64URLDigestCodec returns a DigestCodec for Base64 URL-safe encoding/decoding.
func NewBase64URLDigestCodec() DigestCodec {
return base64URLDigestCodec{}
}

// hexDigestCodec implements DigestCodec for hexadecimal encoding.
type hexDigestCodec struct{}

func (hexDigestCodec) Encode(bytes []byte) string {
return hex.EncodeToString(bytes)
}

func (hexDigestCodec) Decode(encoded string) ([]byte, error) {
return hex.DecodeString(encoded)
}

// base64DigestCodec implements DigestCodec for Base64 encoding.
type base64DigestCodec struct{}

func (base64DigestCodec) Encode(bytes []byte) string {
return base64.StdEncoding.EncodeToString(bytes)
}

func (base64DigestCodec) Decode(encoded string) ([]byte, error) {
return base64.StdEncoding.DecodeString(encoded)
}

// base64URLDigestCodec implements DigestCodec for Base64 URL-safe encoding.
type base64URLDigestCodec struct{}

func (base64URLDigestCodec) Encode(bytes []byte) string {
return base64.RawURLEncoding.EncodeToString(bytes)
}

func (base64URLDigestCodec) Decode(encoded string) ([]byte, error) {
return base64.RawURLEncoding.DecodeString(encoded)
}
135 changes: 135 additions & 0 deletions security/digest_codec_factory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package security

import (
"testing"
)

// Assumptions (adjust/remove if your actual API differs):
// - type DigestCodec interface { Encode([]byte) string; Decode(string) ([]byte, error) }
// - func Hex() DigestCodec
// - func Base64() DigestCodec
// - func Base64URL() DigestCodec

func TestHexEncodeDecode(t *testing.T) {
codec := NewHexDigestCodec()
input := []byte{0x0A, 0x1B, 0xFF}
encoded := codec.Encode(input)
if encoded != "0a1bff" {
t.Fatalf("expected 0a1bff, got %s", encoded)
}
decoded, err := codec.Decode(encoded)
if err != nil {
t.Fatalf("decode error: %v", err)
}
if string(decoded) != string(input) {
t.Fatalf("decoded mismatch: %v != %v", decoded, input)
}
}

func TestHexEncodeEmpty(t *testing.T) {
codec := NewHexDigestCodec()
var input []byte
encoded := codec.Encode(input)
if encoded != "" {
t.Fatalf("expected empty string, got %q", encoded)
}
decoded, err := codec.Decode(encoded)
if err != nil {
t.Fatalf("decode error: %v", err)
}
if len(decoded) != 0 {
t.Fatalf("expected empty slice, got %v", decoded)
}
}

func TestHexDecodeInvalidLength(t *testing.T) {
codec := NewHexDigestCodec()
if _, err := codec.Decode("abc"); err == nil {
t.Fatalf("expected error for odd-length hex input")
}
}

func TestHexDecodeInvalidCharacter(t *testing.T) {
codec := NewHexDigestCodec()
if _, err := codec.Decode("zzzzz"); err == nil {
t.Fatalf("expected error for invalid hex characters")
}
}

func TestBase64EncodeDecode(t *testing.T) {
codec := NewBase64DigestCodec()
input := []byte{1, 2, 3, 4, 5}
encoded := codec.Encode(input)
if encoded != "AQIDBAU=" {
t.Fatalf("expected AQIDBAU=, got %s", encoded)
}
decoded, err := codec.Decode(encoded)
if err != nil {
t.Fatalf("decode error: %v", err)
}
if string(decoded) != string(input) {
t.Fatalf("decoded mismatch: %v != %v", decoded, input)
}
}

func TestBase64EncodeEmpty(t *testing.T) {
codec := NewBase64DigestCodec()
input := make([]byte, 0)
encoded := codec.Encode(input)
if encoded != "" {
t.Fatalf("expected empty string, got %q", encoded)
}
decoded, err := codec.Decode(encoded)
if err != nil {
t.Fatalf("decode error: %v", err)
}
if len(decoded) != 0 {
t.Fatalf("expected empty slice, got %v", decoded)
}
}

func TestBase64DecodeInvalid(t *testing.T) {
codec := NewBase64DigestCodec()
if _, err := codec.Decode("!@#$"); err == nil {
t.Fatalf("expected error for invalid base64 input")
}
}

func TestBase64URLEncodeDecode(t *testing.T) {
codec := NewBase64URLDigestCodec()
input := []byte{10, 20, 30, 40, 50}
encoded := codec.Encode(input)
if encoded != "ChQeKDI" { // raw URL (no padding)
t.Fatalf("expected ChQeKDI, got %s", encoded)
}
decoded, err := codec.Decode(encoded)
if err != nil {
t.Fatalf("decode error: %v", err)
}
if string(decoded) != string(input) {
t.Fatalf("decoded mismatch: %v != %v", decoded, input)
}
}

func TestBase64URLEncodeEmpty(t *testing.T) {
codec := NewBase64URLDigestCodec()
var input []byte
encoded := codec.Encode(input)
if encoded != "" {
t.Fatalf("expected empty string, got %q", encoded)
}
decoded, err := codec.Decode(encoded)
if err != nil {
t.Fatalf("decode error: %v", err)
}
if len(decoded) != 0 {
t.Fatalf("expected empty slice, got %v", decoded)
}
}

func TestBase64URLDecodeInvalid(t *testing.T) {
codec := NewBase64URLDigestCodec()
if _, err := codec.Decode("!@#$"); err == nil {
t.Fatalf("expected error for invalid base64url input")
}
}
Loading