Last active
August 5, 2024 09:59
-
-
Save benc-uk/cf56f16ca6f931afbd3476962285c1e9 to your computer and use it in GitHub Desktop.
Node.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Simple compact HTTP library in vanilla Node.js | |
v1.1 - Ben Coleman, 2020 & 2021 | |
*/ | |
const url = require('url') | |
module.exports = class HTTP { | |
constructor(base, parseResults = true, auth = null, headers = {}, checkStatus = true) { | |
this.baseUrl = base | |
this.parseResults = parseResults | |
this.checkStatus = checkStatus | |
if (auth) { | |
if (!auth.creds) { | |
throw 'HTTP error: auth creds must be set to `token` or `user:password`' | |
} | |
switch (auth.type.toLowerCase()) { | |
case 'bearer': | |
this.baseHeaders = { Authorization: `Bearer ${auth.creds}`, ...headers } | |
break | |
case 'basic': { | |
let basicAuthBuff = Buffer.from(auth.creds) | |
this.baseHeaders = { Authorization: `Basic ${basicAuthBuff.toString('base64')}`, ...headers } | |
break | |
} | |
default: | |
throw "HTTP error: auth type must be 'basic' or 'bearer'" | |
} | |
} else { | |
this.baseHeaders = headers | |
} | |
} | |
// HTTP GET wrapper | |
async get(path, headers = {}) { | |
return await this.request(path, 'GET', headers) | |
} | |
// HTTP POST wrapper | |
async post(path, data, contentType = 'application/json', headers = {}) { | |
const reqBody = typeof data == 'object' ? JSON.stringify(data) : data | |
return await this.request(path, 'POST', { 'Content-Type': contentType, ...headers }, reqBody) | |
} | |
// HTTP PUT wrapper | |
async put(path, data, contentType = 'application/json', headers = {}) { | |
const reqBody = typeof data == 'object' ? JSON.stringify(data) : data | |
return await this.request(path, 'PUT', { 'Content-Type': contentType, ...headers }, reqBody) | |
} | |
// HTTP DELETE wrapper | |
async delete(path, data, contentType = 'application/json', headers = {}) { | |
const reqBody = typeof data == 'object' ? JSON.stringify(data) : data | |
return await this.request(path, 'DELETE', { 'Content-Type': contentType, ...headers }, reqBody) | |
} | |
// Generic low level HTTP request wrapped as a promise | |
request(path, method = 'GET', headers = {}, reqBody = null, fullUrl = false) { | |
let requestURL = new url.URL(fullUrl || `${this.baseUrl}${path}`) | |
let options = { | |
method: method, | |
headers: { ...this.baseHeaders, ...headers }, | |
} | |
return new Promise((resolve, reject) => { | |
const httpLib = requestURL.protocol && requestURL.protocol.startsWith('https') ? require('https') : require('http') | |
if (reqBody) { | |
options.headers['Content-Length'] = reqBody.length | |
} | |
if (this.debug) { | |
console.log(`### HTTP request: ${requestURL}\n### HTTP options: ${JSON.stringify(options)}\n### HTTP body: ${reqBody}`) | |
} | |
const request = httpLib.request(requestURL, options, (response) => { | |
let body = [] | |
response.on('data', (chunk) => body.push(chunk)) | |
response.on('end', () => { | |
// Naive but functional handling of HTTP redirects | |
if ((response.statusCode == 301 || response.statusCode == 302) && response.headers.location) { | |
const redirect = this.request(null, method, headers, reqBody, response.headers.location) | |
resolve(redirect) | |
} | |
let data | |
try { | |
data = this.parseResults ? JSON.parse(body.join('')) : body.join('') | |
} catch (err) { | |
reject(`JSON parsing of response body failed - ${err}`) | |
} | |
// Our custom response result wrapper, with Axios like fields | |
const resp = { | |
headers: response.headers, | |
status: response.statusCode, | |
statusText: response.statusMessage, | |
data, | |
} | |
if (this.debug) { | |
console.log(`### HTTP client response:`, resp) | |
} | |
if (this.checkStatus && (response.statusCode < 200 || response.statusCode > 299)) { | |
console.error(`Error! Request failed with status code: ${response.statusCode}`) | |
reject(resp) | |
} | |
resolve(resp) | |
}) | |
}) | |
request.on('error', (err) => reject(err)) | |
if (reqBody) { | |
request.write(reqBody) | |
} | |
request.end() | |
}) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import crypto from "k6/crypto"; | |
import encoding from "k6/encoding"; | |
const algToHash = { | |
HS256: "sha256", | |
HS384: "sha384", | |
HS512: "sha512" | |
}; | |
// Encodes a JWT token. | |
export function encode(payload, secret, algorithm = "HS256") { | |
algorithm = algorithm || "HS256"; | |
let header = encoding.b64encode(JSON.stringify({ typ: "JWT", alg: algorithm }), "rawurl"); | |
payload = encoding.b64encode(JSON.stringify(payload), "rawurl"); | |
let sig = sign(header + "." + payload, algToHash[algorithm], secret); | |
return [header, payload, sig].join("."); | |
} | |
// Decode a JWT token and return the payload. | |
export function decode(token, secret, algorithm) { | |
let parts = token.split('.'); | |
let header = JSON.parse(encoding.b64decode(parts[0], "rawurl")); | |
let payload = JSON.parse(encoding.b64decode(parts[1], "rawurl")); | |
algorithm = algorithm || algToHash[header.alg]; | |
if (sign(parts[0] + "." + parts[1], algorithm, secret) != parts[2]) { | |
throw Error("JWT signature verification failed"); | |
} | |
return payload; | |
} | |
// Helper for generating a JWT token in OAuth 2.0 format | |
export function encodeOAuthToken(iss, aud, expireTime, claims, secret) { | |
let message = { | |
iss: iss, | |
aud: aud, | |
exp: Math.floor(Date.now() / 1000) + expireTime, | |
iat: Math.floor(Date.now() / 1000), | |
nbf: Math.floor(Date.now() / 1000), | |
}; | |
message = Object.assign({}, message, claims) | |
return encode(message, secret, "HS256"); | |
} | |
// Internal helper for signing | |
function sign(data, hashAlg, secret) { | |
let hasher = crypto.createHMAC(hashAlg, secret); | |
hasher.update(data); | |
// Some manual base64 rawurl encoding as `Hasher.digest(encodingType)` | |
// doesn't support that encoding type yet. | |
return hasher.digest("base64").replace(/\//g, "_").replace(/\+/g, "-").replace(/=/g, ""); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const rs = require("jsrsasign"); | |
const rsu = require("jsrsasign-util"); | |
// Load key from PEM file | |
const keyPem = rsu.readFile("private_key.pem"); | |
const privateKey = rs.KEYUTIL.getKey(keyPem); | |
// Header | |
const header = { alg: "RS256", typ: "JWT" }; | |
// Payload | |
let payload = {}; | |
const timeNow = rs.jws.IntDate.get("now"); | |
const timeEnd = rs.jws.IntDate.get("now + 1hour"); | |
payload.iss = "http://localhost:8080/blah"; | |
payload.sub = "blah blah"; | |
payload.nbf = timeNow; | |
payload.iat = timeNow; | |
payload.exp = timeEnd; | |
payload.jti = "blah"; | |
payload.aud = ["blah"]; | |
//payload.nbf = timeNow; | |
payload.iat = timeNow; | |
payload.exp = timeEnd; | |
payload.auth_time = timeEnd; | |
// Sign JWT with RSA private key | |
var jwt = rs.jws.JWS.sign( | |
"RS256", | |
JSON.stringify(header), | |
JSON.stringify(payload), | |
privateKey | |
); | |
console.log(jwt); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Generic set of static utility functions | |
// | |
module.exports = { | |
// | |
// Calculate the hash of a string, used for creating node ids | |
// | |
hashStr(s) { | |
let hash = 0, i, chr | |
if (s.length === 0) { return hash } | |
for (i = 0; i < s.length; i++) { | |
chr = s.charCodeAt(i) | |
hash = ((hash << 5) - hash) + chr | |
hash |= 0 // Convert to 32bit integer | |
} | |
return hash | |
}, | |
// | |
// I got tired of checking if multiple nested properties existed in objects | |
// | |
checkNested(obj /*, level1, level2, ... levelN*/) { | |
let args = Array.prototype.slice.call(arguments, 1) | |
for (let i = 0; i < args.length; i++) { | |
if (!obj || !Object.prototype.hasOwnProperty.call(obj, args[i])) { | |
return false | |
} | |
obj = obj[args[i]] | |
} | |
return true | |
}, | |
// | |
// Return nested object properties by string path, e.g. "people.5.name" | |
// | |
objectPath(obj, path) { | |
path = path.replace(/\[(\w+)\]/g, '.$1') // convert indexes to properties | |
path = path.replace(/^\./, '') // strip a leading dot | |
var a = path.split('.') | |
for (var i = 0, n = a.length; i < n; ++i) { | |
var k = a[i] | |
if (k in obj) { | |
obj = obj[k] | |
} else { | |
return | |
} | |
} | |
return obj | |
}, | |
// | |
// Date formatting, always a hoot | |
// | |
dateFromISO8601(isostr) { | |
let parts = isostr.match(/\d+/g) | |
return new Date(parts[0], parts[1] - 1, parts[2], parts[3], parts[4], parts[5]) | |
}, | |
// | |
// Simple random ID generator, good enough, with len=6 it's a 1:56 billion chance of a clash | |
// | |
makeId(len) { | |
var text = "" | |
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" | |
for (var i = 0; i < len; i++) | |
text += possible.charAt(Math.floor(Math.random() * possible.length)) | |
return text | |
}, | |
// | |
// Sleep, call with await | |
// | |
sleep(ms) { | |
return new Promise(resolve => setTimeout(resolve, ms)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment