mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-03-27 04:05:40 -05:00
234 lines
6.3 KiB
TypeScript
234 lines
6.3 KiB
TypeScript
/**
|
|
* 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';
|
|
|
|
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 as string;
|
|
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;
|
|
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 && 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 function Net(uri: string) {
|
|
return new NetRequest(uri);
|
|
}
|