@mojojs/core
Version:
Real-time web framework
228 lines • 8.2 kB
JavaScript
import { URL } from 'node:url';
import { CookieJar } from './user-agent/cookie-jar.js';
import { HTTPTransport } from './user-agent/transport/http.js';
import { HTTPSTransport } from './user-agent/transport/https.js';
import { WSTransport } from './user-agent/transport/ws.js';
import { AsyncHooks } from '@mojojs/util';
import FormData from 'form-data';
import yaml from 'js-yaml';
/**
* HTTP and WebSocket user-agent.
*/
class UserAgent {
constructor(options = {}) {
/**
* Cookie jar to use.
*/
this.cookieJar = new CookieJar();
/**
* User-agent hooks.
*/
this.hooks = new AsyncHooks();
/**
* Transport backend to perform HTTP requests with.
*/
this.httpTransport = new HTTPTransport();
/**
* Transport backend to perform HTTPS requests with.
*/
this.httpsTransport = new HTTPSTransport();
/**
* Transport backend to use for WebSocket connections.
*/
this.wsTransport = new WSTransport();
this.baseURL = options.baseURL;
this.maxRedirects = options.maxRedirects ?? 0;
this.name = options.name;
}
async [Symbol.asyncDispose]() {
await this.destroy();
}
/**
* Add a hook to extend the user-agent.
*/
addHook(name, fn) {
this.hooks.addHook(name, fn);
return this;
}
/**
* Destroy all active keep-alive connections.
*/
async destroy() {
await this.httpTransport.destroy();
await this.httpsTransport.destroy();
}
/**
* Perform `DELETE` request.
*/
async delete(url, options) {
return await this._requestConfig('DELETE', url, options);
}
/**
* Perform `GET` request.
*/
async get(url, options) {
return await this._requestConfig('GET', url, options);
}
/**
* Perform `HEAD` request.
*/
async head(url, options) {
return await this._requestConfig('HEAD', url, options);
}
/**
* Perform `OPTIONS` request.
*/
async options(url, options) {
return await this._requestConfig('OPTIONS', url, options);
}
/**
* Perform `PATCH` request.
*/
async patch(url, options) {
return await this._requestConfig('PATCH', url, options);
}
/**
* Perform `POST` request.
*/
async post(url, options) {
return await this._requestConfig('POST', url, options);
}
/**
* Perform `PUT` request.
*/
async put(url, options) {
return await this._requestConfig('PUT', url, options);
}
/**
* Perform HTTP request.
*/
async request(config) {
const filtered = await this._filterConfig(config);
await this.hooks.runHook('request', this, filtered);
if (typeof filtered.body === 'string')
filtered.body = Buffer.from(filtered.body);
if (filtered.body instanceof Buffer)
filtered.headers['Content-Length'] = Buffer.byteLength(filtered.body);
const transport = filtered.url.protocol === 'https:' ? this.httpsTransport : this.httpTransport;
let res = await transport.request(filtered);
if (this.cookieJar !== null)
await this.cookieJar.storeCookies(filtered.url, res.headers.getAll('Set-Cookie'));
if (this.maxRedirects > 0)
res = await this._handleRedirect(config, res);
return res;
}
/**
* Open WebSocket connection.
*/
async websocket(url, options = {}) {
options.url = url;
const filtered = await this._filterSharedConfig(options);
await this.hooks.runHook('websocket', this, filtered);
filtered.url.protocol = filtered.url.protocol === 'https:' ? 'wss:' : 'ws:';
return await this.wsTransport.connect(filtered);
}
async _filterConfig(config) {
const filtered = await this._filterSharedConfig(config);
// Body
if (filtered.json !== undefined) {
if (filtered.headers['Content-Type'] === undefined)
filtered.headers['Content-Type'] = 'application/json';
filtered.body = JSON.stringify(filtered.json);
}
else if (filtered.yaml !== undefined) {
if (filtered.headers['Content-Type'] === undefined)
filtered.headers['Content-Type'] = 'text/yaml';
filtered.body = yaml.dump(filtered.yaml);
}
else if (filtered.form !== undefined) {
if (filtered.headers['Content-Type'] === undefined) {
filtered.headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
filtered.body = new URLSearchParams(filtered.form).toString();
}
else if (filtered.formData !== undefined) {
const form = this._formData(filtered.formData);
Object.assign(filtered.headers, form.getHeaders());
filtered.body = form;
}
return filtered;
}
async _filterSharedConfig(config) {
if (!(config.url instanceof URL))
config.url = new URL(config.url ?? '/', this.baseURL);
// Auth
const url = config.url;
if ((url.username !== '' || url.password !== '') && config.auth === undefined) {
config.auth = decodeURIComponent(`${url.username}:${url.password}`);
}
// Query
if (config.query !== undefined) {
const params = url.searchParams;
for (const [name, value] of Object.entries(config.query)) {
params.append(name, value);
}
}
// Headers
if (config.headers === undefined)
config.headers = {};
if (this.name !== undefined)
config.headers['User-Agent'] = this.name;
if (config.headers['Accept-Encoding'] === undefined)
config.headers['Accept-Encoding'] = 'gzip';
if (this.cookieJar !== null) {
const cookies = await this.cookieJar.loadCookies(config.url);
if (cookies !== null)
config.headers.Cookie = cookies;
}
return config;
}
_formData(values = {}) {
const form = new FormData();
for (const [name, value] of Object.entries(values)) {
if (typeof value === 'string') {
form.append(name, value);
}
else if (typeof value === 'object' && value !== null) {
form.append(name, value.content, { filename: value.filename, contentType: value.type });
}
}
return form;
}
async _handleRedirect(config, res) {
const redirected = config.redirected ?? 0;
if (redirected >= this.maxRedirects)
return res;
const location = res.get('Location');
if (location === null)
return res;
const url = new URL(location, config.url);
// New followup request
const remove = ['Authorization', 'Cookie', 'Host', 'Referer'];
if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 303) {
const newConfig = {
headers: config.headers,
insecure: config.insecure,
method: res.statusCode === 303 || config.method === 'POST' ? 'GET' : config.method,
redirected: redirected + 1,
url
};
remove.push(...Object.keys(newConfig.headers).filter(name => name.toLowerCase().startsWith('content-')));
remove.forEach(name => delete newConfig.headers[name]);
return this.request(newConfig);
// Same request again
}
else if (res.statusCode === 307 || res.statusCode === 308) {
config.url = url;
config.redirected = redirected + 1;
remove.forEach(name => delete config.headers[name]);
return this.request(config);
}
return res;
}
async _requestConfig(method, url = '/', options) {
return await this.request({ url, method, ...options });
}
}
export { UserAgent };
//# sourceMappingURL=user-agent.js.map