Skip to content

Commit

Permalink
feat: RPC API server listens on loopback by default (#142)
Browse files Browse the repository at this point in the history
* feat: authenticate RPC server

* fix: rpc api only listens on loopback by default

* chore: fix rpc server host warning in readme
  • Loading branch information
SgtPooki authored Sep 19, 2024
1 parent ba0a5af commit ac52598
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 37 deletions.
37 changes: 31 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,28 @@ $ npx @libp2p/amino-dht-bootstrapper amino
Options:
--config <CONFIG> Path to IPFS config file (required)
--metrics-path <METRICS_PATH> Metric endpoint path [default: /metrics]
--metrics-port <PORT> Metric endpoint path [default: /metrics]
--metrics-port <PORT> Metric endpoint path [default: 8888]
--enable-kademlia Whether to run the libp2p Kademlia protocol and join the IPFS DHT
--enable-autonat Whether to run the libp2p Autonat protocol
--api-port <PORT> Port to serve the RPC API [default: 8899]
--api-host <HOST> Host to serve the RPC API on [default: 127.0.0.1]
-h, --help Print help
```

### RPC API

To make a request via CURL, you can use the following command:

```sh
# run garbage collection
$ curl http://${HOST}:${RPC_PORT}/api/v0/nodejs/gc

# execute a heapdump
$ curl http://${HOST}:${RPC_PORT}/api/v0/nodejs/heapdump
```

Please note that the RPC API server only listens on the loopback interface (127.0.0.1) by default. If you decide to change the `api-host` option, please make sure that the RPC API server is only used for development purposes and is not accessible publicly.

### Configuring bootstrapper options

```
Expand All @@ -57,29 +73,38 @@ Options:
"description": "Path to IPFS config file",
"type": "string"
},
"enableKademlia": {
"enable-kademlia": {
"description": "Whether to run the libp2p Kademlia protocol and join the IPFS DHT",
"type": "boolean"
},
"enableAutonat": {
"enable-autonat": {
"description": "Whether to run the libp2p Autonat protocol",
"type": "boolean"
},
"metricsPath": {
"metrics-path": {
"description": "Metric endpoint path",
"default": "/metrics",
"type": "string"
},
"metricsPort": {
"metrics-port": {
"description": "Port to serve metrics",
"default": "8888",
"type": "string"
},
"api-port": {
"description": "Port for api endpoint",
"default": "8899",
"type": "string"
},
"api-host": {
"description": "The listen address hostname for the RPC API server",
"default": "127.0.0.1",
"type": "string"
},
"help": {
"description": "Show help text",
"type": "boolean"
}
}
```

## Building the Docker Image
Expand Down
45 changes: 45 additions & 0 deletions src/create-rpc-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createServer } from 'node:http'
import { writeHeapSnapshot } from 'node:v8'

export interface RpcServerOptions {
apiPort?: number
apiHost?: string
}

export async function createRpcServer ({ apiPort, apiHost }: RpcServerOptions): Promise<void> {
if (apiHost !== '127.0.0.1') {
// eslint-disable-next-line no-console
console.info('Warning: The RPC API host has been changed from 127.0.0.1. The RPC server is now running in insecure mode. This may expose critical control to external sources. Ensure that you implement proper authentication and authorization measures.')
}

const apiServer = createServer((req, res) => {
if (req.method === 'GET') {
if (req.url === '/api/v0/nodejs/gc') {
if (globalThis.gc == null) {
// maybe we're running in a non-v8 engine or `--expose-gc` wasn't passed
res.writeHead(503, { 'Content-Type': 'text/plain' })
res.end('Service Unavailable')
return
}
// force nodejs to run garbage collection
globalThis.gc?.()
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('OK')
} else if (req.url === '/api/v0/nodejs/heapdump') {
// force nodejs to generate a heapdump
// you can analyze the heapdump with https://github.com/facebook/memlab#heap-analysis-and-investigation to get some really useful insights
const filename = writeHeapSnapshot(`./snapshot-dir/${(new Date()).toISOString()}.heapsnapshot`)
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end(`OK ${filename}`)
}
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' })
res.end('Not Found')
}
})

await new Promise<void>((resolve) => apiServer.listen(apiPort, apiHost, resolve))

// eslint-disable-next-line no-console
console.info(`RPC api listening on: ${apiHost}:${apiPort}`)
}
39 changes: 8 additions & 31 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { readFile, writeFile } from 'node:fs/promises'
import { createServer } from 'node:http'
import { isAbsolute, join } from 'node:path'
import { parseArgs } from 'node:util'
import { writeHeapSnapshot } from 'node:v8'
import { noise } from '@chainsafe/libp2p-noise'
import { yamux } from '@chainsafe/libp2p-yamux'
import { autoNAT } from '@libp2p/autonat'
Expand All @@ -25,6 +24,7 @@ import { LevelDatastore } from 'datastore-level'
import { createLibp2p, type ServiceFactoryMap } from 'libp2p'
import { register } from 'prom-client'
import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string'
import { createRpcServer } from './create-rpc-server.js'
import { isPrivate } from './utils/is-private-ip.js'
import type { PrivateKey } from '@libp2p/interface'
import type { Multiaddr } from '@multiformats/multiaddr'
Expand Down Expand Up @@ -62,6 +62,11 @@ async function main (): Promise<void> {
default: '8899',
type: 'string'
},
'api-host': {
description: 'The listen address hostname for the RPC API server',
default: '127.0.0.1',
type: 'string'
},
help: {
description: 'Show help text',
type: 'boolean'
Expand All @@ -81,6 +86,7 @@ async function main (): Promise<void> {
'metrics-path': argMetricsPath,
'metrics-port': argMetricsPort,
'api-port': argApiPort,
'api-host': argApiHost,
help: argHelp
} = args.values

Expand Down Expand Up @@ -187,36 +193,7 @@ async function main (): Promise<void> {

console.info('Metrics server listening', `0.0.0.0:${argMetricsPort}${argMetricsPath}`)

const apiServer = createServer((req, res) => {
if (req.method === 'GET') {
if (req.url === '/api/v0/nodejs/gc') {
if (globalThis.gc == null) {
// maybe we're running in a non-v8 engine or `--expose-gc` wasn't passed
res.writeHead(503, { 'Content-Type': 'text/plain' })
res.end('Service Unavailable')
return
}
// force nodejs to run garbage collection
globalThis.gc?.()
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('OK')
} else if (req.url === '/api/v0/nodejs/heapdump') {
// force nodejs to generate a heapdump
// you can analyze the heapdump with https://github.com/facebook/memlab#heap-analysis-and-investigation to get some really useful insights
// TODO: make this authenticated so it can't be used to DOS the server
const filename = writeHeapSnapshot(`./snapshot-dir/${(new Date()).toISOString()}.heapsnapshot`)
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end(`OK ${filename}`)
}
} else {
res.writeHead(404, { 'Content-Type': 'text/plain' })
res.end('Not Found')
}
})

const apiPort = parseInt(argApiPort ?? options['api-port'].default, 10)
await new Promise<void>((resolve) => apiServer.listen(apiPort, '0.0.0.0', resolve))
console.info(`RPC api listening on: 0.0.0.0:${apiPort}`)
await createRpcServer({ apiPort: parseInt(argApiPort ?? options['api-port'].default, 10), apiHost: argApiHost })
}

main().catch(err => {
Expand Down

0 comments on commit ac52598

Please sign in to comment.