Skip to content

Commit 559b592

Browse files
committed
feat(binding-http/http-server): generate id based http paths
fix #1458
1 parent 6bb2c24 commit 559b592

5 files changed

Lines changed: 231 additions & 21 deletions

File tree

packages/binding-http/src/http-server.ts

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export default class HttpServer implements ProtocolServer {
6464
private readonly address?: string;
6565
private readonly baseUri?: string;
6666
private readonly urlRewrite?: Record<string, string>;
67+
private readonly devFriendlyUri: boolean;
6768
private readonly supportedSecuritySchemes: string[] = ["nosec"];
6869
private readonly validOAuthClients: RegExp = /.*/g;
6970
private readonly server: http.Server | https.Server;
@@ -83,6 +84,7 @@ export default class HttpServer implements ProtocolServer {
8384
this.baseUri = config.baseUri;
8485
this.urlRewrite = config.urlRewrite;
8586
this.middleware = config.middleware;
87+
this.devFriendlyUri = config.devFriendlyUri ?? true;
8688

8789
const router = Router({
8890
ignoreTrailingSlash: true,
@@ -267,21 +269,32 @@ export default class HttpServer implements ProtocolServer {
267269
}
268270

269271
public async expose(thing: ExposedThing, tdTemplate: WoT.ExposedThingInit = {}): Promise<void> {
270-
let urlPath = slugify(thing.title, { lower: true });
271-
272-
// avoid URL clashes
273-
if (this.things.has(urlPath)) {
274-
let uniqueUrlPath;
275-
let nameClashCnt = 2;
276-
do {
277-
uniqueUrlPath = urlPath + "_" + nameClashCnt++;
278-
} while (this.things.has(uniqueUrlPath));
279-
urlPath = uniqueUrlPath;
280-
}
281-
282272
if (this.getPort() !== -1) {
283-
debug(`HttpServer on port ${this.getPort()} exposes '${thing.title}' as unique '/${urlPath}'`);
284-
this.things.set(urlPath, thing);
273+
const paths: string[] = [];
274+
// If not id is given we create the path using the title even if devFriendlyUri is false.
275+
// in Thing Description 1.1 id is optional
276+
if (this.devFriendlyUri || thing.id == null) {
277+
let urlPath = slugify(thing.title, { lower: true });
278+
279+
// avoid URL clashes
280+
if (this.things.has(urlPath)) {
281+
let uniqueUrlPath;
282+
let nameClashCnt = 2;
283+
do {
284+
uniqueUrlPath = urlPath + "_" + nameClashCnt++;
285+
} while (this.things.has(uniqueUrlPath));
286+
urlPath = uniqueUrlPath;
287+
}
288+
this.things.set(urlPath, thing);
289+
paths.push(urlPath);
290+
debug("HttpServer on port %d exposes %s as unique '/%s'", this.getPort(), thing.name, urlPath);
291+
}
292+
293+
if (thing.id != null) {
294+
this.things.set(thing.id, thing);
295+
paths.push(thing.id);
296+
debug("HttpServer on port %d exposes %s as unique '/%s'", this.getPort(), thing.name, thing.id);
297+
}
285298

286299
if (this.scheme === "http" && Object.keys(thing.securityDefinitions).length !== 0) {
287300
warn(`HTTP Server will attempt to use your security schemes even if you are not using HTTPS.`);
@@ -290,16 +303,20 @@ export default class HttpServer implements ProtocolServer {
290303
this.fillSecurityScheme(thing);
291304

292305
if (this.baseUri !== undefined) {
293-
const base: string = this.baseUri.concat("/", encodeURIComponent(urlPath));
294-
info("HttpServer TD hrefs using baseUri " + this.baseUri);
295-
this.addEndpoint(thing, tdTemplate, base);
306+
for (const path of paths) {
307+
info("HttpServer TD hrefs using baseUri %s and path %s", this.baseUri, path);
308+
const base: string = this.baseUri.concat("/", encodeURIComponent(path));
309+
this.addEndpoint(thing, tdTemplate, base);
310+
}
296311
} else {
297312
// fill in binding data
298313
for (const address of Helpers.getAddresses()) {
299-
const base: string =
300-
this.scheme + "://" + address + ":" + this.getPort() + "/" + encodeURIComponent(urlPath);
301-
302-
this.addEndpoint(thing, tdTemplate, base);
314+
for (const path of paths) {
315+
const base: string =
316+
this.scheme + "://" + address + ":" + this.getPort() + "/" + encodeURIComponent(path);
317+
info("HttpServer TD hrefs using address %s and path %s", address, path);
318+
this.addEndpoint(thing, tdTemplate, base);
319+
}
303320
}
304321
}
305322
}
@@ -311,6 +328,7 @@ export default class HttpServer implements ProtocolServer {
311328
for (const [name, thing] of this.things.entries()) {
312329
if (thing.id === thingId) {
313330
this.things.delete(name);
331+
this.things.delete(thingId);
314332
info(`HttpServer successfully destroyed '${thing.title}'`);
315333

316334
return true;

packages/binding-http/src/http.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export interface HttpConfig {
4545
serverKey?: string;
4646
serverCert?: string;
4747
security?: SecurityScheme[];
48+
devFriendlyUri?: boolean;
4849
middleware?: MiddlewareRequestHandler;
4950
}
5051

packages/binding-http/test/http-server-test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,4 +1020,53 @@ class HttpServerTest {
10201020

10211021
return httpServer.stop();
10221022
}
1023+
1024+
@test async "should expose Thing with id and title and be reachable from both"() {
1025+
const httpServer = new HttpServer({ port: 0 });
1026+
1027+
await httpServer.start(new Servient());
1028+
1029+
const testThing = new ExposedThing(new Servient(), {
1030+
title: "TestThing",
1031+
id: "urn:dev:wot:test-thing-1234",
1032+
properties: {
1033+
test: {
1034+
type: "string",
1035+
forms: [],
1036+
},
1037+
},
1038+
actions: {
1039+
test: {
1040+
output: { type: "string" },
1041+
forms: [],
1042+
},
1043+
},
1044+
});
1045+
1046+
await httpServer.expose(testThing);
1047+
1048+
const uriByTitle = `http://localhost:${httpServer.getPort()}/testthing`;
1049+
const uriById = `http://localhost:${httpServer.getPort()}/urn:dev:wot:test-thing-1234`;
1050+
1051+
let resp;
1052+
resp = await (await fetch(uriByTitle)).json();
1053+
expect(resp.title).to.be.eq("TestThing");
1054+
expect(resp.properties.test.forms.some((form: { href: string }) => form.href.includes("testthing"))).to.be.true;
1055+
expect(resp.actions.test.forms.some((form: { href: string }) => form.href.includes("testthing"))).to.be.true;
1056+
1057+
resp = await (await fetch(uriById)).json();
1058+
expect(resp.id).to.be.eq("urn:dev:wot:test-thing-1234");
1059+
expect(
1060+
resp.properties.test.forms.some((form: { href: string }) =>
1061+
form.href.includes(encodeURIComponent("urn:dev:wot:test-thing-1234"))
1062+
)
1063+
).to.be.true;
1064+
expect(
1065+
resp.actions.test.forms.some((form: { href: string }) =>
1066+
form.href.includes(encodeURIComponent("urn:dev:wot:test-thing-1234"))
1067+
)
1068+
).to.be.true;
1069+
1070+
return httpServer.stop();
1071+
}
10231072
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const version = "0.9.2" as const;
2+
export default version;
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
const schema = {
2+
definitions: {},
3+
$schema: "http://json-schema.org/draft-07/schema#",
4+
type: "object",
5+
properties: {
6+
$schema: { type: "string" },
7+
servient: {
8+
type: "object",
9+
properties: {
10+
clientOnly: {
11+
description: "setting if no servers shall be started",
12+
type: "boolean",
13+
default: false,
14+
},
15+
staticAddress: {
16+
type: "string",
17+
description: "hostname or IP literal for static address config",
18+
},
19+
},
20+
additionalProperties: false,
21+
},
22+
http: {
23+
type: "object",
24+
properties: {
25+
port: {
26+
type: "integer",
27+
},
28+
address: {
29+
type: "string",
30+
},
31+
baseUri: {
32+
type: "string",
33+
},
34+
urlRewrite: {
35+
type: "object",
36+
description: "map (from URL -> to URL) defining HTTP URL rewrites",
37+
additionalProperties: { type: "string" },
38+
},
39+
proxy: {
40+
type: "object",
41+
description:
42+
"object with 'href' field for the proxy URI, scheme field for either 'basic' or 'bearer', and corresponding credential fields as defined below",
43+
required: ["href"],
44+
properties: {
45+
href: {
46+
type: "string",
47+
},
48+
scheme: {
49+
enum: ["basic", "bearer"],
50+
},
51+
token: {
52+
type: "string",
53+
},
54+
username: {
55+
type: "string",
56+
},
57+
password: {
58+
type: "string",
59+
},
60+
},
61+
additionalProperties: false,
62+
},
63+
allowSelfSigned: {
64+
description: "whether self-signed certificates should be allowed",
65+
type: "boolean",
66+
},
67+
serverKey: {
68+
type: "string",
69+
},
70+
serverCert: {
71+
type: "string",
72+
},
73+
},
74+
additionalProperties: false,
75+
},
76+
mqtt: {
77+
type: "object",
78+
properties: {
79+
broker: {
80+
type: "string",
81+
},
82+
username: {
83+
type: "string",
84+
},
85+
password: {
86+
type: "string",
87+
},
88+
clientId: {
89+
type: "string",
90+
},
91+
protocolVersion: {
92+
type: "integer",
93+
enum: [3, 4, 5],
94+
default: 5,
95+
},
96+
},
97+
additionalProperties: false,
98+
},
99+
coap: {
100+
type: "object",
101+
properties: {
102+
port: {
103+
type: "integer",
104+
},
105+
},
106+
additionalProperties: false,
107+
},
108+
credentials: {
109+
type: "object",
110+
patternProperties: {
111+
"^THING_ID([a-zA-Z0-9_]+)$": {
112+
type: "object",
113+
properties: {
114+
token: {
115+
type: "string",
116+
},
117+
username: {
118+
type: "string",
119+
},
120+
password: {
121+
type: "string",
122+
},
123+
},
124+
},
125+
},
126+
additionalProperties: false,
127+
},
128+
log: {
129+
type: "object",
130+
properties: {
131+
level: {
132+
type: "string",
133+
enum: ["debug", "info", "warn", "error"],
134+
},
135+
},
136+
},
137+
},
138+
additionalProperties: false,
139+
} as const;
140+
export default schema;

0 commit comments

Comments
 (0)