Skip to content

Commit

Permalink
Added support to allow no registration (ie. open for all) (#102)
Browse files Browse the repository at this point in the history
  • Loading branch information
thehenrytsai authored Jan 19, 2024
1 parent 60b0336 commit c4a46c0
Show file tree
Hide file tree
Showing 9 changed files with 97 additions and 41 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ Configuration can be set using environment variables
| `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_STORE_URL` | URL to use for storage of registered DIDs | `sqlite://data/dwn.db` |
| `DWN_REGISTRATION_STORE_URL` | URL to use for storage of registered DIDs. Leave unset to if DWN does not require registration (ie. open for all) | unset |
| `DWN_REGISTRATION_PROOF_OF_WORK_ENABLED` | Require new users to complete a proof-of-work challenge | `false` |
| `DWN_REGISTRATION_PROOF_OF_WORK_INITIAL_MAX_HASH` | Initial maximum allowed hash in 64 char HEX string. The more leading zeros (smaller number) the higher the difficulty. | `false` |
| `DWN_TERMS_OF_SERVICE_FILE_PATH` | Required terms of service agreement if set. Value is path to the terms of service file. | unset |
Expand Down
2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const config = {
eventLog: process.env.DWN_STORAGE_EVENTS || process.env.DWN_STORAGE || 'level://data',

// tenant registration feature configuration
registrationStoreUrl: process.env.DWN_REGISTRATION_STORE_URL || process.env.DWN_STORAGE || 'sqlite://data/dwn.db',
registrationStoreUrl: process.env.DWN_REGISTRATION_STORE_URL || process.env.DWN_STORAGE,
registrationProofOfWorkEnabled: process.env.DWN_REGISTRATION_PROOF_OF_WORK_ENABLED === 'true',
registrationProofOfWorkInitialMaxHash: process.env.DWN_REGISTRATION_PROOF_OF_WORK_INITIAL_MAX_HASH,
termsOfServiceFilePath: process.env.DWN_TERMS_OF_SERVICE_FILE_PATH,
Expand Down
8 changes: 8 additions & 0 deletions src/dwn-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export class DwnServer {

let registrationManager: RegistrationManager;
if (!this.dwn) {
// undefined registrationStoreUrl is used as a signal that there is no need for tenant registration, DWN is open for all.
registrationManager = await RegistrationManager.create({
registrationStoreUrl: this.config.registrationStoreUrl,
termsOfServiceFilePath: this.config.termsOfServiceFilePath,
Expand Down Expand Up @@ -89,4 +90,11 @@ export class DwnServer {
get wsServer(): WebSocketServer {
return this.#wsApi.server;
}

/**
* Gets the RegistrationManager for testing purposes.
*/
get registrationManager(): RegistrationManager {
return this.#httpApi.registrationManager;
}
}
2 changes: 1 addition & 1 deletion src/http-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ export class HttpApi {
this.#api.get('/registration/terms-of-service', (_req: Request, res: Response) => res.send(this.registrationManager.getTermsOfService()));
}

if (this.#config.registrationProofOfWorkEnabled || this.#config.termsOfServiceFilePath !== undefined) {
if (this.#config.registrationStoreUrl !== undefined) {
this.#api.post('/registration', async (req: Request, res: Response) => {
const requestBody = req.body;
console.log('Registration request:', requestBody);
Expand Down
2 changes: 1 addition & 1 deletion src/registration/proof-of-work-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { ProofOfWork } from "./proof-of-work.js";
*/
export class ProofOfWorkManager {
// Takes from seconds to ~1 minute to solve on an M1 MacBook.
private static readonly defaultMaximumAllowedHashValue = '000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF';
public static readonly defaultMaximumAllowedHashValue = '000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF';

// Challenge nonces that can be used for proof-of-work.
private challengeNonces: { currentChallengeNonce: string, previousChallengeNonce?: string };
Expand Down
30 changes: 21 additions & 9 deletions src/registration/registration-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,31 @@ export class RegistrationManager implements TenantGate {
this.termsOfService = termsOfService;
}

private constructor (termsOfServiceFilePath?: string) {
if (termsOfServiceFilePath !== undefined) {
const termsOfService = readFileSync(termsOfServiceFilePath).toString();
this.updateTermsOfService(termsOfService);
}
}

/**
* Creates a new RegistrationManager instance.
* @param input.registrationStoreUrl - The URL of the registration store.
* Set to `undefined` or empty string if tenant registration is not required (ie. DWN is open for all).
*
*/
public static async create(input: {
registrationStoreUrl: string,
registrationStoreUrl?: string,
termsOfServiceFilePath?: string
initialMaximumAllowedHashValue?: string,
}): Promise<RegistrationManager> {
const { termsOfServiceFilePath, registrationStoreUrl, initialMaximumAllowedHashValue } = input;

const registrationManager = new RegistrationManager(termsOfServiceFilePath);
const registrationManager = new RegistrationManager();

// short-circuit if tenant registration is not required.
if (!registrationStoreUrl) {
return registrationManager;
}

// Initialize terms-of-service.
if (termsOfServiceFilePath !== undefined) {
const termsOfService = readFileSync(termsOfServiceFilePath).toString();
registrationManager.updateTermsOfService(termsOfService);
}

// Initialize and start ProofOfWorkManager.
registrationManager.proofOfWorkManager = await ProofOfWorkManager.create({
Expand Down Expand Up @@ -118,6 +125,11 @@ export class RegistrationManager implements TenantGate {
* The TenantGate implementation.
*/
public async isActiveTenant(tenant: string): Promise<ActiveTenantCheckResult> {
// If there is no registration store initialized, then DWN is open for all.
if (this.registrationStore === undefined) {
return { isActiveTenant: true };
}

const tenantRegistration = await this.registrationStore.getTenantRegistration(tenant);

if (tenantRegistration === undefined) {
Expand Down
2 changes: 1 addition & 1 deletion tests/dwn-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { DwnServer } from '../src/dwn-server.js';
describe('DwnServer', function () {
let dwnServer: DwnServer;
before(async function () {
dwnServer = new DwnServer({ config: config });
dwnServer = new DwnServer({ config });
});

after(async function () {
Expand Down
10 changes: 10 additions & 0 deletions tests/registration/proof-of-work-manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,14 @@ describe('ProofOfWorkManager', function () {

expect(proofOfWorkManager.currentMaximumAllowedHashValue === initialMaximumAllowedHashValueAsBigInt).to.be.true;
});

it('should use default difficulty if not given', async function () {
const desiredSolveCountPerMinute = 10;
const proofOfWorkManager = await ProofOfWorkManager.create({
autoStart: false,
desiredSolveCountPerMinute,
});

expect(proofOfWorkManager.currentMaximumAllowedHashValue).to.equal(BigInt('0x' + ProofOfWorkManager.defaultMaximumAllowedHashValue));
});
});
80 changes: 53 additions & 27 deletions tests/scenarios/registration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,16 @@ import {
DataStream,
DidKeyResolver,
} from '@tbd54566975/dwn-sdk-js';
import type { Dwn, Persona } from '@tbd54566975/dwn-sdk-js';
import type { Persona } from '@tbd54566975/dwn-sdk-js';

import { expect } from 'chai';
import { readFileSync } from 'fs';
import type { Server } from 'http';
import fetch from 'node-fetch';
import { webcrypto } from 'node:crypto';
import { useFakeTimers } from 'sinon';
import { v4 as uuidv4 } from 'uuid';

import { config } from '../../src/config.js';
import { HttpApi } from '../../src/http-api.js';
import type {
JsonRpcRequest,
JsonRpcResponse,
Expand All @@ -23,15 +21,15 @@ import {
createJsonRpcRequest,
} from '../../src/lib/json-rpc.js';
import { ProofOfWork } from '../../src/registration/proof-of-work.js';
import { getTestDwn } from '../test-dwn.js';
import {
createRecordsWriteMessage,
} from '../utils.js';
import type { ProofOfWorkChallengeModel } from '../../src/registration/proof-of-work-types.js';
import type { RegistrationData, RegistrationRequest } from '../../src/registration/registration-types.js';
import { RegistrationManager } from '../../src/registration/registration-manager.js';
import type { RegistrationManager } from '../../src/registration/registration-manager.js';
import { DwnServerErrorCode } from '../../src/dwn-error.js';
import { ProofOfWorkManager } from '../../src/registration/proof-of-work-manager.js';
import { DwnServer } from '../../src/dwn-server.js';

if (!globalThis.crypto) {
// @ts-ignore
Expand All @@ -44,47 +42,75 @@ describe('Registration scenarios', function () {
const proofOfWorkEndpoint = 'http://localhost:3000/registration/proof-of-work';
const registrationEndpoint = 'http://localhost:3000/registration';

let httpApi: HttpApi;
let server: Server;
let alice: Persona;
let registrationManager: RegistrationManager;
let dwn: Dwn;
let clock;
let dwnServer: DwnServer;
const dwnServerConfig = { ...config } // not touching the original config

before(async function () {
clock = useFakeTimers({ shouldAdvanceTime: true });

config.registrationStoreUrl = 'sqlite://';
config.registrationProofOfWorkEnabled = true;
config.termsOfServiceFilePath = './tests/fixtures/terms-of-service.txt';
config.registrationProofOfWorkInitialMaxHash = '0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'; // 1 in 16 chance of solving

// RegistrationManager creation
const registrationStoreUrl = config.registrationStoreUrl;
const termsOfServiceFilePath = config.termsOfServiceFilePath;
const initialMaximumAllowedHashValue = config.registrationProofOfWorkInitialMaxHash;
registrationManager = await RegistrationManager.create({ registrationStoreUrl, termsOfServiceFilePath, initialMaximumAllowedHashValue });

dwn = await getTestDwn(registrationManager);

httpApi = new HttpApi(config, dwn, registrationManager);

alice = await DidKeyResolver.generate();

// NOTE: using SQL to workaround an issue where multiple instances of DwnServer can be started using LevelDB in the same test run,
// and dwn-server.spec.ts already uses LevelDB.
dwnServerConfig.messageStore = 'sqlite://',
dwnServerConfig.dataStore = 'sqlite://',
dwnServerConfig.eventLog = 'sqlite://',

// registration config
dwnServerConfig.registrationStoreUrl = 'sqlite://';
dwnServerConfig.registrationProofOfWorkEnabled = true;
dwnServerConfig.termsOfServiceFilePath = './tests/fixtures/terms-of-service.txt';
dwnServerConfig.registrationProofOfWorkInitialMaxHash = '0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'; // 1 in 16 chance of solving

dwnServer = new DwnServer({ config: dwnServerConfig });
await dwnServer.start();
registrationManager = dwnServer.registrationManager;
});

beforeEach(async function () {
server = await httpApi.start(3000);
});

afterEach(async function () {
server.close();
server.closeAllConnections();
});

after(function () {
dwnServer.stop(() => { });
clock.restore();
});

it('should allow tenant registration to be turned off to allow all DWN messages through.', async () => {
// Scenario:
// 1. There is a DWN that does not require tenant registration.
// 2. Alice can write to the DWN without registering as a tenant.

const configClone = {
...dwnServerConfig,
registrationStoreUrl: '', // set to empty to disable tenant registration
port: 3001,
registrationProofOfWorkEnabled: false,
termsOfServiceFilePath: undefined,
};
const dwnServer = new DwnServer({ config: configClone });
await dwnServer.start();

const { jsonRpcRequest, dataBytes } = await generateRecordsWriteJsonRpcRequest(alice);
const writeResponse = await fetch('http://localhost:3001', {
method: 'POST',
headers: {
'dwn-request': JSON.stringify(jsonRpcRequest),
},
body: new Blob([dataBytes]),
});
const writeResponseBody = await writeResponse.json() as JsonRpcResponse;
expect(writeResponse.status).to.equal(200);
expect(writeResponseBody.result.reply.status.code).to.equal(202);

dwnServer.stop(() => { });
});

it('should facilitate tenant registration with terms-of-service and proof-or-work turned on', async () => {
// Scenario:
// 1. Alice fetches the terms-of-service.
Expand All @@ -101,7 +127,7 @@ describe('Registration scenarios', function () {
});
const termsOfServiceFetched = await termsOfServiceGetResponse.text();
expect(termsOfServiceGetResponse.status).to.equal(200);
expect(termsOfServiceFetched).to.equal(readFileSync(config.termsOfServiceFilePath).toString());
expect(termsOfServiceFetched).to.equal(readFileSync(dwnServerConfig.termsOfServiceFilePath).toString());

// 2. Alice fetches the proof-of-work challenge.
const proofOfWorkChallengeGetResponse = await fetch(proofOfWorkEndpoint, {
Expand Down

0 comments on commit c4a46c0

Please sign in to comment.