Skip to content

Instantly share code, notes, and snippets.

@benc-uk
Last active August 5, 2024 09:59
Show Gist options
  • Save benc-uk/cf56f16ca6f931afbd3476962285c1e9 to your computer and use it in GitHub Desktop.
Save benc-uk/cf56f16ca6f931afbd3476962285c1e9 to your computer and use it in GitHub Desktop.
Node.js
/*
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()
})
}
}
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, "");
}
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);
//
// 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