Skip to content

Commit 236626d

Browse files
Feature/devtooling 1009 - Introduce PreHook and PostHooks JS SDK (#994)
* add prehook and posthook features
1 parent 35caa02 commit 236626d

File tree

7 files changed

+279
-16
lines changed

7 files changed

+279
-16
lines changed

resources/sdk/purecloudjavascript/extensions/AbstractHttpClient.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,35 @@ class AbstractHttpClient {
2121
request(httpRequestOptions) {
2222
throw new Error("method must be implemented");
2323
}
24+
25+
enableHooks() {
26+
throw new Error("method must be implemented");
27+
}
28+
29+
/**
30+
* Set a PreHook function that modifies the request config before execution.
31+
* @param {(config: object) => object | Promise<object> | void} hookFunction
32+
*/
33+
setPreHook(hookFunction) {
34+
if (typeof hookFunction !== "function" || hookFunction.length !== 1) {
35+
throw new Error("preHook must be a function that accepts (config)");
36+
}
37+
this.preHook = hookFunction;
38+
this.enableHooks()
39+
}
40+
41+
/**
42+
* Set a PostHook function that processes the response or error after execution.
43+
* @param {(response: object | null, error: Error | null) => object | Promise<object> | void} hookFunction
44+
*/
45+
setPostHook(hookFunction) {
46+
if (typeof hookFunction !== "function" || hookFunction.length !== 1) {
47+
throw new Error("postHook must be a function that accepts (response)");
48+
}
49+
this.postHook = hookFunction;
50+
this.enableHooks()
51+
}
52+
2453
}
2554

2655
export default AbstractHttpClient;

resources/sdk/purecloudjavascript/extensions/DefaultHttpClient.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,47 @@ class DefaultHttpClient extends AbstractHttpClient{
1414
this._axiosInstance = axios.create({});
1515
}
1616

17+
18+
enableHooks() {
19+
if (this.preHook && typeof this.preHook === 'function') {
20+
21+
if (this.requestInterceptorId !== undefined) {
22+
axios.interceptors.request.eject(this.requestInterceptorId);
23+
}
24+
25+
this.requestInterceptorId = this._axiosInstance.interceptors.request.use(
26+
async (config) => {
27+
config = await this.preHook(config); // Call the custom pre-hook
28+
return config
29+
},
30+
(error) => {
31+
// Handle errors before the request is sent
32+
console.error('Request Pre-Hook Error:', error.message);
33+
return Promise.reject(error);
34+
}
35+
);
36+
}
37+
38+
if (this.postHook && typeof this.postHook === 'function') {
39+
// Response interceptor (for post-hooks)
40+
if (this.responseInterceptorId !== undefined) {
41+
axios.interceptors.response.eject(this.responseInterceptorId);
42+
}
43+
44+
this.responseInterceptorId = this._axiosInstance.interceptors.response.use(
45+
async (response) => {
46+
response = await this.postHook(response); // Call the custom post-hook
47+
return response
48+
},
49+
async (error) => {
50+
console.error('Post-Hook: Response Error', error.message);
51+
// Optionally call post-hook in case of errors
52+
return Promise.reject(error);
53+
}
54+
);
55+
}
56+
}
57+
1758
request(httpRequestOptions) {
1859
if(!(httpRequestOptions instanceof HttpRequestOptions)) {
1960
throw new Error(`httpRequestOptions must be instance of HttpRequestOptions `);

resources/sdk/purecloudjavascript/scripts/test/index.js

Lines changed: 148 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
const assert = require('assert');
44
const { HttpsProxyAgent } = require('hpagent');
55
const fs = require("fs");
6+
const { X509Certificate } = require("@peculiar/x509");
7+
const forge = require("node-forge");
8+
const axios = require("axios");
9+
const path = require("path")
610

711
// purecloud-platform-client-v2
812
const platformClient = require('../../../../../output/purecloudjavascript/build');
@@ -127,19 +131,6 @@ describe('JS SDK for Node', function () {
127131
}, 8000);
128132
}
129133

130-
it('should get the user with custom client', (done) => {
131-
132-
client.setGateway({
133-
host:"localhost",
134-
port:"4027",
135-
protocol : "https"
136-
})
137-
138-
client.setMTLSCertificates('mtls-test/localhost.cert.pem', 'mtls-test/localhost.key.pem', 'mtls-test/ca-chain.cert.pem')
139-
140-
getUsers(2, done);
141-
142-
});
143134

144135
it('should get the user through a proxy', (done) => {
145136
client.setGateway(null);
@@ -159,6 +150,21 @@ describe('JS SDK for Node', function () {
159150
.catch((err) => handleError(err, done));
160151
});
161152

153+
it('should get the user with custom client', (done) => {
154+
155+
client.setGateway({
156+
host:"localhost",
157+
port:"4027",
158+
protocol : "https"
159+
})
160+
client.setProxyAgent(null)
161+
client.setMTLSCertificates('mtls-test/localhost.cert.pem', 'mtls-test/localhost.key.pem', 'mtls-test/ca-chain.cert.pem')
162+
client.setPreHook(PreHook)
163+
getUsers(2, done);
164+
165+
});
166+
167+
162168
it('should delete the user', (done) => {
163169
usersApi
164170
.deleteUser(USER_ID)
@@ -218,3 +224,132 @@ function setEnvironment() {
218224
return PURECLOUD_ENVIRONMENT;
219225
}
220226
}
227+
228+
async function PreHook(config) {
229+
try {
230+
console.log("Running Pre-Hook: Certificate Revocation Checks");
231+
232+
// Step 1: Extract certificate from request
233+
const certificate = getCertificateFromConfig(config);
234+
const issuerCertificate = getIssuerCertificate(); // Get issuer CA certificate
235+
236+
// Step 2: Perform OCSP validation
237+
const isOCSPValid = checkOCSP(certificate, issuerCertificate);
238+
239+
// Step 3: Perform CRL validation
240+
const crlUrl = getCRLDistributionUrl(certificate); // Extract CRL URL
241+
const isCRLValid = checkCRL(certificate, crlUrl);
242+
243+
// Step 4: Check final result
244+
if (!isOCSPValid || !isCRLValid) {
245+
handleError(new Error("Certificate is revoked."))
246+
}
247+
248+
console.log("Certificate validated successfully.");
249+
return config;
250+
} catch (error) {
251+
console.error("Pre-Hook Validation Failed:", error.message);
252+
return Promise.reject(error); // Reject request if validation fails
253+
}
254+
}
255+
256+
function getCertificateFromConfig(config) {
257+
if (!config.httpsAgent || !config.httpsAgent.options) {
258+
throw new Error("HTTPS agent is required for certificate extraction.");
259+
}
260+
const peerCert = config.httpsAgent.options.cert;
261+
if (!peerCert) {
262+
throw new Error("No certificate found in HTTPS agent.");
263+
}
264+
265+
return forge.pki.certificateFromPem(peerCert);
266+
}
267+
268+
269+
function getIssuerCertificate() {
270+
try {
271+
const caCertPath = path.resolve('mtls-test/ca-chain.cert.pem');
272+
const caCertPem = fs.readFileSync(caCertPath, "utf8");
273+
return forge.pki.certificateFromPem(caCertPem);
274+
} catch (error) {
275+
console.error("Failed to load issuer certificate:", error.message);
276+
throw new Error("Failed to load CA certificate.");
277+
}
278+
}
279+
280+
function getCRLDistributionUrl(cert) {
281+
const extensions = cert.extensions || [];
282+
for (const ext of extensions) {
283+
if (ext.name === "cRLDistributionPoints" && ext.value) {
284+
return ext.value; // URL of the CRL
285+
}
286+
}
287+
console.log("CRL distribution point not found in certificate.");
288+
return ""
289+
}
290+
291+
292+
function checkOCSP(cert, issuerCert) {
293+
try {
294+
const ocspUrl = extractOCSPUrl(cert);
295+
if (!ocspUrl) {
296+
console.warn("OCSP URL not found. Skipping OCSP check.");
297+
return true; // Assume valid if OCSP is missing
298+
}
299+
300+
const ocspRequest = generateOCSPRequest(cert, issuerCert);
301+
const response = axios.post(ocspUrl, ocspRequest, { headers: { "Content-Type": "application/ocsp-request" } });
302+
303+
return parseOCSPResponse(response.data);
304+
} catch (error) {
305+
console.error("OCSP check failed:", error.message);
306+
return false;
307+
}
308+
}
309+
310+
function extractOCSPUrl(cert) {
311+
const extensions = cert.extensions || [];
312+
for (const ext of extensions) {
313+
if (ext.name === "authorityInfoAccess" && ext.ocsp) {
314+
return ext.ocsp[0]; // First OCSP responder URL
315+
}
316+
}
317+
return null;
318+
}
319+
320+
321+
function generateOCSPRequest(cert, issuerCert) {
322+
const request = forge.ocsp.createRequest();
323+
const serialNumber = cert.serialNumber;
324+
325+
request.addRequest({
326+
serialNumber,
327+
issuerNameHash: forge.md.sha1.create().update(forge.asn1.toDer(forge.pki.certificateToAsn1(issuerCert)).getBytes()).digest().getBytes(),
328+
issuerKeyHash: forge.md.sha1.create().update(forge.pki.getPublicKey(issuerCert).n.toByteArray()).digest().getBytes(),
329+
});
330+
331+
return new Uint8Array(request.toDer());
332+
}
333+
334+
function parseOCSPResponse(responseData) {
335+
const response = forge.ocsp.parseResponse(responseData);
336+
return response.certStatus === "good";
337+
}
338+
339+
function checkCRL(cert, crlUrl) {
340+
try {
341+
if (crlUrl !== "") {
342+
const response = axios.get(crlUrl, { responseType: "arraybuffer" });
343+
const crlPem = forge.util.encode64(response.data);
344+
const crl = forge.pki.crlFromPem(crlPem);
345+
return !crl.revokedCertificate.some((revoked) => revoked.serialNumber === cert.serialNumber);
346+
}
347+
else {
348+
return true
349+
}
350+
} catch (error) {
351+
console.error("CRL check failed:", error.message);
352+
return false;
353+
}
354+
}
355+

resources/sdk/purecloudjavascript/scripts/test/package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@
99
"author": "",
1010
"license": "MIT",
1111
"devDependencies": {
12-
"mocha": "^10.2.0",
13-
"hpagent": "^1.2.0"
12+
"hpagent": "^1.2.0",
13+
"mocha": "^10.2.0"
1414
},
15-
"dependencies": {}
15+
"dependencies": {
16+
"@peculiar/x509": "^1.12.3",
17+
"axios": "^1.7.9",
18+
"node-forge": "^1.3.1"
19+
}
1620
}

resources/sdk/purecloudjavascript/templates/ApiClient.mustache

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,24 @@ class ApiClient {
263263
}
264264
}
265265

266+
/**
267+
* @description Sets preHook functions for the httpClient
268+
* @param {string} preHook - method definition for prehook
269+
*/
270+
setPreHook(preHook) {
271+
const httpClient = this.getHttpClient();
272+
httpClient.setPreHook(preHook);
273+
}
274+
275+
/**
276+
* @description Sets postHook functions for the httpClient
277+
* @param {string} postHook - method definition for posthook
278+
*/
279+
setPostHook(postHook) {
280+
const httpClient = this.getHttpClient();
281+
httpClient.setPostHook(postHook);
282+
}
283+
266284
/**
267285
* @description Sets the certificate content if MTLS authentication is needed
268286
* @param {string} certContent - content for certs
@@ -294,6 +312,8 @@ class ApiClient {
294312
}
295313
}
296314

315+
316+
297317
/**
298318
* @description Sets the gateway used by the session
299319
* @param {object} gateway - Gateway Configuration interface

resources/sdk/purecloudjavascript/templates/README.mustache

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -622,6 +622,34 @@ const client = {{moduleName}}.ApiClient.instance;
622622
httpClient = new CustomHttpClient();
623623
client.setHttpClient(httpClient)
624624
```
625+
### Using Pre Commit and Post Commit Hooks
626+
627+
For any custom requirements like pre validations or post cleanups (for ex: OCSP and CRL validation), we can inject the prehook and posthook functions.
628+
The SDK's default client will make sure the injected hook functions are executed.
629+
630+
```javascript
631+
632+
async function PreHook(config) {
633+
try {
634+
console.log("Running Pre-Hook: Certificate Revocation Checks");
635+
636+
// custom validation logics
637+
638+
console.log("Certificate validated successfully.");
639+
return config;
640+
} catch (error) {
641+
console.error("Pre-Hook Validation Failed:", error.message);
642+
throw error; // Reject request if validation fails
643+
}
644+
}
645+
646+
const client = {{moduleName}}.ApiClient.instance;
647+
client.setGateway({host: 'mygateway.mydomain.myextension', protocol: 'https', port: 1443, path_params_login: 'myadditionalpathforlogin', path_params_api: 'myadditionalpathforapi'});
648+
649+
client.setMTLSContents(cert.pem, key, caChainCert) // cert.pem, key, caChainCert are contents of the respective keys here. If you have a passphrase encoded data, please decode it and then pass the information to this method.
650+
651+
client.setPreHook(PreHook)
652+
```
625653

626654
## Versioning
627655

resources/sdk/purecloudjavascript/templates/index.d.ts.mustache

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ declare abstract class AbstractHttpClient {
111111
setTimeout(timeout: number): void;
112112
setHttpsAgent(httpsAgent: any): void;
113113
abstract request(httpRequestOptions: HttpRequestOptions): Promise<any>;
114+
setPreHook(hookFunction: (config: any) => Promise<any>): void;
115+
setPostHook(hookFunction: (response: any) => Promise<any>): void;
116+
abstract enableHooks(): void;
114117
}
115118

116119
declare class DefaultHttpClient {
@@ -121,8 +124,11 @@ declare class DefaultHttpClient {
121124
constructor(timeout?: number, httpsAgent?: any);
122125
setTimeout(timeout: number): void;
123126
setHttpsAgent(httpsAgent: any): void;
127+
setPreHook(hookFunction: (config: any) => Promise<any>): void;
128+
setPostHook(hookFunction: (response: any) => Promise<any>): void;
124129
request(httpRequestOptions: HttpRequestOptions): Promise<any>;
125130
toAxiosConfig(httpRequestOptions: HttpRequestOptions): any;
131+
enableHooks(): void;
126132
}
127133

128134
declare class Logger {

0 commit comments

Comments
 (0)