mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-11 19:05:11 +08:00
964 lines
21 KiB
Ucode
Executable File
964 lines
21 KiB
Ucode
Executable File
#!/usr/bin/ucode
|
|
/*
|
|
* SPDX-License-Identifier: GPL-2.0-only
|
|
*
|
|
* Clash API display proxy for HomeProxy dashboards.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
import * as socket from 'socket';
|
|
import * as uloop from 'uloop';
|
|
import { cursor } from 'uci';
|
|
import { urldecode, urlencode } from 'luci.http';
|
|
import { parse_controller } from '/etc/homeproxy/scripts/clash_api.uc';
|
|
|
|
const uci = cursor();
|
|
const uciconfig = 'homeproxy';
|
|
const ucimain = 'config';
|
|
const shadowtls_suffix = '-out-shadowtls';
|
|
const filter_timeout = 10000;
|
|
const fallback_delay_limit = 5;
|
|
const upstream_read_timeout = 5; // 秒;单次上游读取超时
|
|
|
|
// Resource limits
|
|
const MAX_CONNECTIONS = 64;
|
|
const MAX_REQUEST_BUFFER_SIZE = 256 * 1024; // 256KB
|
|
const MAX_RESPONSE_BUFFER_SIZE = 8 * 1024 * 1024; // 8MB
|
|
const CLIENT_IDLE_TIMEOUT = 30000; // 30 seconds
|
|
|
|
let next_id = 1;
|
|
let connections = {};
|
|
let target, listen, server;
|
|
|
|
uci.load(uciconfig);
|
|
|
|
function isEmpty(value) {
|
|
return !value || value === 'nil' || (type(value) in ['array', 'object'] && length(value) === 0);
|
|
}
|
|
|
|
function toArray(value) {
|
|
if (type(value) === 'array')
|
|
return value;
|
|
else if (isEmpty(value))
|
|
return [];
|
|
|
|
return [ value ];
|
|
}
|
|
|
|
function deriveProxyController(target) {
|
|
target = parse_controller(target || '127.0.0.1:9090', 9090);
|
|
|
|
if (isEmpty(target.host))
|
|
target.host = '127.0.0.1';
|
|
|
|
return { host: target.host, port: 9091 };
|
|
}
|
|
|
|
function sendAll(sock, data) {
|
|
let offset = 0;
|
|
|
|
while (offset < length(data)) {
|
|
let sent = sock.send(substr(data, offset));
|
|
|
|
if (sent === null || sent <= 0)
|
|
return false;
|
|
|
|
offset += sent;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function recvAvailable(sock) {
|
|
let data = '';
|
|
let eof = false;
|
|
|
|
for (let i = 0; i < 32; i++) {
|
|
let chunk = sock.recv(16384);
|
|
|
|
if (chunk === null)
|
|
break;
|
|
|
|
if (length(chunk) === 0) {
|
|
eof = true;
|
|
break;
|
|
}
|
|
|
|
data += chunk;
|
|
|
|
if (length(chunk) < 16384)
|
|
break;
|
|
}
|
|
|
|
return { data, eof };
|
|
}
|
|
|
|
function contentLength(headers) {
|
|
for (let line in headers) {
|
|
let matched = match(lc(line), /^content-length:[ \t]*([0-9]+)/);
|
|
|
|
if (matched)
|
|
return int(matched[1]);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function headerValue(headers, name) {
|
|
name = lc(name);
|
|
|
|
for (let line in headers) {
|
|
let matched = match(line, /^([^:]+):(.*)$/);
|
|
|
|
if (matched && lc(trim(matched[1])) === name)
|
|
return trim(matched[2]);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function pathSegment(value) {
|
|
return replace(urlencode(value), /\+/g, '%20');
|
|
}
|
|
|
|
function parseHttpMessage(raw, body_to_eof) {
|
|
let header_end = index(raw, "\r\n\r\n");
|
|
|
|
if (header_end < 0)
|
|
return null;
|
|
|
|
let header_text = substr(raw, 0, header_end);
|
|
let headers = split(header_text, "\r\n");
|
|
let body_start = header_end + 4;
|
|
let body_len = contentLength(headers);
|
|
|
|
if (body_len === null)
|
|
body_len = body_to_eof ? length(raw) - body_start : 0;
|
|
|
|
let complete_len = body_start + body_len;
|
|
|
|
if (length(raw) < complete_len)
|
|
return null;
|
|
|
|
let request_line = split(headers[0] || '', /[ \t]+/);
|
|
|
|
return {
|
|
raw: substr(raw, 0, complete_len),
|
|
headers,
|
|
body: substr(raw, body_start, body_len),
|
|
complete_len,
|
|
method: request_line[0] || '',
|
|
path: request_line[1] || '/'
|
|
};
|
|
}
|
|
|
|
function shouldFilter(method, path) {
|
|
return method === 'GET' && (
|
|
path === '/proxies' ||
|
|
index(path, '/proxies?') === 0 ||
|
|
index(path, '/proxies/') === 0 ||
|
|
path === '/providers/proxies' ||
|
|
index(path, '/providers/proxies?') === 0 ||
|
|
index(path, '/providers/proxies/') === 0
|
|
);
|
|
}
|
|
|
|
function isUpgradeRequest(request) {
|
|
return !!headerValue(request.headers, 'Upgrade') ||
|
|
index(lc(headerValue(request.headers, 'Connection') || ''), 'upgrade') >= 0;
|
|
}
|
|
|
|
function rebuildCloseRequest(request) {
|
|
let headers = [];
|
|
|
|
for (let i = 0; i < length(request.headers); i++) {
|
|
let line = request.headers[i],
|
|
header = lc(line);
|
|
|
|
if (i > 0 && match(header, /^(accept-encoding|connection):/))
|
|
continue;
|
|
|
|
push(headers, line);
|
|
}
|
|
|
|
push(headers, 'Accept-Encoding: identity');
|
|
push(headers, 'Connection: close');
|
|
|
|
return join("\r\n", headers) + "\r\n\r\n" + request.body;
|
|
}
|
|
|
|
function isShadowTlsTag(name) {
|
|
return type(name) === 'string' &&
|
|
length(name) > length(shadowtls_suffix) &&
|
|
substr(name, length(name) - length(shadowtls_suffix)) === shadowtls_suffix;
|
|
}
|
|
|
|
function isHiddenProxyTag(name) {
|
|
return isShadowTlsTag(name);
|
|
}
|
|
|
|
function filterProxyItem(item) {
|
|
if (type(item) !== 'object')
|
|
return;
|
|
|
|
if (type(item.all) === 'array')
|
|
item.all = filter(item.all, (name) => !isHiddenProxyTag(name));
|
|
}
|
|
|
|
function filterProxyArray(items) {
|
|
return filter(items, (item) => {
|
|
if (type(item) === 'string')
|
|
return !isHiddenProxyTag(item);
|
|
|
|
if (type(item) === 'object' && isHiddenProxyTag(item.name))
|
|
return false;
|
|
|
|
filterProxyItem(item);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function filterProxiesPayload(payload) {
|
|
if (type(payload) !== 'object')
|
|
return payload;
|
|
|
|
filterProxyItem(payload);
|
|
|
|
if (type(payload.proxies) === 'object') {
|
|
for (let name in payload.proxies) {
|
|
if (isHiddenProxyTag(name)) {
|
|
delete payload.proxies[name];
|
|
continue;
|
|
}
|
|
|
|
filterProxyItem(payload.proxies[name]);
|
|
}
|
|
}
|
|
|
|
if (type(payload.providers) === 'object') {
|
|
for (let name in payload.providers) {
|
|
let provider = payload.providers[name];
|
|
|
|
if (type(provider) !== 'object')
|
|
continue;
|
|
|
|
if (type(provider.proxies) === 'array')
|
|
provider.proxies = filterProxyArray(provider.proxies);
|
|
}
|
|
}
|
|
|
|
return payload;
|
|
}
|
|
|
|
function parseHex(value) {
|
|
let result = 0;
|
|
value = lc(trim(split(value, ';', 2)[0] || ''));
|
|
|
|
for (let i = 0; i < length(value); i++) {
|
|
let digit = index('0123456789abcdef', substr(value, i, 1));
|
|
|
|
if (digit < 0)
|
|
return null;
|
|
|
|
result = result * 16 + digit;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function decodeChunkedBody(body) {
|
|
let offset = 0;
|
|
let decoded = '';
|
|
|
|
while (offset < length(body)) {
|
|
let line_end = index(substr(body, offset), "\r\n");
|
|
|
|
if (line_end < 0)
|
|
return null;
|
|
|
|
let size = parseHex(substr(body, offset, line_end));
|
|
if (size === null)
|
|
return null;
|
|
|
|
offset += line_end + 2;
|
|
|
|
if (size === 0)
|
|
return decoded;
|
|
|
|
if (length(body) < offset + size + 2)
|
|
return null;
|
|
|
|
decoded += substr(body, offset, size);
|
|
offset += size + 2;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function rebuildResponse(response, body) {
|
|
let headers = [];
|
|
|
|
for (let i = 0; i < length(response.headers); i++) {
|
|
let line = response.headers[i];
|
|
|
|
if (i > 0 && match(lc(line), /^(content-length|transfer-encoding):/))
|
|
continue;
|
|
|
|
push(headers, line);
|
|
}
|
|
|
|
push(headers, 'Content-Length: ' + length(body));
|
|
|
|
return join("\r\n", headers) + "\r\n\r\n" + body;
|
|
}
|
|
|
|
function filterResponse(raw) {
|
|
let response = parseHttpMessage(raw, true);
|
|
|
|
if (response === null)
|
|
return raw;
|
|
|
|
let body = response.body;
|
|
if (lc(headerValue(response.headers, 'transfer-encoding') || '') === 'chunked') {
|
|
body = decodeChunkedBody(substr(raw, response.complete_len - length(response.body)));
|
|
if (body === null)
|
|
return raw;
|
|
}
|
|
|
|
let payload;
|
|
|
|
try {
|
|
payload = json(body);
|
|
} catch (e) {
|
|
return raw;
|
|
}
|
|
|
|
if (type(payload) !== 'object')
|
|
return raw;
|
|
|
|
payload = filterProxiesPayload(payload);
|
|
|
|
return rebuildResponse(response, sprintf('%J', payload));
|
|
}
|
|
|
|
function responseBody(response) {
|
|
if (response === null)
|
|
return null;
|
|
|
|
let body = response.body;
|
|
|
|
if (lc(headerValue(response.headers, 'transfer-encoding') || '') === 'chunked') {
|
|
body = decodeChunkedBody(response.body);
|
|
if (body === null)
|
|
return null;
|
|
}
|
|
|
|
return body;
|
|
}
|
|
|
|
function parseJsonBody(raw) {
|
|
if (raw === null)
|
|
return null;
|
|
|
|
let response = parseHttpMessage(raw, true),
|
|
body = responseBody(response);
|
|
|
|
if (body === null)
|
|
return null;
|
|
|
|
try {
|
|
return json(body);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function responseStatus(raw) {
|
|
let response = parseHttpMessage(raw, true);
|
|
|
|
if (response === null)
|
|
return 0;
|
|
|
|
let matched = match(response.headers[0] || '', /^HTTP\/[0-9.]+[ \t]+([0-9]+)/);
|
|
|
|
return matched ? int(matched[1]) : 0;
|
|
}
|
|
|
|
function responseComplete(raw) {
|
|
let header_end = index(raw, "\r\n\r\n");
|
|
if (header_end < 0)
|
|
return false;
|
|
|
|
let headers = split(substr(raw, 0, header_end), "\r\n");
|
|
if (lc(headerValue(headers, 'transfer-encoding') || '') === 'chunked')
|
|
return decodeChunkedBody(substr(raw, header_end + 4)) !== null;
|
|
|
|
if (contentLength(headers) === null)
|
|
return false;
|
|
|
|
return parseHttpMessage(raw) !== null;
|
|
}
|
|
|
|
function closeConnection(conn) {
|
|
if (!conn)
|
|
return;
|
|
|
|
if (conn.client_handle) {
|
|
conn.client_handle.delete();
|
|
conn.client_handle = null;
|
|
}
|
|
|
|
if (conn.upstream_handle) {
|
|
conn.upstream_handle.delete();
|
|
conn.upstream_handle = null;
|
|
}
|
|
|
|
if (conn.timer) {
|
|
conn.timer.cancel();
|
|
conn.timer = null;
|
|
}
|
|
|
|
if (conn.idle_timer) {
|
|
conn.idle_timer.cancel();
|
|
conn.idle_timer = null;
|
|
}
|
|
|
|
if (conn.client) {
|
|
conn.client.close();
|
|
conn.client = null;
|
|
}
|
|
|
|
if (conn.upstream) {
|
|
conn.upstream.close();
|
|
conn.upstream = null;
|
|
}
|
|
|
|
delete connections[conn.id];
|
|
}
|
|
|
|
function isEmptyObject(value) {
|
|
return type(value) === 'object' && length(value) === 0;
|
|
}
|
|
|
|
function buildJsonResponse(request, payload) {
|
|
let body = sprintf('%J', payload),
|
|
headers = [
|
|
'HTTP/1.1 200 OK',
|
|
'Content-Type: application/json',
|
|
'Connection: close'
|
|
],
|
|
origin = headerValue(request.headers, 'Origin');
|
|
|
|
if (!isEmpty(origin)) {
|
|
let allowed_origins = toArray(uci.get(uciconfig, ucimain, 'clash_api_allow_origin'));
|
|
|
|
if (index(allowed_origins, origin) >= 0) {
|
|
push(headers, 'Access-Control-Allow-Origin: ' + origin);
|
|
push(headers, 'Vary: Origin');
|
|
}
|
|
}
|
|
|
|
push(headers, 'Content-Length: ' + length(body));
|
|
|
|
return join("\r\n", headers) + "\r\n\r\n" + body;
|
|
}
|
|
|
|
function parseGroupDelayPath(path) {
|
|
let parts = split(path, '?', 2),
|
|
matched = match(parts[0] || '', /^\/group\/(.+)\/delay$/);
|
|
|
|
if (!matched)
|
|
return null;
|
|
|
|
return {
|
|
name: urldecode(matched[1]),
|
|
query: (length(parts) > 1) ? ('?' + parts[1]) : ''
|
|
};
|
|
}
|
|
|
|
function upstreamHeader(request, name) {
|
|
let value = headerValue(request.headers, name);
|
|
return isEmpty(value) ? null : value;
|
|
}
|
|
|
|
function upstreamRequest(method, path, request, body) {
|
|
body = body || '';
|
|
|
|
let headers = [
|
|
sprintf('%s %s HTTP/1.1', method, path),
|
|
sprintf('Host: %s:%d', target.host, target.port),
|
|
'Accept-Encoding: identity',
|
|
'Connection: close'
|
|
],
|
|
authorization = upstreamHeader(request, 'Authorization'),
|
|
origin = upstreamHeader(request, 'Origin');
|
|
|
|
if (authorization)
|
|
push(headers, 'Authorization: ' + authorization);
|
|
|
|
if (origin)
|
|
push(headers, 'Origin: ' + origin);
|
|
|
|
if (length(body)) {
|
|
let content_type = upstreamHeader(request, 'Content-Type');
|
|
|
|
if (content_type)
|
|
push(headers, 'Content-Type: ' + content_type);
|
|
|
|
push(headers, 'Content-Length: ' + length(body));
|
|
}
|
|
|
|
return join("\r\n", headers) + "\r\n\r\n" + body;
|
|
}
|
|
|
|
function fetchUpstreamAsync(method, path, request, body, done) {
|
|
let upstream = socket.connect(target.host, target.port, null, 3000);
|
|
|
|
if (upstream === null) {
|
|
done(null);
|
|
return null;
|
|
}
|
|
|
|
let fetch = {
|
|
upstream: upstream,
|
|
handle: null,
|
|
timer: null,
|
|
raw: '',
|
|
finished: false
|
|
};
|
|
|
|
function finish(raw) {
|
|
if (fetch.finished)
|
|
return;
|
|
|
|
fetch.finished = true;
|
|
|
|
if (fetch.handle) {
|
|
fetch.handle.delete();
|
|
fetch.handle = null;
|
|
}
|
|
|
|
if (fetch.timer) {
|
|
fetch.timer.cancel();
|
|
fetch.timer = null;
|
|
}
|
|
|
|
if (fetch.upstream) {
|
|
fetch.upstream.close();
|
|
fetch.upstream = null;
|
|
}
|
|
|
|
done(raw);
|
|
}
|
|
|
|
if (!sendAll(upstream, upstreamRequest(method, path, request, body))) {
|
|
finish(null);
|
|
return fetch;
|
|
}
|
|
|
|
fetch.timer = uloop.timer(upstream_read_timeout * 1000, () => finish(null));
|
|
fetch.handle = uloop.handle(upstream, (events, eof, error) => {
|
|
if (events & uloop.ULOOP_READ) {
|
|
let received = recvAvailable(upstream);
|
|
|
|
if (length(received.data)) {
|
|
if (length(fetch.raw) + length(received.data) > MAX_RESPONSE_BUFFER_SIZE) {
|
|
warn(sprintf('homeproxy clash api proxy: async fetch response exceeded %d bytes\n', MAX_RESPONSE_BUFFER_SIZE));
|
|
finish(null);
|
|
return;
|
|
}
|
|
|
|
fetch.raw += received.data;
|
|
}
|
|
|
|
if (responseComplete(fetch.raw) || received.eof) {
|
|
finish(length(fetch.raw) ? fetch.raw : null);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (eof || error)
|
|
finish(length(fetch.raw) ? fetch.raw : null);
|
|
}, uloop.ULOOP_READ);
|
|
|
|
return fetch;
|
|
}
|
|
|
|
function fetchVisibleProxyGroupAsync(group_name, request, done) {
|
|
fetchUpstreamAsync('GET', '/proxies', request, null, (raw) => {
|
|
if (raw === null) {
|
|
done(null);
|
|
return;
|
|
}
|
|
|
|
let payload = filterProxiesPayload(parseJsonBody(raw));
|
|
if (type(payload) !== 'object' || type(payload.proxies) !== 'object') {
|
|
done(null);
|
|
return;
|
|
}
|
|
|
|
let group = payload.proxies[group_name];
|
|
done((type(group) === 'object') ? group : null);
|
|
});
|
|
}
|
|
|
|
function testProxyDelayAsync(proxy_name, query, request, done) {
|
|
fetchUpstreamAsync('GET',
|
|
'/proxies/' + pathSegment(proxy_name) + '/delay' + query,
|
|
request,
|
|
null,
|
|
(raw) => {
|
|
if (raw === null || responseStatus(raw) < 200 || responseStatus(raw) >= 300) {
|
|
done(0);
|
|
return;
|
|
}
|
|
|
|
let payload = parseJsonBody(raw);
|
|
|
|
if (type(payload) === 'object' && type(payload.delay) === 'double') {
|
|
done(int(payload.delay));
|
|
return;
|
|
}
|
|
|
|
if (type(payload) === 'object' && type(payload.delay) === 'int') {
|
|
done(payload.delay);
|
|
return;
|
|
}
|
|
|
|
done(0);
|
|
});
|
|
}
|
|
|
|
function fallbackGroupDelayAsync(group_info, request, progress, done) {
|
|
fetchVisibleProxyGroupAsync(group_info.name, request, (group) => {
|
|
if (progress)
|
|
progress();
|
|
|
|
if (group === null || type(group.all) !== 'array') {
|
|
done(null);
|
|
return;
|
|
}
|
|
|
|
let results = {},
|
|
candidates = [],
|
|
truncated = false;
|
|
|
|
for (let proxy_name in group.all) {
|
|
if (isShadowTlsTag(proxy_name))
|
|
continue;
|
|
|
|
if (length(candidates) >= fallback_delay_limit) {
|
|
truncated = true;
|
|
break;
|
|
}
|
|
|
|
push(candidates, proxy_name);
|
|
}
|
|
|
|
if (truncated)
|
|
warn(sprintf('homeproxy clash api proxy: fallback delay for group %s limited to first %d visible proxies\n',
|
|
group_info.name, fallback_delay_limit));
|
|
|
|
function next(index) {
|
|
if (index >= length(candidates)) {
|
|
done(results);
|
|
return;
|
|
}
|
|
|
|
let proxy_name = candidates[index];
|
|
testProxyDelayAsync(proxy_name, group_info.query, request, (delay) => {
|
|
results[proxy_name] = delay;
|
|
|
|
if (progress)
|
|
progress();
|
|
|
|
next(index + 1);
|
|
});
|
|
}
|
|
|
|
next(0);
|
|
});
|
|
}
|
|
|
|
function finishGroupDelay(conn, request, group_info, raw) {
|
|
if (!connections[conn.id])
|
|
return;
|
|
|
|
let payload = parseJsonBody(raw),
|
|
status = raw === null ? 0 : responseStatus(raw);
|
|
|
|
if (type(payload) === 'object')
|
|
payload = filterProxiesPayload(payload);
|
|
|
|
if (status >= 200 && status < 300 && !isEmptyObject(payload)) {
|
|
sendAll(conn.client, buildJsonResponse(request, payload));
|
|
closeConnection(conn);
|
|
return;
|
|
}
|
|
|
|
resetTimer(conn);
|
|
fallbackGroupDelayAsync(group_info, request, () => resetTimer(conn), (fallback_payload) => {
|
|
if (!connections[conn.id])
|
|
return;
|
|
|
|
if (fallback_payload !== null) {
|
|
sendAll(conn.client, buildJsonResponse(request, fallback_payload));
|
|
closeConnection(conn);
|
|
return;
|
|
}
|
|
|
|
if (raw !== null)
|
|
sendAll(conn.client, raw);
|
|
|
|
closeConnection(conn);
|
|
});
|
|
}
|
|
|
|
function handleGroupDelay(conn, request, group_info) {
|
|
if (conn.client_handle) {
|
|
conn.client_handle.delete();
|
|
conn.client_handle = null;
|
|
}
|
|
|
|
conn.request_buffer = '';
|
|
conn.timer = uloop.timer(filter_timeout, () => closeConnection(conn));
|
|
fetchUpstreamAsync(request.method, request.path, request, request.body,
|
|
(raw) => finishGroupDelay(conn, request, group_info, raw));
|
|
return true;
|
|
}
|
|
|
|
function resetTimer(conn) {
|
|
if (conn.timer)
|
|
conn.timer.set(filter_timeout);
|
|
}
|
|
|
|
function relayRead(conn, from, to) {
|
|
let received = recvAvailable(from);
|
|
|
|
if (length(received.data) && !sendAll(to, received.data)) {
|
|
closeConnection(conn);
|
|
return;
|
|
}
|
|
|
|
// Reset idle timer on data activity
|
|
if (conn.idle_timer && length(received.data) > 0) {
|
|
conn.idle_timer.set(CLIENT_IDLE_TIMEOUT);
|
|
}
|
|
|
|
if (received.eof)
|
|
closeConnection(conn);
|
|
}
|
|
|
|
function setupRelay(conn) {
|
|
// Set idle timer for relay phase (30s idle timeout)
|
|
conn.idle_timer = uloop.timer(CLIENT_IDLE_TIMEOUT, () => {
|
|
warn(`Connection ${conn.id} idle timeout during relay\n`);
|
|
closeConnection(conn);
|
|
});
|
|
|
|
conn.client_handle = uloop.handle(conn.client, (events, eof, error) => {
|
|
if (events & uloop.ULOOP_READ)
|
|
relayRead(conn, conn.client, conn.upstream);
|
|
|
|
if (eof || error)
|
|
closeConnection(conn);
|
|
}, uloop.ULOOP_READ);
|
|
|
|
conn.upstream_handle = uloop.handle(conn.upstream, (events, eof, error) => {
|
|
if (events & uloop.ULOOP_READ)
|
|
relayRead(conn, conn.upstream, conn.client);
|
|
|
|
if (eof || error)
|
|
closeConnection(conn);
|
|
}, uloop.ULOOP_READ);
|
|
}
|
|
|
|
function finishFilteredResponse(conn) {
|
|
sendAll(conn.client, filterResponse(conn.response_buffer));
|
|
closeConnection(conn);
|
|
}
|
|
|
|
function setupFilteredResponse(conn) {
|
|
conn.response_buffer = '';
|
|
conn.timer = uloop.timer(filter_timeout, () => closeConnection(conn));
|
|
|
|
conn.upstream_handle = uloop.handle(conn.upstream, (events, eof, error) => {
|
|
if (events & uloop.ULOOP_READ) {
|
|
let received = recvAvailable(conn.upstream);
|
|
|
|
if (length(received.data)) {
|
|
// Check response buffer size limit
|
|
if (length(conn.response_buffer) + length(received.data) > MAX_RESPONSE_BUFFER_SIZE) {
|
|
warn(`Connection ${conn.id} response buffer exceeded ${MAX_RESPONSE_BUFFER_SIZE} bytes\n`);
|
|
closeConnection(conn);
|
|
return;
|
|
}
|
|
conn.response_buffer += received.data;
|
|
resetTimer(conn);
|
|
}
|
|
|
|
if (responseComplete(conn.response_buffer) || received.eof) {
|
|
finishFilteredResponse(conn);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (eof || error) {
|
|
if (length(conn.response_buffer))
|
|
finishFilteredResponse(conn);
|
|
else
|
|
closeConnection(conn);
|
|
}
|
|
}, uloop.ULOOP_READ);
|
|
}
|
|
|
|
function startUpstream(conn, request) {
|
|
try {
|
|
let group_delay = parseGroupDelayPath(request.path);
|
|
|
|
if (request.method === 'GET' && group_delay !== null)
|
|
return handleGroupDelay(conn, request, group_delay);
|
|
|
|
if (conn.client_handle) {
|
|
conn.client_handle.delete();
|
|
conn.client_handle = null;
|
|
}
|
|
|
|
conn.filter_read = shouldFilter(request.method, request.path);
|
|
conn.upstream = socket.connect(target.host, target.port, null, 3000);
|
|
|
|
if (conn.upstream === null) {
|
|
warn(sprintf('homeproxy clash api proxy: connect to %s:%d failed: %s\n',
|
|
target.host, target.port, socket.error()));
|
|
closeConnection(conn);
|
|
return;
|
|
}
|
|
|
|
let upstream_request = (conn.filter_read || !isUpgradeRequest(request))
|
|
? rebuildCloseRequest(request)
|
|
: conn.request_buffer;
|
|
if (!sendAll(conn.upstream, upstream_request)) {
|
|
warn('homeproxy clash api proxy: failed to send upstream request\n');
|
|
closeConnection(conn);
|
|
return;
|
|
}
|
|
|
|
conn.request_buffer = '';
|
|
|
|
if (conn.filter_read)
|
|
setupFilteredResponse(conn);
|
|
else
|
|
setupRelay(conn);
|
|
} catch (e) {
|
|
warn(sprintf('homeproxy clash api proxy: start upstream exception: %J\n', e));
|
|
closeConnection(conn);
|
|
}
|
|
}
|
|
|
|
function onClientRequest(conn, events, eof, error) {
|
|
if (events & uloop.ULOOP_READ) {
|
|
let received = recvAvailable(conn.client);
|
|
|
|
if (length(received.data)) {
|
|
// Check request buffer size limit
|
|
if (length(conn.request_buffer) + length(received.data) > MAX_REQUEST_BUFFER_SIZE) {
|
|
warn(`Connection ${conn.id} request buffer exceeded ${MAX_REQUEST_BUFFER_SIZE} bytes\n`);
|
|
closeConnection(conn);
|
|
return;
|
|
}
|
|
conn.request_buffer += received.data;
|
|
}
|
|
|
|
let request = parseHttpMessage(conn.request_buffer);
|
|
if (request !== null) {
|
|
// Cancel idle timer when request is complete
|
|
if (conn.idle_timer) {
|
|
conn.idle_timer.cancel();
|
|
delete conn.idle_timer;
|
|
}
|
|
startUpstream(conn, request);
|
|
return;
|
|
}
|
|
|
|
if (received.eof) {
|
|
closeConnection(conn);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (eof || error)
|
|
closeConnection(conn);
|
|
}
|
|
|
|
function acceptClients(server) {
|
|
while (true) {
|
|
// Check connection limit
|
|
let active_connections = length(keys(connections));
|
|
if (active_connections >= MAX_CONNECTIONS) {
|
|
// Accept and immediately close to reject (don't leave in backlog)
|
|
let rejected = server.accept();
|
|
if (rejected) {
|
|
rejected.close();
|
|
}
|
|
warn(`Connection limit reached: ${active_connections}/${MAX_CONNECTIONS}\n`);
|
|
break;
|
|
}
|
|
|
|
let client = server.accept();
|
|
|
|
if (client === null)
|
|
break;
|
|
|
|
let conn = {
|
|
id: next_id++,
|
|
client,
|
|
upstream: null,
|
|
client_handle: null,
|
|
upstream_handle: null,
|
|
timer: null,
|
|
request_buffer: '',
|
|
response_buffer: '',
|
|
created_at: time()
|
|
};
|
|
|
|
connections[conn.id] = conn;
|
|
|
|
// Set idle timeout (uloop.timer signature: timeout_ms, callback)
|
|
conn.idle_timer = uloop.timer(CLIENT_IDLE_TIMEOUT, () => {
|
|
warn(`Connection ${conn.id} idle timeout\n`);
|
|
closeConnection(conn);
|
|
});
|
|
|
|
conn.client_handle = uloop.handle(client, (events, eof, error) => onClientRequest(conn, events, eof, error), uloop.ULOOP_READ);
|
|
}
|
|
}
|
|
|
|
const clash_api_enabled = uci.get(uciconfig, ucimain, 'clash_api_enabled') || '0';
|
|
|
|
if (clash_api_enabled !== '1')
|
|
exit(0);
|
|
|
|
target = parse_controller(uci.get(uciconfig, ucimain, 'clash_api_external_controller') || '127.0.0.1:9090', 9090);
|
|
listen = isEmpty(uci.get(uciconfig, ucimain, 'clash_api_proxy_external_controller'))
|
|
? deriveProxyController((index(target.host, ':') >= 0 ? '[' + target.host + ']' : target.host) + ':' + target.port)
|
|
: parse_controller(uci.get(uciconfig, ucimain, 'clash_api_proxy_external_controller'), 9091);
|
|
|
|
if (isEmpty(listen.host))
|
|
listen.host = '127.0.0.1';
|
|
|
|
server = socket.listen(listen.host, listen.port, null, 128, true);
|
|
|
|
if (server === null) {
|
|
warn(sprintf('homeproxy clash api proxy: failed to listen on %s:%d: %s\n', listen.host, listen.port, socket.error()));
|
|
exit(1);
|
|
}
|
|
|
|
uloop.init();
|
|
uloop.handle(server, () => acceptClients(server), uloop.ULOOP_READ);
|
|
|
|
warn(sprintf('homeproxy clash api proxy: listening on %s:%d, forwarding to %s:%d\n',
|
|
listen.host, listen.port, target.host, target.port));
|
|
|
|
uloop.run();
|