Add GNOME Shell extension showing sing-box status via the Clash API
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
// 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();
|
||||
this._session.timeout = 5;
|
||||
this.setEndpoint(url, secret);
|
||||
}
|
||||
|
||||
setEndpoint(url, secret) {
|
||||
this._baseUrl = String(url || '').replace(/\/+$/, '');
|
||||
this._secret = secret || '';
|
||||
}
|
||||
|
||||
_message(method, path, body) {
|
||||
const msg = Soup.Message.new(method, `${this._baseUrl}${path}`);
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user