UNPKG

leat-stratum-proxy

Version:

A fork of coinhive with donation and some other 'uneeded' logic pulled out.

775 lines (751 loc) 23.6 kB
const DEBUG = process.env.DEBUG || void 0; const defaults = { host: "pool.supportxmr.com", port: 3333, pass: "x", ssl: false, address: null, user: null, diff: null, dynamicPool: false, maxMinersPerConnection: 100, cookie: 'loginCookie' }; const EventEmitter = require("events"); const WebSocket = require("ws"); const url = require("url"); const http = require("http"); const https = require("https"); function Queue(ms) { EventEmitter.call(this); if (ms === void 0) { ms = 100; } this.events = []; this.interval = null; this.bypassed = false; this.ms = 100; this.ms = ms; return this; } Queue.prototype = Object.create(EventEmitter.prototype); Queue.prototype.constructor = Queue; Queue.prototype.start = function () { var _this = this; if (this.interval == null) { var that_1 = this; this.interval = setInterval(function () { var event = that_1.events.pop(); if (event) { that_1.emit(event.type, event.payload); } else { _this.bypass(); } }, this.ms); } }; Queue.prototype.stop = function () { if (this.interval != null) { clearInterval(this.interval); this.interval = null; } }; Queue.prototype.bypass = function () { this.bypassed = true; this.stop(); }; Queue.prototype.push = function (event) { if (this.bypassed) { this.emit(event.type, event.payload); } else { this.events.push(event); } }; var pmx = require("pmx"); var probe = pmx.probe(); Metrics = { minersCounter: probe.counter({ name: "Miners" }), connectionsCounter: probe.counter({ name: "Connections" }), sharesCounter: probe.counter({ name: "Shares" }), sharesMeter: probe.meter({ name: "Shares per minute", samples: 60 }) } const net = require("net"); const tls = require("tls"); const uuid = require("uuid"); function Connection(options) { EventEmitter.call(this); this.id = uuid.v4(); this.host = null; this.port = null; this.ssl = null; this.online = null; this.socket = null; this.queue = null; this.buffer = ""; this.rpcId = 1; this.rpc = {}; this.auth = {}; this.minerId = {}; this.miners = []; this.host = options.host; this.port = options.port; this.ssl = options.ssl; return this; } Connection.prototype = Object.create(EventEmitter.prototype); Connection.prototype.constructor = Connection; Connection.prototype.connect = function () { var _this = this; if (this.online) { this.kill(); } this.queue = new Queue(); if (this.ssl) { this.socket = tls.connect(+this.port, this.host, { rejectUnauthorized: false }); } else { this.socket = net.connect(+this.port, this.host); } this.socket.on("connect", this.ready.bind(this)); this.socket.on("error", function (error) { console.warn("socket error (" + _this.host + ":" + _this.port + ")", error.message); _this.emit("error", error); _this.connect(); }); this.socket.on("close", function () { DEBUG && console.log("socket closed (" + _this.host + ":" + _this.port + ")"); _this.emit("close"); }); this.socket.setKeepAlive(true); this.socket.setEncoding("utf8"); this.online = true; Metrics.connectionsCounter.inc(); }; Connection.prototype.kill = function () { if (this.socket != null) { try { this.socket.end(); this.socket.destroy(); } catch (e) { console.warn("something went wrong while destroying socket (" + this.host + ":" + this.port + "):", e.message); } } if (this.queue != null) { this.queue.stop(); } if (this.online) { this.online = false; Metrics.connectionsCounter.dec(); } }; Connection.prototype.ready = function () { var _this = this; // message from pool this.socket.on("data", function (chunk) { _this.buffer += chunk; while (_this.buffer.includes("\n")) { var newLineIndex = _this.buffer.indexOf("\n"); var stratumMessage = _this.buffer.slice(0, newLineIndex); _this.buffer = _this.buffer.slice(newLineIndex + 1); _this.receive(stratumMessage); } }); // message from miner this.queue.on("message", function (message) { if (!_this.online) { return false; } if (!_this.socket.writable) { if (message.method === "keepalived") { return false; } var retry = message.retry ? message.retry * 2 : 1; var ms = retry * 100; message.retry = retry; setTimeout(function () { _this.queue.push({ type: "message", payload: message }); }, ms); return false; } try { if (message.retry) { delete message.retry; } _this.socket.write(JSON.stringify(message) + "\n"); } catch (e) { console.warn("failed to send message to pool (" + _this.host + ":" + _this.port + "): " + JSON.stringify(message)); } }); // kick it this.queue.start(); this.emit("ready"); }; Connection.prototype.receive = function (message) { var data = null; try { data = JSON.parse(message); } catch (e) { return console.warn("invalid stratum message:", message); } // it's a response if (data.id) { var response = data; if (!this.rpc[response.id]) { // miner is not online anymore return; } var minerId = this.rpc[response.id].minerId; var method = this.rpc[response.id].message.method; switch (method) { case "login": { if (response.error && response.error.code === -1) { this.emit(minerId + ":error", { error: "invalid_site_key" }); return; } var result = response.result; var auth = result.id; this.auth[minerId] = auth; this.minerId[auth] = minerId; this.emit(minerId + ":authed", auth); if (result.job) { this.emit(minerId + ":job", result.job); } break; } case "submit": { var job = this.rpc[response.id].message.params; if (response.result && response.result.status === "OK") { this.emit(minerId + ":accepted", job); } else if (response.error) { this.emit(minerId + ":error", response.error); } break; } default: { if (response.error && response.error.code === -1) { this.emit(minerId + ":error", response.error); } } } delete this.rpc[response.id]; } else { // it's a request var request = data; switch (request.method) { case "job": { var jobParams = request.params; var minerId = this.minerId[jobParams.id]; if (!minerId) { // miner is not online anymore return; } this.emit(minerId + ":job", request.params); break; } } } }; Connection.prototype.send = function (id, method, params) { if (params === void 0) { params = {}; } var message = { id: this.rpcId++, method: method, params: params }; switch (method) { case "login": { // .. break; } case "keepalived": { if (this.auth[id]) { var keepAliveParams = message.params; keepAliveParams.id = this.auth[id]; } else { return false; } } case "submit": { if (this.auth[id]) { var submitParams = message.params; submitParams.id = this.auth[id]; } else { return false; } } } this.rpc[message.id] = { minerId: id, message: message }; this.queue.push({ type: "message", payload: message }); }; Connection.prototype.addMiner = function (miner) { if (this.miners.indexOf(miner) === -1) { this.miners.push(miner); } }; Connection.prototype.removeMiner = function (minerId) { var miner = this.miners.find(function (x) { return x.id === minerId; }); if (miner) { this.miners = this.miners.filter(function (x) { return x.id !== minerId; }); this.clear(miner.id); } }; Connection.prototype.clear = function (id) { var _this = this; var auth = this.auth[id]; delete this.auth[id]; delete this.minerId[auth]; Object.keys(this.rpc).forEach(function (key) { if (_this.rpc[key].minerId === id) { delete _this.rpc[key]; } }); }; /* * Miner * */ function Miner(options) { EventEmitter.call(this) this.id = uuid.v4(); this.login = null; this.address = null; this.user = null; this.diff = null; this.pass = null; this.heartbeat = null; this.connection = null; this.queue = new Queue(); this.ws = null; this.online = false; this.jobs = []; this.hashes = 0; this.connection = options.connection; this.ws = options.ws; this.address = options.address; this.user = options.user; this.diff = options.diff; this.pass = options.pass; return this; } Miner.prototype = Object.create(EventEmitter.prototype); Miner.prototype.constructor = Miner; Miner.prototype.connect = function () { var _this = this; DEBUG && console.log("miner connected (" + this.id + ")"); Metrics.minersCounter.inc(); this.ws.on("message", this.handleMessage.bind(this)); this.ws.on("close", function () { if (_this.online) { DEBUG && console.log("miner connection closed (" + _this.id + ")"); _this.kill(); } }); this.ws.on("error", function (error) { if (_this.online) { DEBUG && console.log("miner connection error (" + _this.id + "):", error.message); _this.kill(); } }); this.connection.addMiner(this); this.connection.on(this.id + ":authed", this.handleAuthed.bind(this)); this.connection.on(this.id + ":job", this.handleJob.bind(this)); this.connection.on(this.id + ":accepted", this.handleAccepted.bind(this)); this.connection.on(this.id + ":error", this.handleError.bind(this)); this.queue.on("message", function (message) { return _this.connection.send(_this.id, message.method, message.params); }); this.heartbeat = setInterval(function () { return _this.connection.send(_this.id, "keepalived"); }, 30000); this.online = true; if (this.online) { this.queue.start(); DEBUG && console.log("miner started (" + this.id + ")"); this.emit("open", { id: this.id }); } }; Miner.prototype.kill = function () { this.queue.stop(); this.connection.removeMiner(this.id); this.connection.removeAllListeners(this.id + ":authed"); this.connection.removeAllListeners(this.id + ":job"); this.connection.removeAllListeners(this.id + ":accepted"); this.connection.removeAllListeners(this.id + ":error"); this.jobs = []; this.hashes = 0; this.ws.close(); if (this.heartbeat) { clearInterval(this.heartbeat); this.heartbeat = null; } if (this.online) { this.online = false; Metrics.minersCounter.dec(); DEBUG && console.log("miner disconnected (" + this.id + ")"); this.emit("close", { id: this.id, login: this.login }); } this.removeAllListeners(); }; Miner.prototype.sendToMiner = function (payload) { var coinhiveMessage = JSON.stringify(payload); if (this.online) { try { this.ws.send(coinhiveMessage); } catch (e) { this.kill(); } } }; Miner.prototype.sendToPool = function (method, params) { this.queue.push({ type: "message", payload: { method: method, params: params } }); }; Miner.prototype.handleAuthed = function (auth) { DEBUG && console.log("miner authenticated (" + this.id + "):", auth); this.sendToMiner({ type: "authed", params: { token: "", hashes: 0 } }); this.emit("authed", { id: this.id, login: this.login, auth: auth }); }; Miner.prototype.handleJob = function (job) { var _this = this; DEBUG && console.log("job arrived (" + this.id + "):", job.job_id); this.jobs.push(job); this.sendToMiner({ type: "job", params: this.jobs.pop() }); this.emit("job", { id: this.id, login: this.login, job: job }); }; Miner.prototype.handleAccepted = function (job) { this.hashes++; DEBUG && console.log("shares accepted (" + this.id + "):", this.hashes); Metrics.sharesCounter.inc(); Metrics.sharesMeter.mark(); this.sendToMiner({ type: "hash_accepted", params: { hashes: this.hashes } }); this.emit("accepted", { id: this.id, login: this.login, hashes: this.hashes }); }; Miner.prototype.handleError = function (error) { console.warn("pool connection error (" + this.id + "):", error.error || (error && JSON.stringify(error)) || "unknown error"); this.sendToMiner({ type: "error", params: error }); this.emit("error", { id: this.id, login: this.login, error: error }); this.kill(); }; Miner.prototype.handleMessage = function (message) { var data; try { data = JSON.parse(message); } catch (e) { console.warn("can't parse message as JSON from miner:", message, e.message); return; } switch (data.type) { case "auth": { var params = data.params; this.login = this.address || params.site_key; var user = this.user || params.user; if (user) { this.login += "." + user; } if (this.diff) { this.login += "+" + this.diff; } this.sendToPool("login", { login: this.login, pass: this.pass }); break; } case "submit": { var job = data.params; DEBUG && console.log("job submitted (" + this.id + "):", job.job_id); this.sendToPool("submit", job); this.emit("found", { id: this.id, login: this.login, job: job }); break; } } }; G_UP_TIME = Date.now() function Proxy(constructorOptions) { EventEmitter.call(this); if (constructorOptions === void 0) { constructorOptions = defaults; } this.host = null; this.port = null; this.pass = null; this.ssl = null; this.address = null; this.user = null; this.diff = null; this.dynamicPool = false; this.maxMinersPerConnection = 100; this.connections = {}; this.wss = null; this.key = null; this.cert = null; this.path = null; this.server = null; this.credentials = null; var options = Object.assign({}, defaults, constructorOptions); this.host = options.host; this.port = options.port; this.pass = options.pass; this.ssl = options.ssl; this.address = options.address; this.user = options.user; this.diff = options.diff; this.dynamicPool = options.dynamicPool; this.maxMinersPerConnection = options.maxMinersPerConnection; this.key = options.key; this.cert = options.cert; this.path = options.path; this.server = options.server; this.credentials = options.credentials; this.on("error", function () { /* prevent unhandled error events from stopping the proxy */ }); return this; } Proxy.prototype = Object.create(EventEmitter.prototype); Proxy.prototype.constructor = Proxy; Proxy.prototype.listen = function (port, host, callback) { var _this = this; // create server var isHTTPS = !!(this.key && this.cert); if (!this.server) { var stats = function (req, res) { if (_this.credentials) { var auth = require("basic-auth")(req); if (!auth || auth.name !== _this.credentials.user || auth.pass !== _this.credentials.pass) { res.statusCode = 401; res.setHeader("WWW-Authenticate", 'Basic realm="Access to stats"'); res.end("Access denied"); return; } } var url = require("url").parse(req.url); var proxyStats = _this.getStats(); var body = JSON.stringify({ code: 404, error: "Not Found" }); if (url.pathname === "/stats") { body = JSON.stringify({ miners: proxyStats.miners.length, connections: proxyStats.connections.length }, null, 2); } if (url.pathname === "/miners") { body = JSON.stringify(proxyStats.miners, null, 2); } if (url.pathname === "/connections") { body = JSON.stringify(proxyStats.connections, null, 2); } res.writeHead(200, { "Content-Length": Buffer.byteLength(body), "Content-Type": "application/json" }); res.end(body); }; if (isHTTPS) { var certificates = { key: this.key, cert: this.cert }; this.server = https.createServer(certificates, stats); } else { this.server = http.createServer(stats); } } var wssOptions = { server: this.server }; if (this.path) { wssOptions.path = this.path; } this.wss = new WebSocket.Server(wssOptions); this.wss.on("connection", function (ws, req) { var cookie = new RegExp(defaults.cookie + '=(.*?)(?:; |$)').exec(req.headers.cookie); if(cookie) cookie = cookie[1]; var params = url.parse(req.url, true).query; var host = _this.host; var port = _this.port; var pass = _this.pass; if (params.pool && _this.dynamicPool) { var split = params.pool.split(":"); host = split[0] || _this.host; port = Number(split[1]) || _this.port; pass = split[2] || _this.pass; } var connection = _this.getConnection(host, port); var miner = new Miner({ connection: connection, ws: ws, address: _this.address, user: _this.user, diff: _this.diff, pass: pass, }); const _ = data => Object.assign(data, {cookie: cookie}) miner.on("open", function (data) { return _this.emit("open", _(data)); }); miner.on("authed", function (data) { return _this.emit("authed", _(data)); }); miner.on("job", function (data) { return _this.emit("job", _(data)); }); miner.on("found", function (data) { return _this.emit("found", _(data)); }); miner.on("accepted", function (data) { return _this.emit("accepted", _(data)); }); miner.on("close", function (data) { return _this.emit("close", _(data)); }); miner.on("error", function (data) { return _this.emit("error", _(data)); }); miner.connect(); }); if (!host && !callback) { this.server.listen(port); } else if (!host && callback) { this.server.listen(port, callback); } else if (host && !callback) { this.server.listen(port, host); } else { this.server.listen(port, host, callback); } DEBUG && console.log("listening on port " + port + (isHTTPS ? ", using a secure connection" : "")); if (wssOptions.path) { DEBUG && console.log("path: " + wssOptions.path); } if (!this.dynamicPool) { DEBUG && console.log("host: " + this.host); DEBUG && console.log("port: " + this.port); DEBUG && console.log("pass: " + this.pass); } }; Proxy.prototype.getConnection = function (host, port) { var _this = this; var connectionId = host + ":" + port; if (!this.connections[connectionId]) { this.connections[connectionId] = []; } var connections = this.connections[connectionId]; var availableConnections = connections.filter(function (connection) { return _this.isAvailable(connection); }); if (availableConnections.length === 0) { var connection = new Connection({ host: host, port: port, ssl: this.ssl }); connection.connect(); connection.on("close", function () { DEBUG && console.log("connection closed (" + connectionId + ")"); }); connection.on("error", function (error) { DEBUG && console.log("connection error (" + connectionId + "):", error.message); }); connections.push(connection); return connection; } return availableConnections.pop(); }; Proxy.prototype.isAvailable = function (connection) { return connection.miners.length < this.maxMinersPerConnection; }; Proxy.prototype.isEmpty = function (connection) { return connection.miners.length === 0; }; Proxy.prototype.getStats = function () { var _this = this; const UP_TIME = Date.now() - G_UP_TIME; return Object.keys(this.connections).reduce(function (stats, key) { return ({ miners: stats.miners.concat(_this.connections[key].reduce(function (miners, connection) { return miners.concat(connection.miners.map(function (miner) { return ({ id: miner.id, login: miner.login, hashes: miner.hashes }); })); }, [])), connections: stats.connections.concat(_this.connections[key].map(function (connection) { return ({ id: connection.id, host: connection.host, port: connection.port, miners: connection.miners.length }); })), uptime: UP_TIME }); }, { miners: [], connections: [] }); }; Proxy.prototype.kill = function () { var _this = this; Object.keys(this.connections).forEach(function (connectionId) { var connections = _this.connections[connectionId]; connections.forEach(function (connection) { connection.kill(); connection.miners.forEach(function (miner) { return miner.kill(); }); }); }); this.wss.close(); }; //module.exports = {Proxy: Proxy, Miner: Miner, Connection: Connection, Queue: Queue, Metrics: Metrics}; module.exports = Proxy;