104 lines
3.8 KiB
JavaScript
104 lines
3.8 KiB
JavaScript
// Minimal async client for the sing-box Clash API, built on libsoup 3.
|
|
//
|
|
// Endpoints used:
|
|
// GET /version liveness + version string
|
|
// GET /connections snapshot: totals + active connections
|
|
// GET /proxies all proxies and selector groups
|
|
// PUT /proxies/{group} {name} switch a Selector group's active node
|
|
// GET /proxies/{name}/delay?url=... measure latency of a node
|
|
|
|
import Soup from 'gi://Soup';
|
|
import GLib from 'gi://GLib';
|
|
|
|
const textDecoder = new TextDecoder();
|
|
const textEncoder = new TextEncoder();
|
|
|
|
export class ClashApi {
|
|
constructor({ url = 'http://127.0.0.1:9090', secret = '' } = {}) {
|
|
this._session = new Soup.Session();
|
|
// Above the 5 s /delay probe budget so libsoup's inactivity timeout
|
|
// doesn't abort a slow-but-alive latency measurement client-side.
|
|
this._session.timeout = 10;
|
|
this.setEndpoint(url, secret);
|
|
}
|
|
|
|
setEndpoint(url, secret) {
|
|
this._baseUrl = String(url || '').replace(/\/+$/, '');
|
|
this._secret = secret || '';
|
|
}
|
|
|
|
// Abort all in-flight requests and drop idle keep-alive connections.
|
|
destroy() {
|
|
this._session.abort();
|
|
}
|
|
|
|
_message(method, path, body) {
|
|
const uri = `${this._baseUrl}${path}`;
|
|
const msg = Soup.Message.new(method, uri);
|
|
if (!msg)
|
|
throw new Error(`Invalid API URL: ${uri}`);
|
|
const headers = msg.get_request_headers();
|
|
headers.append('Accept', 'application/json');
|
|
if (this._secret)
|
|
headers.append('Authorization', `Bearer ${this._secret}`);
|
|
if (body !== undefined) {
|
|
const bytes = new GLib.Bytes(textEncoder.encode(JSON.stringify(body)));
|
|
msg.set_request_body_from_bytes('application/json', bytes);
|
|
}
|
|
return msg;
|
|
}
|
|
|
|
_request(method, path, body, cancellable) {
|
|
return new Promise((resolve, reject) => {
|
|
const msg = this._message(method, path, body);
|
|
this._session.send_and_read_async(
|
|
msg,
|
|
GLib.PRIORITY_DEFAULT,
|
|
cancellable ?? null,
|
|
(session, res) => {
|
|
let bytes;
|
|
try {
|
|
bytes = session.send_and_read_finish(res);
|
|
} catch (e) {
|
|
reject(e);
|
|
return;
|
|
}
|
|
const status = msg.get_status();
|
|
if (status < 200 || status >= 300) {
|
|
reject(new Error(`HTTP ${status} ${msg.get_reason_phrase() ?? ''} (${path})`.trim()));
|
|
return;
|
|
}
|
|
try {
|
|
const data = bytes?.get_data();
|
|
const text = data && data.length ? textDecoder.decode(data) : '';
|
|
resolve(text ? JSON.parse(text) : null);
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
getVersion(cancellable) {
|
|
return this._request('GET', '/version', undefined, cancellable);
|
|
}
|
|
|
|
getConnections(cancellable) {
|
|
return this._request('GET', '/connections', undefined, cancellable);
|
|
}
|
|
|
|
getProxies(cancellable) {
|
|
return this._request('GET', '/proxies', undefined, cancellable);
|
|
}
|
|
|
|
selectProxy(group, name, cancellable) {
|
|
return this._request('PUT', `/proxies/${encodeURIComponent(group)}`, { name }, cancellable);
|
|
}
|
|
|
|
getDelay(name, { url = 'https://www.gstatic.com/generate_204', timeout = 3000 } = {}, cancellable) {
|
|
const query = `url=${encodeURIComponent(url)}&timeout=${timeout}`;
|
|
return this._request('GET', `/proxies/${encodeURIComponent(name)}/delay?${query}`, undefined, cancellable);
|
|
}
|
|
}
|