Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
This repository contains a selection of command-line tools for administering Data Unions on Streamr:

* [autokick](#binautokickjs)
* [joinserver](#binjoinserverjs)

### Installation

Expand Down Expand Up @@ -78,3 +79,36 @@ Where the arguments are:
* `--private-key [key]`: An Ethereum private key for an address that has been granted permissions to:
* Read data in all the streams given with `--stream`, and
* Write to the join/part stream of your data union (not required in `--dry-run` mode, as you're not really kicking anybody)

### bin/joinserver.js

A http server to validate join requests to a Data Union. This can be used to implement custom validation for join requests, such as adding a CAPTCHA.

The util ships with a dummy validation logic called `hardcoded` that simply checks that the requests contain a pre-defined secret. In real life, you'll want to implement your own validation logic and pass it to the script with the `--validation-logic` option.

To get help, just run `bin/joinserver.js` without any arguments:

```
Usage: joinserver.js --private-key [privateKeyHex] ...

Options:
--help Show help [boolean]
--version Show version number [boolean]
--private-key Ethereum private key of the Data Union admin or join agent
[required]
--ethereum-rpc Ethereum RPC URL to use [string]
--validation-logic Loads the desired join request validation logic from
src/joinserver folder. The default 'hardcoded' logic is a
dummy logic that accepts requests that supply a hard-coded
secret. [string] [default: "hardcoded"]
--port TCP port to listen on for HTTP requests
[number] [default: 16823]
```

The script must be run with a private key for a user that has the permission to join members to the Data Union.

Once the join server is running, you can try it out by doing a HTTP request:

```
curl -v -H "Content-Type: application/json" -d "{\"secret\":\"my-very-secret-password\"}" http://localhost:16823/0x103efb97b56ac6c5e697e58812a1a0eaa2529b14/0x9e3d69305Da51f34eE29BfB52721e3A824d59e69
```
139 changes: 139 additions & 0 deletions bin/joinserver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/usr/bin/env node
const StreamrClient = require('streamr-client')
const ethers = require('ethers')
const express = require('express')
const app = express()

require('console-stamp')(console, { pattern: 'yyyy-mm-dd HH:MM:ss' })

const options = require('yargs')
.usage('Usage: $0 --private-key [privateKeyHex] ...')
.option('private-key', {
default: undefined,
describe: 'Ethereum private key of the Data Union admin or join agent',
})
.option('ethereum-rpc', {
type: 'string',
describe: 'Ethereum RPC URL to use',
default: undefined,
})
.option('validation-logic', {
type: 'string',
describe: 'Loads the desired join request validation logic from src/joinserver folder. The default \'hardcoded\' logic is a dummy logic that accepts requests that supply a hard-coded secret.',
default: 'hardcoded',
})
.option('port', {
type: 'number',
describe: 'TCP port to listen on for HTTP requests',
default: 16823,
})
.demandOption(['private-key'])
.argv;

const privateKeyWithPrefix = (options['private-key'].startsWith('0x') ? '' : '0x') + options['private-key']
const wallet = new ethers.Wallet(privateKeyWithPrefix)
console.log(`Configured with a private key for address: ${wallet.address}`)

// Load the specified validation logic
const ValidationLogic = require('../src/joinserver/' + options['validation-logic'])
const logic = new ValidationLogic(options)
console.log(`Validation logic: ${options['validation-logic']}`)

/**
* StreamrClient setup
*/
const clientConfig = {
auth: {
privateKey: options['private-key']
}
}

if (options['ethereum-rpc']) {
clientConfig.ethereumRpc = options['ethereum-rpc'] // TODO: does not exist, need to make this configurable in client!
}

// Create client
const streamr = new StreamrClient(clientConfig)
streamr.on('error', (err) => {
console.error(err)
})

// Configure HTTP server
app.use(express.json());
app.post('/:mainchainContractAddress/:memberAddress', async (req, res) => {
// Has this address already joined?
try {
const stats = await streamr.getMemberStats(req.params.mainchainContractAddress, req.params.memberAddress)
if (stats.active) {
const msg = `Address ${req.params.memberAddress} is already a member of Data Union ${req.params.mainchainContractAddress}`
console.log(msg)
res.status(400).send({
code: 'ALREADY_JOINED',
message: msg
})
return
}
} catch (err) {
// Attempt to join if the status could not be verified in advance
}

// Call validation logic. Throws on failure.
try {
logic.validate(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

case for TypeScript: "logic" interface

req.params.mainchainContractAddress,
req.params.memberAddress,
req.body
)
} catch (err) {
const msg = `Address ${req.params.memberAddress} failed validation while attempting to join Data Union ${req.params.mainchainContractAddress}. Error: ${err.message}`
console.log(msg)
res.status(400).send({
code: 'VALIDATION_FAILED',
message: msg
})
return
}

// Join the member! (assumption: promise resolves when added successfully)
try {
await streamr.addMembers(req.params.mainchainContractAddress, [req.params.memberAddress])
} catch (err) {
console.error(err)
res.status(500).send({
code: 'JOIN_FAILED',
message: `Failed to add member ${req.params.memberAddress} to join Data Union ${req.params.mainchainContractAddress}. The error was: ${err.message}`
})
return
}

// Verify by querying stats after joining
try {
const stats = await streamr.getMemberStats(req.params.mainchainContractAddress, req.params.memberAddress)
if (!stats.active) {
const msg = `Failed to add member ${req.params.memberAddress} to join Data Union ${req.params.mainchainContractAddress}. The member never became active.`
console.error(msg)
res.status(500).send({
code: 'JOIN_FAILED',
message: msg,
})
} else {
res.status(200).send(stats)
}
} catch (err) {
console.error(err)
res.status(500).send({
code: 'STATE_UNREACHABLE',
message: `Could not verify that the member ${req.params.memberAddress} joined Data Union ${req.params.mainchainContractAddress} successfully, because the Data Union state could not be retrieved.`
})
}
})

console.log(`Port: ${options.port}`)
app.listen(options.port, () => {
console.log('Data Union join server started.')
})

// Log unhandled rejection traces
process.on('unhandledRejection', (err, p) => {
console.error('Unhandled Rejection at: Promise', p, 'err:', err, `stack:`, err.stack)
})
Loading