Skip to content

Commit

Permalink
Add http and https proxy feature
Browse files Browse the repository at this point in the history
Added new proxy feature based on http and https proxy agents.
Proxy feature works like notifications, there is many proxy
could be related one proxy entry.

Supported features
- Proxies can activate and disable in bulk
- Proxies auto enabled by default for new monitors
- Proxies could be applied in bulk to current monitors
- Both authenticated and anonymous proxies supported
- Export and import support for proxies
  • Loading branch information
ugurerkan committed Nov 3, 2021
1 parent 7ded97b commit 2b061ce
Show file tree
Hide file tree
Showing 12 changed files with 598 additions and 7 deletions.
23 changes: 23 additions & 0 deletions db/patch-proxy.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- You should not modify if this have pushed to Github, unless it does serious wrong with the db.
BEGIN TRANSACTION;

CREATE TABLE proxy (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
user_id INT NOT NULL,
protocol VARCHAR(10) NOT NULL,
host VARCHAR(255) NOT NULL,
port SMALLINT NOT NULL,
auth BOOLEAN NOT NULL,
username VARCHAR(255) NULL,
password VARCHAR(255) NULL,
active BOOLEAN NOT NULL DEFAULT 1,
'default' BOOLEAN NOT NULL DEFAULT 0,
created_date DATETIME DEFAULT (DATETIME('now')) NOT NULL
);

ALTER TABLE monitor ADD COLUMN proxy_id INTEGER REFERENCES proxy(id);

CREATE INDEX proxy_id ON monitor (proxy_id);
CREATE INDEX proxy_user_id ON proxy (user_id);

COMMIT;
21 changes: 19 additions & 2 deletions server/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,23 @@ async function sendImportantHeartbeatList(socket, monitorID, toUser = false, ove

}

/**
* Delivers proxy list
*
* @param socket
* @return {Promise<Bean[]>}
*/
async function sendProxyList(socket) {
const timeLogger = new TimeLogger();

const list = await R.find("proxy", " user_id = ? ", [socket.userID]);
io.to(socket.userID).emit("proxyList", list.map(bean => bean.export()));

timeLogger.print("Send Proxy List");

return list;
}

