Spaces:
Running
Running
/** | |
* Net - abstraction layer around Node's HTTP/S request system. | |
* Advantages: | |
* - easier acquiring of data | |
* - mass disabling of outgoing requests via Config. | |
*/ | |
import * as https from 'https'; | |
import * as http from 'http'; | |
import * as url from 'url'; | |
import * as Streams from './streams'; | |
declare const Config: any; | |
export interface PostData { | |
[key: string]: string | number; | |
} | |
export interface NetRequestOptions extends https.RequestOptions { | |
body?: string | PostData; | |
writable?: boolean; | |
query?: PostData; | |
} | |
export class HttpError extends Error { | |
statusCode?: number; | |
body: string; | |
constructor(message: string, statusCode: number | undefined, body: string) { | |
super(message); | |
this.name = 'HttpError'; | |
this.statusCode = statusCode; | |
this.body = body; | |
Error.captureStackTrace(this, HttpError); | |
} | |
} | |
export class NetStream extends Streams.ReadWriteStream { | |
opts: NetRequestOptions | null; | |
uri: string; | |
request: http.ClientRequest; | |
/** will be a Promise before the response is received, and the response itself after */ | |
response: Promise<http.IncomingMessage | null> | http.IncomingMessage | null; | |
statusCode: number | null; | |
/** response headers */ | |
headers: http.IncomingHttpHeaders | null; | |
state: 'pending' | 'open' | 'timeout' | 'success' | 'error'; | |
constructor(uri: string, opts: NetRequestOptions | null = null) { | |
super(); | |
this.statusCode = null; | |
this.headers = null; | |
this.uri = uri; | |
this.opts = opts; | |
// make request | |
this.response = null; | |
this.state = 'pending'; | |
this.request = this.makeRequest(opts); | |
} | |
makeRequest(opts: NetRequestOptions | null) { | |
if (!opts) opts = {}; | |
let body = opts.body; | |
if (body && typeof body !== 'string') { | |
if (!opts.headers) opts.headers = {}; | |
if (!opts.headers['Content-Type']) { | |
opts.headers['Content-Type'] = 'application/x-www-form-urlencoded'; | |
} | |
body = NetStream.encodeQuery(body); | |
} | |
if (opts.query) { | |
this.uri += (this.uri.includes('?') ? '&' : '?') + NetStream.encodeQuery(opts.query); | |
} | |
if (body) { | |
if (!opts.headers) opts.headers = {}; | |
if (!opts.headers['Content-Length']) { | |
opts.headers['Content-Length'] = Buffer.byteLength(body); | |
} | |
} | |
const protocol = url.parse(this.uri).protocol; | |
const net = protocol === 'https:' ? https : http; | |
let resolveResponse: ((value: http.IncomingMessage | null) => void) | null; | |
this.response = new Promise(resolve => { | |
resolveResponse = resolve; | |
}); | |
const request = net.request(this.uri, opts, response => { | |
this.state = 'open'; | |
this.nodeReadableStream = response; | |
this.response = response; | |
this.statusCode = response.statusCode || null; | |
this.headers = response.headers; | |
response.setEncoding('utf-8'); | |
resolveResponse!(response); | |
resolveResponse = null; | |
response.on('data', data => { | |
this.push(data); | |
}); | |
response.on('end', () => { | |
if (this.state === 'open') this.state = 'success'; | |
if (!this.atEOF) this.pushEnd(); | |
}); | |
}); | |
request.on('close', () => { | |
if (!this.atEOF) { | |
this.state = 'error'; | |
this.pushError(new Error("Unexpected connection close")); | |
} | |
if (resolveResponse) { | |
this.response = null; | |
resolveResponse(null); | |
resolveResponse = null; | |
} | |
}); | |
request.on('error', error => { | |
if (!this.atEOF) this.pushError(error, true); | |
}); | |
if (opts.timeout || opts.timeout === undefined) { | |
request.setTimeout(opts.timeout || 5000, () => { | |
this.state = 'timeout'; | |
this.pushError(new Error("Request timeout")); | |
request.abort(); | |
}); | |
} | |
if (body) { | |
request.write(body); | |
request.end(); | |
if (opts.writable) { | |
throw new Error(`options.body is what you would have written to a NetStream - you must choose one or the other`); | |
} | |
} else if (opts.writable) { | |
this.nodeWritableStream = request; | |
} else { | |
request.end(); | |
} | |
return request; | |
} | |
static encodeQuery(data: PostData) { | |
let out = ''; | |
for (const key in data) { | |
if (out) out += `&`; | |
out += `${key}=${encodeURIComponent(`${data[key]}`)}`; | |
} | |
return out; | |
} | |
_write(data: string | Buffer): Promise<void> | void { | |
if (!this.nodeWritableStream) { | |
throw new Error("You must specify opts.writable to write to a request."); | |
} | |
const result = this.nodeWritableStream.write(data); | |
if (result !== false) return undefined; | |
if (!this.drainListeners.length) { | |
this.nodeWritableStream.once('drain', () => { | |
for (const listener of this.drainListeners) listener(); | |
this.drainListeners = []; | |
}); | |
} | |
return new Promise(resolve => { | |
this.drainListeners.push(resolve); | |
}); | |
} | |
_read() { | |
this.nodeReadableStream?.resume(); | |
} | |
_pause() { | |
this.nodeReadableStream?.pause(); | |
} | |
} | |
export class NetRequest { | |
uri: string; | |
/** Response from last request, made so response stuff is available without being hacky */ | |
response?: http.IncomingMessage; | |
constructor(uri: string) { | |
this.uri = uri; | |
} | |
/** | |
* Makes a http/https get request to the given link and returns a stream. | |
* The request data itself can be read with ReadStream#readAll(). | |
* The NetStream class also holds headers and statusCode as a property. | |
* | |
* @param opts request opts - headers, etc. | |
* @param body POST body | |
*/ | |
getStream(opts: NetRequestOptions = {}) { | |
if (typeof Config !== 'undefined' && Config.noNetRequests) { | |
throw new Error(`Net requests are disabled.`); | |
} | |
const stream = new NetStream(this.uri, opts); | |
return stream; | |
} | |
/** | |
* Makes a basic http/https request to the URI. | |
* Returns the response data. | |
* | |
* Will throw if the response code isn't 200 OK. | |
* | |
* @param opts request opts - headers, etc. | |
*/ | |
async get(opts: NetRequestOptions = {}): Promise<string> { | |
const stream = this.getStream(opts); | |
const response = await stream.response; | |
if (response) this.response = response; | |
if (response && response.statusCode !== 200) { | |
throw new HttpError(response.statusMessage || "Connection error", response.statusCode, await stream.readAll()); | |
} | |
return stream.readAll(); | |
} | |
/** | |
* Makes a http/https POST request to the given link. | |
* @param opts request opts - headers, etc. | |
* @param body POST body | |
*/ | |
post(opts: Omit<NetRequestOptions, 'body'>, body: PostData | string): Promise<string>; | |
/** | |
* Makes a http/https POST request to the given link. | |
* @param opts request opts - headers, etc. | |
*/ | |
post(opts?: NetRequestOptions): Promise<string>; | |
post(opts: NetRequestOptions = {}, body?: PostData | string) { | |
if (!body) body = opts.body; | |
return this.get({ | |
...opts, | |
method: 'POST', | |
body, | |
}); | |
} | |
} | |
export const Net = Object.assign((path: string) => new NetRequest(path), { | |
NetRequest, NetStream, | |
}); | |