Last active
November 9, 2020 10:43
-
-
Save FND/0bbb3816499065db06d4e974b9dec2db to your computer and use it in GitHub Desktop.
stateful HTTP client (Node)
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
/node_modules |
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
"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}`); | |
} |
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
{ | |
"dependencies": { | |
"tough-cookie": "^4.0.0" | |
} | |
} |
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
#!/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