From c4a46c07b9c64a57ab511bb4688647b706b3e005 Mon Sep 17 00:00:00 2001 From: Henry Tsai Date: Thu, 18 Jan 2024 18:58:14 -0800 Subject: [PATCH] Added support to allow no registration (ie. open for all) (#102) --- README.md | 2 +- src/config.ts | 2 +- src/dwn-server.ts | 8 ++ src/http-api.ts | 2 +- src/registration/proof-of-work-manager.ts | 2 +- src/registration/registration-manager.ts | 30 ++++--- tests/dwn-server.spec.ts | 2 +- .../proof-of-work-manager.spec.ts | 10 +++ tests/scenarios/registration.spec.ts | 80 ++++++++++++------- 9 files changed, 97 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 7bf861d..66072e1 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/src/config.ts b/src/config.ts index 44d71da..4f835bb 100644 --- a/src/config.ts +++ b/src/config.ts @@ -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, diff --git a/src/dwn-server.ts b/src/dwn-server.ts index 8d55674..c19a030 100644 --- a/src/dwn-server.ts +++ b/src/dwn-server.ts @@ -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, @@ -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; + } } diff --git a/src/http-api.ts b/src/http-api.ts index 351e863..1a096b0 100644 --- a/src/http-api.ts +++ b/src/http-api.ts @@ -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); diff --git a/src/registration/proof-of-work-manager.ts b/src/registration/proof-of-work-manager.ts index 14554b5..5a19503 100644 --- a/src/registration/proof-of-work-manager.ts +++ b/src/registration/proof-of-work-manager.ts @@ -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 }; diff --git a/src/registration/registration-manager.ts b/src/registration/registration-manager.ts index 220af58..45a4732 100644 --- a/src/registration/registration-manager.ts +++ b/src/registration/registration-manager.ts @@ -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 { 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({ @@ -118,6 +125,11 @@ export class RegistrationManager implements TenantGate { * The TenantGate implementation. */ public async isActiveTenant(tenant: string): Promise { + // 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) { diff --git a/tests/dwn-server.spec.ts b/tests/dwn-server.spec.ts index ab00f4e..3197b91 100644 --- a/tests/dwn-server.spec.ts +++ b/tests/dwn-server.spec.ts @@ -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 () { diff --git a/tests/registration/proof-of-work-manager.spec.ts b/tests/registration/proof-of-work-manager.spec.ts index 70e87e9..8dde4ae 100644 --- a/tests/registration/proof-of-work-manager.spec.ts +++ b/tests/registration/proof-of-work-manager.spec.ts @@ -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)); + }); }); diff --git a/tests/scenarios/registration.spec.ts b/tests/scenarios/registration.spec.ts index 39d44db..ed5d1ee 100644 --- a/tests/scenarios/registration.spec.ts +++ b/tests/scenarios/registration.spec.ts @@ -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, @@ -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 @@ -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. @@ -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, {