Skip to content

DEVTOOLING - 1013 Prehook/Posthook #1058

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
8 changes: 4 additions & 4 deletions resources/sdk/purecloudgo/extensions/abstract_httpclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ type AbstractHttpClient interface {
// SetRetryWaitMin sets the minimum wait time between retries
SetRetryWaitMin(duration time.Duration)

// SetRequestLogHook sets a logging hook that is called before each retry attempt
SetRequestLogHook(hook func(retryablehttp.Logger, *http.Request, int))
// SetPreHook sets a logging hook that is called before each retry attempt
SetPreHook(hook func(retryablehttp.Logger, *http.Request, int))

// SetResponseLogHook sets a logging hook that is called after each response
SetResponseLogHook(hook func(retryablehttp.Logger, *http.Response))
// SetPostHook sets a logging hook that is called after each response
SetPostHook(hook func(retryablehttp.Logger, *http.Response))

// SetCheckRetry sets the retry policy function to determine if a request should be retried
SetCheckRetry(checkRetry func(ctx context.Context, resp *http.Response, err error) (bool, error))
Expand Down
8 changes: 4 additions & 4 deletions resources/sdk/purecloudgo/extensions/default_httpclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ func (c *DefaultHttpClient) SetRetryWaitMin(duration time.Duration) {
c.client.RetryWaitMin = duration
}

// SetRequestLogHook sets a logging hook that runs before each retry
func (c *DefaultHttpClient) SetRequestLogHook(hook func(retryablehttp.Logger, *http.Request, int)) {
// SetPreHook sets a logging hook that runs before each retry
func (c *DefaultHttpClient) SetPreHook(hook func(retryablehttp.Logger, *http.Request, int)) {
c.client.RequestLogHook = hook
}

// SetResponseLogHook sets a logging hook that runs after each retry
func (c *DefaultHttpClient) SetResponseLogHook(hook func(retryablehttp.Logger, *http.Response)) {
// SetPostHook sets a logging hook that runs after each retry
func (c *DefaultHttpClient) SetPostHook(hook func(retryablehttp.Logger, *http.Response)) {
c.client.ResponseLogHook = hook
}

Expand Down
8 changes: 4 additions & 4 deletions resources/sdk/purecloudgo/extensions/retry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ func testRetryErrorCode(t *testing.T, errorCode int) {
APIClient.client.SetRetryWaitMin(10 * time.Millisecond)
APIClient.client.SetRetryWaitMax(50 * time.Millisecond)
APIClient.client.SetRetryMax(50)
APIClient.client.SetRequestLogHook(hook)
APIClient.client.SetResponseLogHook(responseHook)
APIClient.client.SetPreHook(hook)
APIClient.client.SetPostHook(responseHook)

// Create a request
testBytes := []byte("hello")
Expand Down Expand Up @@ -136,8 +136,8 @@ func testDoNotRetryErrorCode(t *testing.T, errorCode int) {
APIClient := NewAPIClient(&Configuration{})
APIClient.client.SetRetryWaitMax(0)
APIClient.client.SetRetryMax(0)
APIClient.client.SetRequestLogHook(hook)
APIClient.client.SetResponseLogHook(responseHook)
APIClient.client.SetPreHook(hook)
APIClient.client.SetPostHook(responseHook)

// Create a request
testBytes := []byte("hello")
Expand Down
1 change: 1 addition & 0 deletions resources/sdk/purecloudgo/extensions/usersapi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func ExampleUsersApi_GetUsers() {
clientID := os.Getenv("PURECLOUD_CLIENT_ID")
clientSecret := os.Getenv("PURECLOUD_CLIENT_SECRET")
config.GateWayConfiguration = nil
config.MTLSConfiguration = nil

// Authorize the configuration
err := config.AuthorizeClientCredentials(clientID, clientSecret)
Expand Down
155 changes: 155 additions & 0 deletions resources/sdk/purecloudgo/extensions/usersapiproxy_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package platformclientv2

import (
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"net/http"
"os"
"testing"

"github.com/hashicorp/go-retryablehttp"
)

func TestExampleUsersApi_GetUsers_OnProxy(t *testing.T) {
Expand Down Expand Up @@ -95,6 +101,8 @@ func TestExampleUsersApi_GetUsers_CustomClient(t *testing.T) {

config.APIClient.SetMTLSCertificates("mtls-test/localhost.cert.pem", "mtls-test/localhost.key.pem", "mtls-test/ca-chain.cert.pem")

config.APIClient.client.SetPreHook(PreHook)

// Create an API instance using the default config
usersAPI := NewUsersApiWithConfig(config)

Expand All @@ -107,3 +115,150 @@ func TestExampleUsersApi_GetUsers_CustomClient(t *testing.T) {
}
// Output: Successfully retrieved user data with status code 200
}

// PreHook implements the certificate revocation check
func PreHook(logger retryablehttp.Logger, req *http.Request, retry int) {
config := GetDefaultConfiguration()

//Extract certificate from request
certificate, err := getCertificateFromConfig(config)
if err != nil {
logger.Printf("[ERROR] Certificate extraction failed: %v", err)
return
}
issuerCertificate, err := getIssuerCertificate()
if err != nil {
logger.Printf("[ERROR] Issuer Certificate extraction failed: %v", err)
return
}

// Check certificate status
isValid, err := validateCertificate(issuerCertificate, certificate)
if err != nil {
logger.Printf("[ERROR] Certificate validation failed: %v", err)
return
}

if !isValid {
logger.Printf("[ERROR] Certificate is revoked")
}

logger.Printf("[INFO] Certificate validation successful")
}

// getCertificateFromConfig extracts the certificate from the configuration
func getCertificateFromConfig(config *Configuration) (*x509.Certificate, error) {
if config.MTLSConfiguration == nil {
return nil, fmt.Errorf("MTLS Configuration is required for certificate extraction.")
}

if config.MTLSConfiguration.CertFile == nil {
return nil, fmt.Errorf("No Certificate found")
}

pemData := config.MTLSConfiguration.CertFile

block, _ := pem.Decode(pemData)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}

cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %v", err)
}

return cert, nil
}

func getIssuerCertificate() (*x509.Certificate, error) {
// Load issuer certificate from file
issuerCertPath := "mtls-test/ca-chain.cert.pem"
issuerCert, err := loadCACertificate(issuerCertPath)
if err != nil {
return nil, fmt.Errorf("Failed to load CA Certificate")
}

return issuerCert, nil
}

// loadCACertificate loads the CA certificate from file
func loadCACertificate(caCertPath string) (*x509.Certificate, error) {
pemData, err := ioutil.ReadFile(caCertPath)
if err != nil {
return nil, fmt.Errorf("failed to read CA certificate: %v", err)
}

block, _ := pem.Decode(pemData)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}

cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse CA certificate: %v", err)
}

return cert, nil
}

// validateCertificate checks if the certificate is valid and not revoked
func validateCertificate(caCert *x509.Certificate, cert *x509.Certificate) (bool, error) {
certPool := x509.NewCertPool()
certPool.AddCert(caCert)

// Verify certificate
opts := x509.VerifyOptions{
Roots: certPool,
CurrentTime: cert.NotBefore,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
Intermediates: x509.NewCertPool(),
}

_, err := cert.Verify(opts)
if err != nil {
return false, fmt.Errorf("certificate verification failed: %v", err)
}

// Perform CRL validation
if len(cert.CRLDistributionPoints) > 0 {
isRevoked, err := checkCRL(cert)
if err != nil {
return false, err
}
if isRevoked {
return false, nil
}
}

return true, nil
}

// checkCRL performs a basic CRL check
func checkCRL(cert *x509.Certificate) (bool, error) {
for _, crlURL := range cert.CRLDistributionPoints {
resp, err := http.Get(crlURL)
if err != nil {
continue
}
defer resp.Body.Close()

crlBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
continue
}

crl, err := x509.ParseCRL(crlBytes)
if err != nil {
continue
}

for _, revokedCert := range crl.TBSCertList.RevokedCertificates {
if cert.SerialNumber.Cmp(revokedCert.SerialNumber) == 0 {
return true, nil // Certificate is revoked
}
}
}

return false, nil // Certificate is not revoked
}
35 changes: 35 additions & 0 deletions resources/sdk/purecloudgo/templates/README.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,41 @@ if err != nil {
// If your private key is passphrase-protected, make sure to decrypt it before passing to SetMTLSContents
```

### Using Pre Commit and Post Commit Hooks

For any custom requirements like pre validations or post cleanups (for ex: OCSP and CRL validation), we can inject the prehook and posthook functions.
The SDK's default client will make sure the injected hook functions are executed.

```go
// PreHook implements the certificate revocation check
func PreHook(logger retryablehttp.Logger, req *http.Request, retry int) {
config := platformclientv2.GetDefaultConfiguration()

// certificate extraction logic

// Check certificate status
isValid, err := validateCertificate(issuerCertificate, certificate)
if err != nil {
logger.Printf("[ERROR] Certificate validation failed: %v", err)
return
}

if !isValid {
logger.Printf("[ERROR] Certificate is revoked")
}
else {
logger.Printf("[INFO] Certificate validation successful")
}
}

// Set MTLS certificates
config.APIClient.SetMTLSCertificates("mtls-test/localhost.cert.pem", "mtls-test/localhost.key.pem", "mtls-test/ca-chain.cert.pem")

// Set the pre-hook
config.APIClient.client.SetPreHook(PreHook)

```

## Versioning

The SDK's version is incremented according to the [Semantic Versioning Specification](https://semver.org/). The decision to increment version numbers is determined by [diffing the Platform API's swagger](https://github.com/purecloudlabs/platform-client-sdk-common/blob/master/modules/swaggerDiff.js) for automated builds, and optionally forcing a version bump when a build is triggered manually (e.g. releasing a bugfix).
Expand Down
4 changes: 2 additions & 2 deletions resources/sdk/purecloudgo/templates/apiclient.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -391,12 +391,12 @@ func (c *APIClient) CallAPI(path string, method string,
c.client.SetRetryWaitMin(c.configuration.RetryConfiguration.RetryWaitMin)
c.client.SetRetryMax(c.configuration.RetryConfiguration.RetryMax)
if c.configuration.RetryConfiguration.RequestLogHook != nil {
c.client.SetRequestLogHook(func(_ retryablehttp.Logger, req *http.Request, retryNumber int) {
c.client.SetPreHook(func(_ retryablehttp.Logger, req *http.Request, retryNumber int) {
c.configuration.RetryConfiguration.RequestLogHook(req, retryNumber)
})
}
if c.configuration.RetryConfiguration.ResponseLogHook != nil {
c.client.SetResponseLogHook(func(_ retryablehttp.Logger, res *http.Response) {
c.client.SetPostHook(func(_ retryablehttp.Logger, res *http.Response) {
c.configuration.RetryConfiguration.ResponseLogHook(res)
})
}
Expand Down