Skip to content

Instantly share code, notes, and snippets.

@FND
Last active November 9, 2020 10:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save FND/0bbb3816499065db06d4e974b9dec2db to your computer and use it in GitHub Desktop.
Save FND/0bbb3816499065db06d4e974b9dec2db to your computer and use it in GitHub Desktop.
stateful HTTP client (Node)
/node_modules
"use strict";
let { Cookie, CookieJar } = require("tough-cookie");
let { promisify } = require("util");
exports.HTTPClient = class HTTPClient {
constructor() {
this.jar = new CookieJar();
}
async request(method, uri, headers, body, _trail = []) {
if(!headers) {
headers = {};
}
let { jar } = this;
let getCookies = await promisify(jar.getCookies.bind(jar)); // XXX: inefficient
let cookies = await getCookies(uri);
if(cookies.length) {
// XXX: overwrites caller's existing cookie header, if any
headers.cookie = cookies.map(c => c.cookieString()).join("; ");
}
_trail.length || console.log("\n----"); // XXX: DEBUG
console.log("\n>", method, uri); // XXX: DEBUG
console.log("> Cookie:", headers.cookie || "--"); // XXX: DEBUG
let res = await _request(method, uri, headers, body);
console.log("\n<", res.status); // XXX: DEBUG
cookies = res.headers["set-cookie"];
if(cookies) {
cookies.forEach(cookie => void console.log("< Set-Cookie:", cookie)); // XXX: DEBUG
cookies = cookies.pop ?
cookies.map(Cookie.parse) :
[Cookie.parse(cookies)]; // TODO: error handling
cookies.forEach(cookie => {
jar.setCookie(cookie, uri); // TODO: error handling
});
}
// follow redirects
let { status } = res;
let { location } = res.headers;
if(location && status >= 300 && status < 400) {
console.log("< Location:", location); // XXX: DEBUG
if(!location.includes("://")) { // not fully qualified
let _uri = new URL(uri);
location = _uri.origin + location;
console.log("< Location:", location); // XXX: DEBUG
}
// XXX: simplistic because `GET` might not be correct for 307, 308 and perhaps 302
[res, _trail] = await this.request("GET", location, null, null,
_trail.concat(res));
}
return [res, _trail];
}
};
function _request(method, uri, headers, body) {
let proto = determineProtocol(uri);
return new Promise((resolve, reject) => {
let onResponse = res => {
let chunks = [];
res.on("data", chunk => {
chunks.push(chunk);
});
res.on("end", () => {
let body = chunks.length && chunks.
// XXX: assumes UTF-8 string
map(buffer => buffer.toString("utf8")).join("");
chunks = null;
resolve(new HTTPResponse(uri, res.statusCode, res.headers, body));
});
res.on("error", err => {
reject(err);
});
};
let req = proto.request(uri, { method }, onResponse);
req.on("error", err => void reject(err));
if(body) {
console.log("----", body); // XXX: DEBUG
req.write(body);
}
req.end();
});
}
class HTTPResponse {
constructor(uri, status, headers, body) {
this.uri = uri;
this.status = status;
this.headers = headers;
this.body = body || null;
}
}
function determineProtocol(uri) {
if(uri.startsWith("https://")) {
return require("https");
}
if(uri.startsWith("http://")) {
return require("http");
}
throw new Error(`unrecognized URI: ${uri}`);
}
{
"dependencies": {
"tough-cookie": "^4.0.0"
}
}
#!/usr/bin/env node
"use strict";
let { HTTPClient } = require("./http");
let qs = require("querystring");
let ENTRY_POINT = "http://example.org";
let LOGIN_FORM = "https://example.org/login";
let RESOURCE = "https://example.org/dashboard";
let USERNAME = "foo";
let PASSWORD = "bar";
main();
async function main() {
let client = new HTTPClient();
await client.request("GET", ENTRY_POINT);
await client.request("POST", LOGIN_FORM, {
Acept: "text/html",
"Content-Type": "application/x-www-form-urlencoded"
}, qs.stringify({
username: USERNAME,
password: PASSWORD
}));
let [res, trail] = await client.request("GET", RESOURCE, {
Accept: "application/json"
});
console.log("~~", res.status, res.body);
console.log("--", trail);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment