The API for the client was designed with all use-cases in mind. You should be able to import the library into your code without changing how it's bundled/the module type.
In order to accomplish this, we provide two versions of the client:
- ESM - ECMAScript modules,
import
syntax - CommonJS - CommonJS modules,
require()
syntax
We encourage the use of modern JavaScript and recommend using ESM if you're starting a new project/codebase. But you don't have to.
Deno is a potential environment for the Bare client. It supports fetch()
and import
ing web URLs
But usually, this is done in browsers
<script type="module">
// type="module" indicates that this script uses ESM
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#applying_the_module_to_your_html
import { createBareClient } from 'https://unpkg.com/@tomphttp/[email protected]/dist/index.js';
</script>
Bare client has an alternative API that gives you access to all the exports in the bare
variable.
<script src="https://unpkg.com/@tomphttp/[email protected]/dist/bare.cjs"></script>
<script>
bare.BareClient; // class...
bare.createBareClient; // function...
</script>
This can be done in NodeJS v17+ (fetch()
api was added natively) or via a bundler.
Bundlers include: webpack, rollup
Install with NPM:
npm i @tomphttp/bare-client
import { BareClient, createBareClient } from '@tomphttp/bare-client';
const { BareClient, createBareClient } = require('@tomphttp/bare-client');
This concept is fundamental to understanding what API to use for creating an instance of BareClient.
Bare servers have a manifest that expose JSON data that help the client determine what versions of the Bare server are supported.
Here's an example:
https://uv.holyubofficial.net/
{
"versions": ["v1", "v2", "v3"],
"language": "NodeJS",
"memoryUsage": 22.84,
"project": {
"name": "bare-server-node",
"description": "TOMPHTTP NodeJS Bare Server",
"repository": "https://github.com/tomphttp/bare-server-node",
"version": "2.0.0-beta"
}
}
This will be fetched one way or another by the client. The link to the manifest is used as a base URL for the client.
It's expected that APIs such as ./v2/
relative to the manifest URL.
Now that you have access to BareClient
and createBareClient
, you have several ways to utilize the library.
One of the APIs for new BareClient()
:
class BareClient {
// ...
/**
* Lazily create a BareClient. Calls to fetch and connect will request the manifest once on-demand.
* @param server A full URL to the bare server.
* @param signal An abort signal for fetching the manifest on demand.
*/
// ...
}
Usage:
const client = new BareClient("https://uv.holyubofficial.net/"); // string
const client = new BareClient(new URL("https://uv.holyubofficial.net/")); // accepts instances of `URL`
If you have your Bare server on http://example.com/bare/, and the page is on example.com, you might be tempted to do:
const client = new BareClient('/bare/');
THIS WILL NOT WORK!
Uncaught TypeError: URL constructor: /bare/ is not a valid URL.
This is because you need to pass a full URL. Here's a few examples:
// Similar to passing `/bare/`, except it's being resolved using the `location` and `URL` api:
const client = new BareClient(new URL("/bare/", location.toString()));
// A more compact solution that's slightly less recommended:
// location.origin will always be http://example.com
// There's no `/` at the end of the origin so this will work.
const client = new BareClient(`${location.origin}/bare/`);
If you don't pass a manifest to new BareClient
, BareClient
will enter on-demand mode.
on-demand mode is as such:
The client won't know what version of the Bare server is supported until the manifest is fetched. The manifest won't be fetched until client.fetch
is called. Because client.fetch
is asynchronous, the API doesn't change when on-demand mode is enabled. If the manifest fails to be fetch, an error will be thrown in client.fetch
. If an error was thrown, the client will try to fetch the manifest the next time client.fetch
is called. client.createWebSocket
won't work unless the manifest has been fetched.
client.createWebSocket
not working unless client.fetch
was called should be a huge deal depending on your use-case. If you don't expect to be creating a WebSocket from that environment (you're probably in a Service Worker), on-demand mode is perfect. If it's still a big deal, see Synchronously with cache and Asynchronously
Other API for new BareClient()
:
class BareClient {
// ...
/**
* Immediately create a BareClient.
* @param server A full URL to the bare server.
* @param manfiest A Bare server manifest.
*/
constructor(server: string | URL, manfiest?: BareManifest);
// ...
}
You can re-use the Bare server manifest from one instance to another.
For example, this code won't work due to the Bare client being in on-demand mode:
const client = new BareClient('https://uv.holyubofficial.net');
// Uncaught TypeError: You need to wait for the client to finish fetching the manifest before creating any WebSockets. Try caching the manifest data before making this request.
while (true) client.createWebSocket('wss://www.google.com/');
However, this code will:
// takes some time to fetch the manifest
const firstClient = await createBareServer('https://uv.holyubofficial.net');
// instant
const secondClient = new BareClient(
'https://uv.holyubofficial.net',
firstClient.manifest
);
// you're free to create all the WebSockets you want...
while (true) secondClient.createWebSocket('wss://www.google.com/');
The manifest is serializable and can be passed from a Service Worker to a client (this is a use-case for TompHTTP Service Worker proxies).
const secondClient = new BareClient(
'https://uv.holyubofficial.net',
JSON.parse(JSON.stringify(firstClient.manifest))
);
This will fetch the manifest before making the BareClient available to you. The returned client is guaranteed to work with .createWebSocket
.
// takes some time to fetch the manifest
const client = await createBareServer('https://uv.holyubofficial.net');
// but it's worth it
// now you're free to create all the WebSockets you want...
while (true) client.createWebSocket('wss://www.google.com/');
The client exposes a fetch-like API:
class BareClient {
// ...
async fetch(
url: string | URL,
init?: RequestInit
): Promise<BareResponseFetch>;
// ...
}
It does NOT accept instances of Request
due to too many weird technicalities (restricted headers/headers being lost, duplex not being accessible, body always being a ReadableStream
)
API used in this example: https://platform.openai.com/docs/api-reference/completions/create
// const client = ...
async function openaiCompletions(OPENAI_API_KEY, prompt) {
const res = await client.fetch('https://api.openai.com/v1/completions', {
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: 'text-davinci-003',
prompt,
max_tokens: 7,
temperature: 0,
}),
});
return res.json();
}
const res = await openaiCompletions(
'my-api-key-asjkdioasopd',
'Say this is a test'
);
console.log(res); // {id: ...,choices: [{...}]}
const res = await client.fetch('https://api.github.com/orgs/tomphttp', {
headers: new Headers({
'user-agent': navigator.userAgent,
}),
});
// We pass (url: Request, init: RequestInit)
console.log(res.status); // 200!
console.log(await res.json()); // {login: 'tomphttp', id: 98234273, ... }
The client exposes a WebSocket-like API:
class BareClient {
// ...
createWebSocket(
remote: string | URL,
protocols?: BareWebSocket.Options | string | string[] | undefined,
options?: BareWebSocket.Options
): WebSocket;
// ...
}
Options:
interface Options {
/**
* A provider of request headers to pass to the remote.
* Usually one of `User-Agent`, `Origin`, and `Cookie`
* Can be just the headers object or an synchronous/asynchronous function that returns the headers object
*/
headers?: BareWebSocket.HeadersProvider;
/**
* A hook executed by this function with helper arguments for hooking the readyState property. If a hook isn't provided, bare-client will hook the property on the instance. Hooking it on an instance basis is good for small projects, but ideally the class should be hooked by the user of bare-client.
*/
readyStateHook?:
| ((
socket: WebSocket,
getReadyState: BareWebSocket.GetReadyStateCallback
) => void)
| undefined;
/**
* A hook executed by this function with helper arguments for determining if the send function should throw an error. If a hook isn't provided, bare-client will hook the function on the instance.
*/
sendErrorHook?:
| ((
socket: WebSocket,
getSendError: BareWebSocket.GetSendErrorCallback
) => void)
| undefined;
/**
* A hook executed by this function with the URL. If a hook isn't provided, bare-client will hook the URL.
*/
urlHook?: ((socket: WebSocket, url: URL) => void) | undefined;
/**
* A hook executed by this function with a helper for getting the current fake protocol. If a hook isn't provided, bare-client will hook the protocol.
*/
protocolHook?:
| ((
socket: WebSocket,
getProtocol: BareWebSocket.GetProtocolCallback
) => void)
| undefined;
/**
* A callback executed by this function with an array of cookies. This is called once the metadata from the server is received.
*/
setCookiesCallback?: ((setCookies: string[]) => void) | undefined;
webSocketImpl?: WebSocketImpl;
}
Example:
Postman hosts a WebSocket echo API. Anything you send to the socket will be sent directly back to you. This is an easy way to test WebSockets.
See https://blog.postman.com/introducing-postman-websocket-echo-service/
const socket = client.createWebSocket('wss://ws.postman-echo.com/raw');
console.log('URL:', socket.url); // wss://ws.postman-echo.com/raw
socket.addEventListener('open', () => {
console.log('Open!');
console.log('Saying hi to Postman');
socket.send('Hello, Postman!');
});
socket.addEventListener('message', (event) => {
console.log('Postman says:', event.data); // Postman says: Hello, Postman!
});
Passing a protocol:
client.createWebSocket('wss://ws.postman-echo.com/raw', 'test-protocol');
Passing headers:
Usually this is done to pass Origin
, User-Agent
, and Cookie
undefined
is passed for the protocol to specify no protocol. we just want to specify options
client.createWebSocket('wss://ws.postman-echo.com/raw', undefined, {
headers: {
'user-agent': navigator.userAgent, // pass the browser user-agent
},
});
Bare client exposes every hook possible for WebSockets. By default, the Bare client will hook the WebSocket itself which should work with most websites, however they can easily be bypassed if the WebSocket.prototype
methods are called on the instance of WebSocket. Bare client will never modify the prototypes of objects, it's up to the implementation to do that.
The purpose of this hook is to check if the WebSocket should throw an error according to its fake readyState
. readyState
on the WebSocket is spoofed due to the open
event being deferred until the Bare server connects to the destination server.
This is the default hook done by Bare client. No prototype hooks here:
socket.send = function (...args) {
const error = getSendError();
if (error) throw error;
else WebSocketFields.prototype.send.call(this, ...args);
};
This is what a more proper implementation would look like:
/**
* A map containing the send hooks of hooked WebSockets
* @type {WeakMap<WebSocket, import('@tomphttp/bare-client').BareWebSocket.GetSendErrorCallback}
*/
const getSendErrorMap = new WeakMap();
// save the unhooked function for later
const { send } = WebSocket.prototype;
WebSocket.prototype.send = function (...args) {
const getSendError = getSendErrorMap.get(this);
if (getSendError) {
const error = getSendError();
if (error) throw error;
}
return Reflect.apply(target, that, args);
};
client.createWebSocket('wss://ws.postman-echo.com/raw', undefined, {
headers: {
'user-agent': navigator.userAgent, // pass the browser user-agent
sendErrorHook: (socket, getSendError) => {
getSendErrorMap.set(socket, getSendError);
},
},
});
This is a basic implementation and it has some pitfalls like:
-
The
toString()
value is leaked: Unhooked:WebSocket.prototype.send.toString(); // function send() { [native code] }
Hooked:
WebSocket.prototype.send.toString(); // function (...args) { /* ... */ }
This can be fixed by hooking
Function.prototype.toString
. -
send
contains aprototype
Unhooked:'prototype' in WebSocket.prototype.send; // false
Hooked:
WebSocket.prototype.send.toString(); // true
This can be fixed by using shorthand methods.
Whether your hooks get detected is more of a proxy script issue than a Bare client issue. You've been given every opportunity to hide the hook.