async function sendInfo(socket) {
socket.emit("info", {
version: checkVersion.version,
Expand All @@ -95,6 +112,6 @@ module.exports = {
sendNotificationList,
sendImportantHeartbeatList,
sendHeartbeatList,
sendInfo
sendProxyList,
sendInfo,
};

1 change: 1 addition & 0 deletions server/database.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class Database {
"patch-http-monitor-method-body-and-headers.sql": true,
"patch-2fa-invalidate-used-token.sql": true,
"patch-notification_sent_history.sql": true,
"patch-proxy.sql": true,
}

/**
Expand Down
51 changes: 47 additions & 4 deletions server/model/monitor.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const https = require("https");
const HttpProxyAgent = require("http-proxy-agent");
const HttpsProxyAgent = require("https-proxy-agent");
const dayjs = require("dayjs");
const utc = require("dayjs/plugin/utc");
let timezone = require("dayjs/plugin/timezone");
Expand Down Expand Up @@ -75,6 +77,7 @@ class Monitor extends BeanModel {
dns_resolve_server: this.dns_resolve_server,
dns_last_result: this.dns_last_result,
pushToken: this.pushToken,
proxyId: this.proxy_id,
notificationIDList,
tags: tags,
};
Expand Down Expand Up @@ -141,6 +144,11 @@ class Monitor extends BeanModel {
// Do not do any queries/high loading things before the "bean.ping"
let startTime = dayjs().valueOf();

const httpsAgentOptions = {
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: !this.getIgnoreTls(),
};

const options = {
url: this.url,
method: (this.method || "get").toLowerCase(),
Expand All @@ -151,15 +159,50 @@ class Monitor extends BeanModel {
"User-Agent": "Uptime-Kuma/" + version,
...(this.headers ? JSON.parse(this.headers) : {}),
},
httpsAgent: new https.Agent({
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
rejectUnauthorized: ! this.getIgnoreTls(),
}),
maxRedirects: this.maxredirects,
validateStatus: (status) => {
return checkStatusCode(status, this.getAcceptedStatuscodes());
},
};

if (this.proxy_id) {
const proxy = await R.load("proxy", this.proxy_id);

if (proxy && proxy.active) {
const httpProxyAgentOptions = {
protocol: proxy.protocol,
host: proxy.host,
port: proxy.port,
};
const httpsProxyAgentOptions = {
...httpsAgentOptions,
protocol: proxy.protocol,
hostname: proxy.host,
port: proxy.port,
};

if (proxy.auth) {
httpProxyAgentOptions.auth = `${proxy.username}:${proxy.password}`;
httpsProxyAgentOptions.auth = `${proxy.username}:${proxy.password}`;
}

debug(`HTTP options: ${JSON.stringify({
"http": httpProxyAgentOptions,
"https": httpsProxyAgentOptions,
})}`);

options.proxy = false;
options.httpAgent = new HttpProxyAgent(httpProxyAgentOptions);
options.httpsAgent = new HttpsProxyAgent(httpsProxyAgentOptions);
}
}

if (!options.httpsAgent) {
options.httpsAgent = new https.Agent(httpsAgentOptions);
}

debug(`Request options: ${JSON.stringify(options)}`);

let res = await axios.request(options);
bean.msg = `${res.status} - ${res.statusText}`;
bean.ping = dayjs().valueOf() - startTime;
Expand Down
21 changes: 21 additions & 0 deletions server/model/proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const { BeanModel } = require("redbean-node/dist/bean-model");

class Proxy extends BeanModel {
toJSON() {
return {
id: this._id,
userId: this._user_id,
protocol: this._protocol,
host: this._host,
port: this._port,
auth: !!this._auth,
username: this._username,
password: this._password,
active: !!this._active,
default: !!this._default,
createdDate: this._created_date,
};
}
}

module.exports = Proxy;
99 changes: 99 additions & 0 deletions server/proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
const { R } = require("redbean-node");

class Proxy {

/**
* Saves and updates given proxy entity
*
* @param proxy
* @param proxyID
* @param userID
* @return {Promise<Bean>}
*/
static async save(proxy, proxyID, userID) {
let bean;

if (proxyID) {
bean = await R.findOne("proxy", " id = ? AND user_id = ? ", [proxyID, userID]);

if (!bean) {
throw new Error("proxy not found");
}

} else {
bean = R.dispense("proxy");
}

// Make sure given proxy protocol is supported
if (!["http", "https"].includes(proxy.protocol)) {
throw new Error(`Unsupported proxy protocol "${proxy.protocol}. Supported protocols are http and https."`);
}

// When proxy is default update deactivate old default proxy
if (proxy.default) {
await R.exec("UPDATE proxy SET `default` = 0 WHERE `default` = 1");
}

bean.user_id = userID;
bean.protocol = proxy.protocol;
bean.host = proxy.host;
bean.port = proxy.port;
bean.auth = proxy.auth;
bean.username = proxy.username;
bean.password = proxy.password;
bean.active = proxy.active || true;
bean.default = proxy.default || false;

await R.store(bean);

if (proxy.applyExisting) {
await applyProxyEveryMonitor(bean.id, userID);
}

return bean;
}

/**
* Deletes proxy with given id and removes it from monitors
*
* @param proxyID
* @param userID
* @return {Promise<void>}
*/
static async delete(proxyID, userID) {
const bean = await R.findOne("proxy", " id = ? AND user_id = ? ", [proxyID, userID]);

if (!bean) {
throw new Error("proxy not found");
}

// Delete removed proxy from monitors if exists
await R.exec("UPDATE monitor SET proxy_id = null WHERE proxy_id = ?", [proxyID]);

// Delete proxy from list
await R.trash(bean);
}
}

/**
* Applies given proxy id to monitors
*
* @param proxyID
* @param userID
* @return {Promise<void>}
*/
async function applyProxyEveryMonitor(proxyID, userID) {
// Find all monitors with id and proxy id
const monitors = await R.getAll("SELECT id, proxy_id FROM monitor WHERE user_id = ?", [userID]);

// Update proxy id not match with given proxy id
for (const monitor of monitors) {
if (monitor.proxy_id !== proxyID) {
await R.exec("UPDATE monitor SET proxy_id = ? WHERE id = ?", [proxyID, monitor.id]);
}
}
}

module.exports = {
Proxy,
};
Loading

0 comments on commit 2b061ce

Please sign in to comment.