Skip to content

Commit

Permalink
replace url encoding with base64url encoding for protocol uri in the …
Browse files Browse the repository at this point in the history
…path segement
  • Loading branch information
LiranCohen committed Sep 19, 2024
1 parent 295be4a commit c9b92e8
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 66 deletions.
99 changes: 92 additions & 7 deletions package-lock.json

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

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
"dependencies": {
"@tbd54566975/dwn-sdk-js": "0.4.7",
"@tbd54566975/dwn-sql-store": "0.6.7",
"@web5/common": "^1.0.2",
"@web5/crypto": "^1.0.3",
"@web5/dids": "^1.1.3",
"better-sqlite3": "^8.5.0",
"body-parser": "^1.20.2",
"bytes": "3.1.2",
Expand Down Expand Up @@ -61,7 +63,6 @@
"@types/ws": "8.5.10",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"@web5/dids": "^1.1.3",
"c8": "8.0.1",
"chai": "4.3.6",
"chai-as-promised": "7.1.1",
Expand Down Expand Up @@ -108,7 +109,7 @@
},
"overrides": {
"express": {
"serve-static": "^1.16.2"
"serve-static": "^1.16.2"
}
}
}
102 changes: 61 additions & 41 deletions src/http-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { jsonRpcRouter } from './json-rpc-api.js';
import { Web5ConnectServer } from './web5-connect/web5-connect-server.js';
import { createJsonRpcErrorResponse, JsonRpcErrorCodes } from './lib/json-rpc.js';
import { requestCounter, responseHistogram } from './metrics.js';
import { Convert } from '@web5/common';


export class HttpApi {
Expand Down Expand Up @@ -49,7 +50,7 @@ export class HttpApi {
httpApi.#packageInfo.version = packageJson.version;
httpApi.#packageInfo.sdkVersion = packageJson.dependencies ? packageJson.dependencies['@tbd54566975/dwn-sdk-js'] : undefined;
} catch (error: any) {
log.error('could not read `package.json` for version info', error);
log.info('could not read `package.json` for version info', error);
}

httpApi.#config = config;
Expand Down Expand Up @@ -150,58 +151,77 @@ export class HttpApi {
return res.status(400).send('protocol path is required');
}

const queryOptions = { filter: {} } as any;
for (const param in req.query) {
const keys = param.split('.');
const lastKey = keys.pop();
const lastLevelObject = keys.reduce((obj, key) => obj[key] = obj[key] || {}, queryOptions)
lastLevelObject[lastKey] = req.query[param];
}
// wrap request in a try-catch block to handle any unexpected errors
try {
const queryOptions = { filter: {} } as any;
for (const param in req.query) {
const keys = param.split('.');
const lastKey = keys.pop();
const lastLevelObject = keys.reduce((obj, key) => obj[key] = obj[key] || {}, queryOptions)
lastLevelObject[lastKey] = req.query[param];
}

queryOptions.filter.protocol = req.params.protocol;
queryOptions.filter.protocolPath = req.params[0].replace(leadTailSlashRegex, '');
// the protocol path segment is base64url encoded, as the actual protocol is a URL
// we decode it here in order to filter for the correct protocol
const protocol = Convert.base64Url(req.params.protocol).toString()
queryOptions.filter.protocol = protocol;
queryOptions.filter.protocolPath = req.params[0].replace(leadTailSlashRegex, '');

const query = await RecordsQuery.create({
filter: queryOptions.filter,
pagination: { limit: 1 },
dateSort: DateSort.PublishedDescending
});
const query = await RecordsQuery.create({
filter: queryOptions.filter,
pagination: { limit: 1 },
dateSort: DateSort.PublishedDescending
});

const { entries, status } = await this.dwn.processMessage(req.params.did, query.message);
const { entries, status } = await this.dwn.processMessage(req.params.did, query.message);

if (status.code === 200) {
if (entries[0]) {
const record = await RecordsRead.create({
filter: { recordId: entries[0].recordId },
});
const reply = await this.dwn.processMessage(req.params.did, record.toJSON());
return readReplyHandler(res, reply);
} else {
if (status.code === 200) {
if (entries[0]) {
const record = await RecordsRead.create({
filter: { recordId: entries[0].recordId },
});
const reply = await this.dwn.processMessage(req.params.did, record.toJSON());
return readReplyHandler(res, reply);
} else {
return res.sendStatus(404);
}
} else if (status.code === 401) {
return res.sendStatus(404);
} else {
return res.sendStatus(status.code);
}
} else if (status.code === 401) {
return res.sendStatus(404);
} else {
return res.sendStatus(status.code);
} catch(error) {
log.error(`Error processing request: ${decodeURI(req.url)}`, error);
return res.sendStatus(400);
}
})

this.#api.get('/:did/read/protocols/:protocol', async (req, res) => {
const query = await ProtocolsQuery.create({
filter: { protocol: req.params.protocol }
});
const { entries, status } = await this.dwn.processMessage(req.params.did, query.message);
if (status.code === 200) {
if (entries.length) {
res.status(status.code);
res.json(entries[0]);
} else {
// wrap request in a try-catch block to handle any unexpected errors
try {

// the protocol segment is base64url encoded, as the actual protocol is a URL
// we decode it here in order to filter for the correct protocol
const protocol = Convert.base64Url(req.params.protocol).toString()
const query = await ProtocolsQuery.create({
filter: { protocol }
});
const { entries, status } = await this.dwn.processMessage(req.params.did, query.message);
if (status.code === 200) {
if (entries.length) {
res.status(status.code);
res.json(entries[0]);
} else {
return res.sendStatus(404);
}
} else if (status.code === 401) {
return res.sendStatus(404);
} else {
return res.sendStatus(status.code);
}
} else if (status.code === 401) {
return res.sendStatus(404);
} else {
return res.sendStatus(status.code);
} catch(error) {
log.error(`Error processing request: ${decodeURI(req.url)}`, error);
return res.sendStatus(400);
}
})

Expand Down
33 changes: 17 additions & 16 deletions tests/http-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
} from './utils.js';
import { RegistrationManager } from '../src/registration/registration-manager.js';
import CommonScenarioValidator from './common-scenario-validator.js';
import { Convert } from '@web5/common';

