Skip to content

Commit cd96f50

Browse files
author
Diane Huxley
authored
Reject messages with nonnormalize protocol URIs (#317)
* Reject messages with nonnormalize protocol URIs * PR comments * Lint * Fix README + PR comment
1 parent ec94bcd commit cd96f50

File tree

19 files changed

+343
-22
lines changed

19 files changed

+343
-22
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Decentralized Web Node (DWN) SDK
44

55
Code Coverage
6-
![Statements](https://img.shields.io/badge/statements-93.86%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-93.39%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-91.54%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-93.86%25-brightgreen.svg?style=flat)
6+
![Statements](https://img.shields.io/badge/statements-93.95%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-93.46%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-91.69%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-93.95%25-brightgreen.svg?style=flat)
77

88
## Introduction
99

src/core/dwn-error.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,7 @@ export enum DwnErrorCode {
2626
RecordsInvalidAncestorKeyDerivationSegment = 'RecordsInvalidAncestorKeyDerivationSegment',
2727
RecordsWriteGetEntryIdUndefinedAuthor = 'RecordsWriteGetEntryIdUndefinedAuthor',
2828
RecordsWriteValidateIntegrityEncryptionCidMismatch = 'RecordsWriteValidateIntegrityEncryptionCidMismatch',
29-
Secp256k1KeyNotValid = 'Secp256k1KeyNotValid'
29+
Secp256k1KeyNotValid = 'Secp256k1KeyNotValid',
30+
UrlProtocolNotNormalized = 'UrlProtocolNotNormalized',
31+
UrlPrococolNotNormalizable = 'UriPrococolNotNormalizable'
3032
};

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export type { EventLog, Event } from './event-log/event-log.js';
88
export type { EventsGetMessage, EventsGetReply } from './interfaces/events/types.js';
99
export type { HooksWriteMessage } from './interfaces/hooks/types.js';
1010
export type { MessagesGetMessage, MessagesGetReply } from './interfaces/messages/types.js';
11-
export type { ProtocolDefinition, ProtocolRuleSet, ProtocolsConfigureMessage, ProtocolsQueryMessage } from './interfaces/protocols/types.js';
11+
export type { ProtocolDefinition, ProtocolRuleSet, ProtocolsQueryFilter, ProtocolsConfigureMessage, ProtocolsQueryMessage } from './interfaces/protocols/types.js';
1212
export type { RecordsDeleteMessage, RecordsQueryMessage, RecordsWriteMessage } from './interfaces/records/types.js';
1313
export { AllowAllTenantGate, TenantGate } from './core/tenant-gate.js';
1414
export { Cid } from './utils/cid.js';

src/interfaces/protocols/messages/protocols-configure.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getCurrentTimeInHighPrecision } from '../../../utils/time.js';
55
import { validateAuthorizationIntegrity } from '../../../core/auth.js';
66

77
import { DwnInterfaceName, DwnMethodName, Message } from '../../../core/message.js';
8+
import { normalizeProtocolUri, validateProtocolUriNormalized } from '../../../utils/url.js';
89

910
export type ProtocolsConfigureOptions = {
1011
dateCreated? : string;
@@ -17,6 +18,7 @@ export class ProtocolsConfigure extends Message<ProtocolsConfigureMessage> {
1718

1819
public static async parse(message: ProtocolsConfigureMessage): Promise<ProtocolsConfigure> {
1920
await validateAuthorizationIntegrity(message);
21+
validateProtocolUriNormalized(message.descriptor.protocol);
2022

2123
return new ProtocolsConfigure(message);
2224
}
@@ -26,7 +28,7 @@ export class ProtocolsConfigure extends Message<ProtocolsConfigureMessage> {
2628
interface : DwnInterfaceName.Protocols,
2729
method : DwnMethodName.Configure,
2830
dateCreated : options.dateCreated ?? getCurrentTimeInHighPrecision(),
29-
protocol : options.protocol,
31+
protocol : normalizeProtocolUri(options.protocol),
3032
definition : options.definition // TODO: #139 - move definition out of the descriptor - https://github.com/TBD54566975/dwn-sdk-js/issues/139
3133
};
3234

src/interfaces/protocols/messages/protocols-query.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
import type { SignatureInput } from '../../../jose/jws/general/types.js';
2-
import type { ProtocolsQueryDescriptor, ProtocolsQueryMessage } from '../types.js';
2+
import type { ProtocolsQueryDescriptor, ProtocolsQueryFilter, ProtocolsQueryMessage } from '../types.js';
33

44
import { getCurrentTimeInHighPrecision } from '../../../utils/time.js';
55
import { removeUndefinedProperties } from '../../../utils/object.js';
66
import { validateAuthorizationIntegrity } from '../../../core/auth.js';
7-
87
import { DwnInterfaceName, DwnMethodName, Message } from '../../../core/message.js';
8+
import { normalizeProtocolUri, validateProtocolUriNormalized } from '../../../utils/url.js';
99

1010
export type ProtocolsQueryOptions = {
1111
dateCreated?: string;
12-
filter?: {
13-
protocol: string;
14-
}
12+
filter?: ProtocolsQueryFilter,
1513
authorizationSignatureInput: SignatureInput;
1614
};
1715

@@ -20,6 +18,10 @@ export class ProtocolsQuery extends Message<ProtocolsQueryMessage> {
2018
public static async parse(message: ProtocolsQueryMessage): Promise<ProtocolsQuery> {
2119
await validateAuthorizationIntegrity(message);
2220

21+
if (message.descriptor.filter !== undefined) {
22+
validateProtocolUriNormalized(message.descriptor.filter.protocol);
23+
}
24+
2325
return new ProtocolsQuery(message);
2426
}
2527

@@ -28,7 +30,7 @@ export class ProtocolsQuery extends Message<ProtocolsQueryMessage> {
2830
interface : DwnInterfaceName.Protocols,
2931
method : DwnMethodName.Query,
3032
dateCreated : options.dateCreated ?? getCurrentTimeInHighPrecision(),
31-
filter : options.filter,
33+
filter : ProtocolsQuery.normalizeFilter(options.filter),
3234
};
3335

3436
// delete all descriptor properties that are `undefined` else the code will encounter the following IPLD issue when attempting to generate CID:
@@ -43,4 +45,15 @@ export class ProtocolsQuery extends Message<ProtocolsQueryMessage> {
4345
const protocolsQuery = new ProtocolsQuery(message);
4446
return protocolsQuery;
4547
}
48+
49+
private static normalizeFilter(filter: ProtocolsQueryFilter | undefined): ProtocolsQueryFilter | undefined {
50+
if (filter === undefined) {
51+
return undefined;
52+
}
53+
54+
return {
55+
...filter,
56+
protocol: normalizeProtocolUri(filter.protocol),
57+
};
58+
}
4659
}

src/interfaces/protocols/types.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@ export type ProtocolsConfigureMessage = BaseMessage & {
4141
descriptor: ProtocolsConfigureDescriptor;
4242
};
4343

44+
export type ProtocolsQueryFilter = {
45+
protocol: string,
46+
};
47+
4448
export type ProtocolsQueryDescriptor = {
4549
interface : DwnInterfaceName.Protocols,
4650
method: DwnMethodName.Query;
4751
dateCreated: string;
48-
filter?: {
49-
protocol: string;
50-
}
52+
filter?: ProtocolsQueryFilter
5153
};
5254

5355
export type ProtocolsQueryMessage = BaseMessage & {

src/interfaces/records/messages/records-query.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Message } from '../../../core/message.js';
77
import { removeUndefinedProperties } from '../../../utils/object.js';
88
import { validateAuthorizationIntegrity } from '../../../core/auth.js';
99
import { DwnInterfaceName, DwnMethodName } from '../../../core/message.js';
10+
import { normalizeProtocolUri, validateProtocolUriNormalized } from '../../../utils/url.js';
1011

1112
export enum DateSort {
1213
CreatedAscending = 'createdAscending',
@@ -26,6 +27,11 @@ export class RecordsQuery extends Message<RecordsQueryMessage> {
2627

2728
public static async parse(message: RecordsQueryMessage): Promise<RecordsQuery> {
2829
await validateAuthorizationIntegrity(message);
30+
31+
if (message.descriptor.filter?.protocol !== undefined) {
32+
validateProtocolUriNormalized(message.descriptor.filter.protocol);
33+
}
34+
2935
return new RecordsQuery(message);
3036
}
3137

@@ -34,7 +40,7 @@ export class RecordsQuery extends Message<RecordsQueryMessage> {
3440
interface : DwnInterfaceName.Records,
3541
method : DwnMethodName.Query,
3642
dateCreated : options.dateCreated ?? getCurrentTimeInHighPrecision(),
37-
filter : options.filter,
43+
filter : RecordsQuery.normalizerFilter(options.filter),
3844
dateSort : options.dateSort
3945
};
4046

@@ -94,4 +100,18 @@ export class RecordsQuery extends Message<RecordsQueryMessage> {
94100

95101
return filterCopy as Filter;
96102
}
103+
104+
public static normalizerFilter(filter: RecordsQueryFilter): RecordsQueryFilter {
105+
let protocol;
106+
if (filter.protocol === undefined) {
107+
protocol = undefined;
108+
} else {
109+
protocol = normalizeProtocolUri(filter.protocol);
110+
}
111+
112+
return {
113+
...filter,
114+
protocol,
115+
};
116+
}
97117
}

src/interfaces/records/messages/records-write.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ import { ProtocolAuthorization } from '../../../core/protocol-authorization.js';
2424
import { Records } from '../../../utils/records.js';
2525
import { removeUndefinedProperties } from '../../../utils/object.js';
2626
import { Secp256k1 } from '../../../utils/secp256k1.js';
27-
2827
import { authorize, validateAuthorizationIntegrity } from '../../../core/auth.js';
2928
import { Cid, computeCid } from '../../../utils/cid.js';
3029
import { DwnError, DwnErrorCode } from '../../../core/dwn-error.js';
3130
import { DwnInterfaceName, DwnMethodName } from '../../../core/message.js';
31+
import { normalizeProtocolUri, validateProtocolUriNormalized } from '../../../utils/url.js';
3232

3333
export type RecordsWriteOptions = {
3434
recipient?: string;
@@ -161,7 +161,7 @@ export class RecordsWrite extends Message<RecordsWriteMessage> {
161161
const descriptor: RecordsWriteDescriptor = {
162162
interface : DwnInterfaceName.Records,
163163
method : DwnMethodName.Write,
164-
protocol : options.protocol,
164+
protocol : options.protocol !== undefined ? normalizeProtocolUri(options.protocol) : undefined,
165165
protocolPath : options.protocolPath,
166166
recipient : options.recipient!,
167167
schema : options.schema,
@@ -368,6 +368,10 @@ export class RecordsWrite extends Message<RecordsWriteMessage> {
368368
);
369369
}
370370
}
371+
372+
if (this.message.descriptor.protocol !== undefined) {
373+
validateProtocolUriNormalized(this.message.descriptor.protocol);
374+
}
371375
}
372376

373377
/**
@@ -500,7 +504,7 @@ export class RecordsWrite extends Message<RecordsWriteMessage> {
500504
/**
501505
* Creates the `attestation` property of a RecordsWrite message if given signature inputs; returns `undefined` otherwise.
502506
*/
503-
private static async createAttestation(descriptorCid: string, signatureInputs?: SignatureInput[]): Promise<GeneralJws | undefined> {
507+
public static async createAttestation(descriptorCid: string, signatureInputs?: SignatureInput[]): Promise<GeneralJws | undefined> {
504508
if (signatureInputs === undefined || signatureInputs.length === 0) {
505509
return undefined;
506510
}
@@ -515,7 +519,7 @@ export class RecordsWrite extends Message<RecordsWriteMessage> {
515519
/**
516520
* Creates the `authorization` property of a RecordsWrite message.
517521
*/
518-
private static async createAuthorization(
522+
public static async createAuthorization(
519523
recordId: string,
520524
contextId: string | undefined,
521525
descriptorCid: string,

src/utils/url.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
2+
3+
4+
export function validateProtocolUriNormalized(protocol: string): void {
5+
let normalized: string | undefined;
6+
try {
7+
normalized = normalizeProtocolUri(protocol);
8+
} catch {
9+
normalized = undefined;
10+
}
11+
12+
if (protocol !== normalized) {
13+
throw new DwnError(DwnErrorCode.UrlProtocolNotNormalized, 'Protocol URI must be normalized.');
14+
}
15+
}
16+
17+
export function normalizeProtocolUri(url: string): string {
18+
let fullUrl: string;
19+
if (/^[^:]+:\/\/./.test(url)) {
20+
fullUrl = url;
21+
} else {
22+
fullUrl = `http://${url}`;
23+
}
24+
25+
try {
26+
const result = new URL(fullUrl);
27+
result.search = '';
28+
result.hash = '';
29+
return removeTrailingSlash(result.href);
30+
} catch (e) {
31+
throw new DwnError(DwnErrorCode.UrlPrococolNotNormalizable, 'Could not normalize protocol URI');
32+
}
33+
}
34+
35+
function removeTrailingSlash(str: string): string {
36+
if (str.endsWith('/')) {
37+
return str.slice(0, -1);
38+
} else {
39+
return str;
40+
}
41+
}

tests/interfaces/protocols/handlers/protocols-configure.spec.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { MessageStoreLevel } from '../../../../src/store/message-store-level.js'
1414
import { TestDataGenerator } from '../../../utils/test-data-generator.js';
1515
import { TestStubGenerator } from '../../../utils/test-stub-generator.js';
1616

17-
import { DidResolver, Dwn, Encoder, Jws } from '../../../../src/index.js';
17+
import { DidResolver, Dwn, DwnErrorCode, Encoder, Jws } from '../../../../src/index.js';
1818

1919
chai.use(chaiAsPromised);
2020

@@ -140,6 +140,30 @@ describe('ProtocolsConfigureHandler.handle()', () => {
140140
expect(actualDefinition).to.equal(expectedDefinition);
141141
});
142142

143+
it('should return 400 if protocol is not normalized', async () => {
144+
const alice = await DidKeyResolver.generate();
145+
146+
// query for non-normalized protocol
147+
const protocolsConfig = await TestDataGenerator.generateProtocolsConfigure({
148+
requester : alice,
149+
protocol : 'example.com/',
150+
});
151+
152+
// overwrite protocol because #create auto-normalizes protocol
153+
protocolsConfig.message.descriptor.protocol = 'example.com/';
154+
155+
// Re-create auth because we altered the descriptor after signing
156+
protocolsConfig.message.authorization = await Message.signAsAuthorization(
157+
protocolsConfig.message.descriptor,
158+
Jws.createSignatureInput(alice)
159+
);
160+
161+
// Send records write message
162+
const reply = await dwn.processMessage(alice.did, protocolsConfig.message);
163+
expect(reply.status.code).to.equal(400);
164+
expect(reply.status.detail).to.contain(DwnErrorCode.UrlProtocolNotNormalized);
165+
});
166+
143167
describe('event log', () => {
144168
it('should add event for ProtocolsConfigure', async () => {
145169
const alice = await DidKeyResolver.generate();

0 commit comments

Comments
 (0)