Skip to content

Commit

Permalink
Add optional (disabled by default) support for requiring new DIDs to …
Browse files Browse the repository at this point in the history
…complete some tasks before they may user the server.

proof of work challenge and terms of service challenges are currently available
  • Loading branch information
finn-block committed Nov 17, 2023
1 parent a1d2549 commit 1bd5a91
Show file tree
Hide file tree
Showing 17 changed files with 1,090 additions and 204 deletions.
44 changes: 35 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,15 +277,18 @@ cloudflared tunnel --url http://localhost:3000

Configuration can be set using environment variables

| Env Var | Description | Default |
| ------------------------- | -------------------------------------------------------------------------------------- | ---------------------- |
| `DS_PORT` | Port that the server listens on | `3000` |
| `DS_MAX_RECORD_DATA_SIZE` | maximum size for `RecordsWrite` data. use `b`, `kb`, `mb`, `gb` for value | `1gb` |
| `DS_WEBSOCKET_SERVER` | whether to enable listening over `ws:`. values: `on`,`off` | `on` |
| `DWN_STORAGE` | URL to use for storage by default. See [Storage Options](#storage-options) for details | `level://data` |
| `DWN_STORAGE_MESSAGES` | URL to use for storage of messages. | value of `DWN_STORAGE` |
| `DWN_STORAGE_DATA` | URL to use for data storage | value of `DWN_STORAGE` |
| `DWN_STORAGE_EVENTS` | URL to use for event storage | value of `DWN_STORAGE` |
| Env Var | Description | Default |
| -------------------------- | ----------------------------------------------------------------------------------------- | ---------------------- |
| `DS_PORT` | Port that the server listens on | `3000` |
| `DS_MAX_RECORD_DATA_SIZE` | maximum size for `RecordsWrite` data. use `b`, `kb`, `mb`, `gb` for value | `1gb` |
| `DS_WEBSOCKET_SERVER` | whether to enable listening over `ws:`. values: `on`,`off` | `on` |
| `DWN_REGISTRATION_POW` | require new users to complete a proof-of-work challenge | `false` |
| `DWN_REGISTRATION_TOS` | require users to agree to a terms of service. Value is path to the terms of service file. | unset |
| `DWN_STORAGE` | URL to use for storage by default. See [Storage Options](#storage-options) for details | `level://data` |
| `DWN_STORAGE_MESSAGES` | URL to use for storage of messages. | value of `DWN_STORAGE` |
| `DWN_STORAGE_DATA` | URL to use for data storage | value of `DWN_STORAGE` |
| `DWN_STORAGE_EVENTS` | URL to use for event storage | value of `DWN_STORAGE` |
| `DWN_STORAGE_REGISTRATION` | URL to use for storage of registered DIDs | `sqlite://data/dwn.db` |

### Storage Options

Expand All @@ -297,3 +300,26 @@ Several storage formats are supported, and may be configured with the `DWN_STORA
| Sqlite | `sqlite://dwn.db` | use three slashes for absolute paths, two for relative. Example shown creates a file `dwn.db` in the current working directory |
| MySQL | `mysql://user:pass@host/db?debug=true&timezone=-0700` | [all URL options documented here](https://github.com/mysqljs/mysql#connection-options) |
| PostgreSQL | `postgres:///dwn` | any options other than the URL scheme (`postgres://`) may also be specified via [standard environment variables](https://node-postgres.com/features/connecting#environment-variables) |

## Registration Requirements

There are multiple optional registration gates, each of which can be enabled (all are disabled by default). Tenants (DIDs) must comply with whatever
requirements are enabled before they are allowed to use the server. Tenants that have not completed the registration requirements will be met with a 401. Note that registration is tracked in a database, and only SQL-based databases are supported (LevelDB is not supported). Current registration
requirements are available at the `/info.json` endpoint.

- **Proof of Work** (`DWN_REGISTRATION_POW=true`) - new tenants must GET `/register/pow` for a challenge, then generate a nonce that produces a string that has a sha256 hex sum starting with the specified (`complexity`) number of zeros (`0`) when added to the end of the challenge (`sha256(challenge + nonce)`). This nonce should be POSTed to `/register/pow` with a JSON body including the `challenge`, the nonce in field `response` and `did`. Challenges expire after 5 minutes, and complexity will increase based on the number of successful proof-of-work registrations that have been completed within the last hour. This registration requirement is listed in `/info.json` as `proof-of-work-sha256-v0`.
- **Terms of Service** (`DWN_REGISTRATION_TOS=/path/to/tos.txt`) - new tenants must GET `/register/tos` to fetch the terms. These terms must be displayed to the human end-user, who must actively accept them. When the user accepts the terms, send the sha256 hash of the accepted terms and the user's did via POST `/register/tos`. The JSON body should have fields `tosHash` and `did`. To change the terms, update the file and restart the server. Users that accepted the old terms will be blocked until they accept the new terms. This registration requirement is listed in `/info.json` as `terms-of-service`.

## Server info

the server exposes information about itself via the `/info.json` endpoint, which returns data in the following format:

```json
{
"server": "@web5/dwn-server",
"maxFileSize": 1073741824,
"registrationRequirements": ["proof-of-work-sha256-v0", "terms-of-service"],
"version": "0.1.5",
"sdkVersion": "0.2.6"
}
```
55 changes: 47 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@
"@tbd54566975/dwn-sdk-js": "0.2.6",
"@tbd54566975/dwn-sql-store": "0.2.2",
"better-sqlite3": "^8.5.0",
"body-parser": "^1.20.2",
"bytes": "3.1.2",
"cors": "2.8.5",
"express": "4.18.2",
"kysely": "^0.26.3",
"loglevel": "^1.8.1",
"loglevel-plugin-prefix": "^0.8.4",
"multiformats": "11.0.2",
Expand Down
9 changes: 9 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ export const config = {
eventLog:
process.env.DWN_STORAGE_EVENTS || process.env.DWN_STORAGE || 'level://data',

// require POW-based registration for new tenants
registrationRequirementPow: process.env.DWN_REGISTRATION_POW == 'true',
tenantRegistrationStore:
process.env.DWN_STORAGE_REGISTRATION ||
process.env.DWN_STORAGE ||
'sqlite://data/dwn.db',

registrationRequirementTos: process.env.DWN_REGISTRATION_TOS,

// log level - trace/debug/info/warn/error
logLevel: process.env.DWN_SERVER_LOG_LEVEL || 'INFO',
};
25 changes: 21 additions & 4 deletions src/dwn-server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Dwn } from '@tbd54566975/dwn-sdk-js';

import { readFileSync } from 'fs';
import type { Server } from 'http';
import log from 'loglevel';
import prefix from 'loglevel-plugin-prefix';
Expand All @@ -10,7 +11,8 @@ import { HttpServerShutdownHandler } from './lib/http-server-shutdown-handler.js
import { type Config, config as defaultConfig } from './config.js';
import { HttpApi } from './http-api.js';
import { setProcessHandlers } from './process-handlers.js';
import { getDWNConfig } from './storage.js';
import { getDWNConfig, getDialectFromURI } from './storage.js';
import { TenantGate } from './tenant-gate.js';
import { WsApi } from './ws-api.js';

export type DwnServerOptions = {
Expand Down Expand Up @@ -46,12 +48,27 @@ export class DwnServer {
* The DWN creation is secondary and only happens if it hasn't already been done.
*/
async #setupServer(): Promise<void> {
let tenantGate: TenantGate;
if (!this.dwn) {
this.dwn = await Dwn.create(getDWNConfig(this.config));
const tenantGateDB = getDialectFromURI(
new URL(this.config.tenantRegistrationStore),
);
const tos =
this.config.registrationRequirementTos !== undefined
? readFileSync(this.config.registrationRequirementTos).toString()
: null;
tenantGate = new TenantGate(
tenantGateDB,
this.config.registrationRequirementPow,
this.config.registrationRequirementTos !== undefined,
tos,
);

this.dwn = await Dwn.create(getDWNConfig(this.config, tenantGate));
}

this.#httpApi = new HttpApi(this.dwn);
this.#httpApi.start(this.config.port, () => {
this.#httpApi = new HttpApi(this.dwn, tenantGate);
await this.#httpApi.start(this.config.port, () => {
log.info(`HttpServer listening on port ${this.config.port}`);
});

Expand Down
40 changes: 38 additions & 2 deletions src/http-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import cors from 'cors';
import type { Express, Request, Response } from 'express';
import express from 'express';
import { readFileSync } from 'fs';
import http from 'http';
import log from 'loglevel';
import { register } from 'prom-client';
Expand All @@ -20,18 +21,26 @@ import {
JsonRpcErrorCodes,
} from './lib/json-rpc.js';

import { config } from './config.js';
import { jsonRpcApi } from './json-rpc-api.js';
import { requestCounter, responseHistogram } from './metrics.js';
import type { TenantGate } from './tenant-gate.js';

const packagejson = process.env.npm_package_json
? JSON.parse(readFileSync(process.env.npm_package_json).toString())
: {};

export class HttpApi {
#api: Express;
#server: http.Server;
tenantGate: TenantGate;
dwn: Dwn;

constructor(dwn: Dwn) {
constructor(dwn: Dwn, tenantGate: TenantGate) {
this.#api = express();
this.#server = http.createServer(this.#api);
this.dwn = dwn;
this.tenantGate = tenantGate;

this.#setupMiddleware();
this.#setupRoutes();
Expand All @@ -47,6 +56,7 @@ export class HttpApi {

#setupMiddleware(): void {
this.#api.use(cors({ exposedHeaders: 'dwn-response' }));
this.#api.use(express.json());

this.#api.use(
responseTime((req: Request, res: Response, time) => {
Expand Down Expand Up @@ -181,13 +191,39 @@ export class HttpApi {
return res.json(jsonRpcResponse);
}
});

if (this.tenantGate) {
this.tenantGate.setupRoutes(this.#api);
}

this.#api.get('/info.json', (req, res) => {
res.setHeader('content-type', 'application/json');
const registrationRequirements: string[] = [];
if (config.registrationRequirementPow) {
registrationRequirements.push('proof-of-work-sha256-v0');
}
if (config.registrationRequirementTos) {
registrationRequirements.push('terms-of-service');
}

res.json({
server: process.env.npm_package_name,
maxFileSize: config.maxRecordDataSize,
registrationRequirements: registrationRequirements,
version: packagejson.version,
sdkVersion: packagejson.dependencies['@tbd54566975/dwn-sdk-js'],
});
});
}

#listen(port: number, callback?: () => void): void {
this.#server.listen(port, callback);
}

start(port: number, callback?: () => void): http.Server {
async start(port: number, callback?: () => void): Promise<http.Server> {
if (this.tenantGate) {
await this.tenantGate.initialize();
}
this.#listen(port, callback);
return this.#server;
}
Expand Down
12 changes: 8 additions & 4 deletions src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import pg from 'pg';
import Cursor from 'pg-cursor';

import type { Config } from './config.js';
import type { TenantGate } from './tenant-gate.js';

export enum EStoreType {
DataStore,
Expand All @@ -41,15 +42,18 @@ export enum BackendTypes {

export type StoreType = DataStore | EventLog | MessageStore;

export function getDWNConfig(config: Config): DwnConfig {
export function getDWNConfig(
config: Config,
tenantGate: TenantGate,
): DwnConfig {
const dataStore: DataStore = getStore(config.dataStore, EStoreType.DataStore);
const eventLog: EventLog = getStore(config.eventLog, EStoreType.EventLog);
const messageStore: MessageStore = getStore(
config.messageStore,
EStoreType.MessageStore,
);

return { eventLog, dataStore, messageStore };
return { eventLog, dataStore, messageStore, tenantGate };
}

function getLevelStore(
Expand Down Expand Up @@ -113,14 +117,14 @@ function getStore(storeString: string, storeType: EStoreType): StoreType {
case BackendTypes.SQLITE:
case BackendTypes.MYSQL:
case BackendTypes.POSTGRES:
return getDBStore(getDBFromURI(storeURI), storeType);
return getDBStore(getDialectFromURI(storeURI), storeType);

default:
throw invalidStorageSchemeMessage(storeURI.protocol);
}
}

function getDBFromURI(u: URL): Dialect {
export function getDialectFromURI(u: URL): Dialect {
switch (u.protocol.slice(0, -1)) {
case BackendTypes.SQLITE:
return new SqliteDialect({
Expand Down
Loading

0 comments on commit 1bd5a91

Please sign in to comment.