if (!globalThis.crypto) {
// @ts-ignore
Expand Down Expand Up @@ -564,8 +565,8 @@ describe('http api', function () {


// Fetch the protocol definition using the HTTP API
const urlEncodedProtocol = encodeURIComponent(protocolConfigure.message.descriptor.definition.protocol);
const protocolUrl = `http://localhost:3000/${alice.did}/read/protocols/${urlEncodedProtocol}`;
const base64urlEncodedProtocol = Convert.string(protocolConfigure.message.descriptor.definition.protocol).toBase64Url();
const protocolUrl = `http://localhost:3000/${alice.did}/read/protocols/${base64urlEncodedProtocol}`;
const protocolQueryResponse = await fetch(protocolUrl);
expect(protocolQueryResponse.status).to.equal(200);

Expand Down Expand Up @@ -606,8 +607,8 @@ describe('http api', function () {


// Fetch the protocol definition using the HTTP API
const urlEncodedProtocol = encodeURIComponent(protocolConfigure.message.descriptor.definition.protocol);
const protocolUrl = `http://localhost:3000/${alice.did}/read/protocols/${urlEncodedProtocol}`;
const base64urlEncodedProtocol = Convert.string(protocolConfigure.message.descriptor.definition.protocol).toBase64Url();
const protocolUrl = `http://localhost:3000/${alice.did}/read/protocols/${base64urlEncodedProtocol}`;
const protocolQueryResponse = await fetch(protocolUrl);
expect(protocolQueryResponse.status).to.equal(404);
});
Expand Down Expand Up @@ -752,8 +753,8 @@ describe('http api', function () {
expect(responseJson.result.reply.status.code).to.equal(202);

// Fetch the record using the HTTP API
const urlEncodedProtocol = encodeURIComponent(protocolConfigure.message.descriptor.definition.protocol);
const protocolUrl = `http://localhost:3000/${alice.did}/read/protocols/${urlEncodedProtocol}/foo`;
const base64urlEncodedProtocol = Convert.string(protocolConfigure.message.descriptor.definition.protocol).toBase64Url();
const protocolUrl = `http://localhost:3000/${alice.did}/read/protocols/${base64urlEncodedProtocol}/foo`;
const recordReadResponse = await fetch(protocolUrl);
expect(recordReadResponse.status).to.equal(200);

Expand All @@ -771,8 +772,8 @@ describe('http api', function () {
it('removes the trailing slash from the protocol path', async function () {
const recordsQueryCreateSpy = sinon.spy(RecordsQuery, 'create');

const urlEncodedProtocol = encodeURIComponent('http://example.com/protocol');
const protocolUrl = `http://localhost:3000/${alice.did}/read/protocols/${urlEncodedProtocol}/foo/`; // trailing slash
const base64urlEncodedProtocol = Convert.string('http://example.com/protocol').toBase64Url();
const protocolUrl = `http://localhost:3000/${alice.did}/read/protocols/${base64urlEncodedProtocol}/foo/`; // trailing slash
const recordReadResponse = await fetch(protocolUrl);
expect(recordReadResponse.status).to.equal(404);

Expand Down Expand Up @@ -845,16 +846,16 @@ describe('http api', function () {
expect(responseJson.result.reply.status.code).to.equal(202);

// Fetch the record using the HTTP API
const urlEncodedProtocol = encodeURIComponent(protocolConfigure.message.descriptor.definition.protocol);
const protocolUrl = `http://localhost:3000/${alice.did}/read/protocols/${urlEncodedProtocol}/foo`;
const base64urlEncodedProtocol = Convert.string(protocolConfigure.message.descriptor.definition.protocol).toBase64Url();
const protocolUrl = `http://localhost:3000/${alice.did}/read/protocols/${base64urlEncodedProtocol}/foo`;
const recordReadResponse = await fetch(protocolUrl);
expect(recordReadResponse.status).to.equal(404);
});

it('returns a 400 if protocol path is not provided', async function () {
// Fetch a protocol record without providing a protocol path
const urlEncodedProtocol = encodeURIComponent('http://example.com/protocol');
const protocolUrl = `http://localhost:3000/${alice.did}/read/protocols/${urlEncodedProtocol}/`; // missing protocol path
const base64urlEncodedProtocol = Convert.string('http://example.com/protocol').toBase64Url();
const protocolUrl = `http://localhost:3000/${alice.did}/read/protocols/${base64urlEncodedProtocol}/`; // missing protocol path
const recordReadResponse = await fetch(protocolUrl);
expect(recordReadResponse.status).to.equal(400);
expect(await recordReadResponse.text()).to.equal('protocol path is required');
Expand Down Expand Up @@ -961,8 +962,8 @@ describe('http api', function () {
it('verify /info still returns when package.json file does not exist', async function () {
await httpApi.close();

// set up spy to check for an error log by the server
const logSpy = sinon.spy(log, 'error');
// set up spy to check for an info log by the server
const logSpy = sinon.spy(log, 'info');

// set the config to an invalid file path
const packageJsonConfig = config.packageJsonPath;
Expand All @@ -982,8 +983,8 @@ describe('http api', function () {
expect(info['version']).to.be.undefined;

// check the logSpy was called
expect(logSpy.callCount).to.equal(1);
expect(logSpy.args[0][0]).to.contain('could not read `package.json` for version info');
expect(logSpy.callCount).to.be.gt(0);
expect(logSpy.calledWith(sinon.match('could not read `package.json` for version info'))).to.be.true;

// restore old config path
config.packageJsonPath = packageJsonConfig;
Expand Down

0 comments on commit c9b92e8

Please sign in to comment.