diff --git a/README.md b/README.md index a902706..a5b15a2 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,16 @@ based on the [Stratum Mining Protocol](https://en.bitcoin.it/wiki/Stratum_mining ## Stats -You can see your proxy stats (number of miners and connections) by hittings `/stats`, ie: -`https://localhost:8892/stats`. +The proxy provides a few endpoints to see your stats: + +* `/stats`: shows the number of miners and connections + +* `/miners`: list of all miners, showing id, login and hashes for each one. + +* `/connections`: list of connections, showing id, host, port and amount of miners for each one. + +If you want to protect these endpoints (recommended) use the `credentials: { user, pass }` option in the proxy +constructor or the `--credentials=username:password` flag for the CLI. To get more advanced metrcis you will have to [run the proxy with PM2](https://github.com/cazala/coin-hive-stratum/wiki/Run-with-PM2). @@ -77,6 +85,7 @@ Options: --path Accept connections on a specific path. --key Path to private key file. Used for HTTPS/WSS. --cert Path to certificate file. Used for HTTPS/WSS. + --credentials Credentials to access the /stats, /miners and /connections endponts. (usage: --credentials=username:password) ``` ## API @@ -111,6 +120,14 @@ Options: * `cert`: path to certificate file (used for https/wss). + * `credentials`: specify credentials for the API endpoints (`/stats`, `/miners`, `/connections`). If credentials are + provided, you will need to use [Basic Auth](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) to + access the endpoints. + + * `user`: a username for the API endpoints + + * `pass`: a password for the API endpoints. + * `proxy.listen(port [, host])`: launches the server listening on the specified port (and optionally a host). * `proxy.on(event, callback)`: specify a callback for an event, each event has information about the miner who triggered diff --git a/bin/coin-hive-stratum b/bin/coin-hive-stratum index 9cd1889..55dc3fa 100755 --- a/bin/coin-hive-stratum +++ b/bin/coin-hive-stratum @@ -34,6 +34,18 @@ const options = { path: argv.path || defaults.path }; +if (argv["credentials"]) { + try { + const split = argv["credentials"].split(":"); + options.credentials = { + user: split[0], + pass: split[1] + }; + } catch (e) { + console.warn(`invalid credentials: "${argv["credentials"]}", the should be like "user:pass"`); + } +} + if (isHTTPS) { options.key = fs.readFileSync(key); options.cert = fs.readFileSync(cert); diff --git a/bin/help b/bin/help index 49da42d..c62ef01 100644 --- a/bin/help +++ b/bin/help @@ -15,4 +15,5 @@ Options: --max-miners-per-connection Set the max amount of miners per TCP connection. When this number is exceded, a new socket is created. By default it's 100. --path Accept connections on a specific path. --key Path to private key file. Used for HTTPS/WSS. - --cert Path to certificate file. Used for HTTPS/WSS. \ No newline at end of file + --cert Path to certificate file. Used for HTTPS/WSS. + --credentials Credentials to access the /stats, /miners and /connections endponts. (usage: --credentials=username:password) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a5c4ae9..890f689 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,14 @@ "shimmer": "1.2.0" } }, + "basic-auth": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", + "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", + "requires": { + "safe-buffer": "5.1.1" + } + }, "continuation-local-storage": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", diff --git a/package.json b/package.json index f7d0daa..1a1a7f5 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,11 @@ "type": "git", "url": "git+https://github.com/cazala/coin-hive-stratum.git" }, - "keywords": ["coinhive", "stratum", "proxy"], + "keywords": [ + "coinhive", + "stratum", + "proxy" + ], "author": "", "license": "MIT", "bugs": { @@ -26,6 +30,7 @@ "dependencies": { "@types/node": "^8.0.53", "@types/ws": "^3.2.0", + "basic-auth": "^2.0.0", "minimist": "^1.2.0", "moment": "^2.19.1", "pmx": "^1.5.5", diff --git a/src/Proxy.ts b/src/Proxy.ts index f40f6c2..814800a 100644 --- a/src/Proxy.ts +++ b/src/Proxy.ts @@ -17,7 +17,8 @@ import { FoundEvent, JobEvent, AuthedEvent, - OpenEvent + OpenEvent, + Credentials } from "src/types"; import { ServerRequest } from "http"; @@ -36,6 +37,7 @@ export type Options = { cert: Buffer; path: string; server: http.Server | https.Server; + credentials: Credentials; }; class Proxy extends EventEmitter { @@ -55,8 +57,9 @@ class Proxy extends EventEmitter { cert: Buffer = null; path: string = null; server: http.Server | https.Server = null; + credentials: Credentials = null; - constructor(constructorOptions: Options = defaults) { + constructor(constructorOptions: Partial = defaults) { super(); let options = Object.assign({}, defaults, constructorOptions) as Options; this.host = options.host; @@ -73,6 +76,7 @@ class Proxy extends EventEmitter { this.cert = options.cert; this.path = options.path; this.server = options.server; + this.credentials = options.credentials; this.on("error", () => { /* prevent unhandled error events from stopping the proxy */ }); @@ -83,15 +87,46 @@ class Proxy extends EventEmitter { const isHTTPS = !!(this.key && this.cert); if (!this.server) { const stats = (req, res) => { + if (this.credentials) { + const 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; + } + } const url = require("url").parse(req.url); + const proxyStats = this.getStats(); + let body = JSON.stringify({ + code: 404, + error: "Not Found" + }); + if (url.pathname === "/stats") { - const body = JSON.stringify(this.getStats(), null, 2); - res.writeHead(200, { - "Content-Length": Buffer.byteLength(body), - "Content-Type": "application/json" - }); - res.end(body); + 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) { const certificates = { @@ -207,13 +242,33 @@ class Proxy extends EventEmitter { getStats(): Stats { return Object.keys(this.connections).reduce( (stats, key) => ({ - miners: - stats.miners + this.connections[key].reduce((miners, connection) => miners + connection.miners.length, 0), - connections: stats.connections + this.connections[key].filter(connection => !connection.donation).length + miners: [ + ...stats.miners, + ...this.connections[key].reduce( + (miners, connection) => [ + ...miners, + ...connection.miners.map(miner => ({ + id: miner.id, + login: miner.login, + hashes: miner.hashes + })) + ], + [] + ) + ], + connections: [ + ...stats.connections, + ...this.connections[key].filter(connection => !connection.donation).map(connection => ({ + id: connection.id, + host: connection.host, + port: connection.port, + miners: connection.miners.length + })) + ] }), { - miners: 0, - connections: 0 + miners: [], + connections: [] } ); } diff --git a/src/types.ts b/src/types.ts index 1623e93..637cdef 100644 --- a/src/types.ts +++ b/src/types.ts @@ -16,8 +16,21 @@ export type TakenJob = Job & { }; export type Stats = { + miners: MinerStats[]; + connections: ConnectionStats[]; +}; + +export type MinerStats = { + id: string; + login: string | null; + hashes: number; +}; + +export type ConnectionStats = { + id: string; + host: string; + port: string; miners: number; - connections: number; }; export type WebSocketQuery = { @@ -39,6 +52,8 @@ export type Socket = NodeJS.Socket & { setKeepAlive: (value: boolean) => void; }; +export type Credentials = { user: string; pass: string }; + // CoinHive export type CoinHiveRequest = {