Skip to content

Commit 7b60140

Browse files
committed
Add http middleware and websocket upgrade on existing listener
1 parent 2f93df2 commit 7b60140

File tree

8 files changed

+547
-10
lines changed

8 files changed

+547
-10
lines changed

modules/network/http/http.js

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export class Request {
115115
}
116116

117117
close() {
118-
this.socket?.close();
118+
if( 16 & this.flags == 0) this.socket?.close();
119119
delete this.socket;
120120
delete this.buffers;
121121
delete this.callback;
@@ -452,7 +452,7 @@ function done(error = false, data) {
452452
state:
453453
454454
0 - connecting
455-
1 - receieving request status
455+
1 - receiving request status
456456
2 - receiving request headers
457457
458458
@@ -661,8 +661,10 @@ function server(message, value, etc) {
661661
const status = response?.status ?? 200;
662662
const message = response?.reason?.toString() ?? reason(status);
663663
const parts = ["HTTP/1.1 ", status.toString(), " ", message, "\r\n",
664-
"connection: ", "close\r\n"];
665-
664+
"connection: ", "close\r\n"];
665+
if ( status === 101 ) {
666+
this.flags = 16;
667+
}
666668
if (response) {
667669
let byteLength;
668670

@@ -682,11 +684,13 @@ function server(message, value, etc) {
682684
this.flags = 4;
683685
}
684686
else {
685-
this.flags = 1;
686-
let count = 0;
687-
if (this.body)
688-
count = ("string" === typeof this.body) ? this.body.length : this.body.byteLength; //@@ utf-8 hell
689-
parts.push("content-length: ", count.toString(), "\r\n");
687+
if ( this.flags !== 16 ) {
688+
this.flags = 1;
689+
let count = 0;
690+
if (this.body)
691+
count = ("string" === typeof this.body) ? this.body.length : this.body.byteLength; //@@ utf-8 hell
692+
parts.push("content-length: ", count.toString(), "\r\n");
693+
}
690694
}
691695
}
692696
else
@@ -702,6 +706,11 @@ function server(message, value, etc) {
702706
if (count > (socket.write() - ((2 & this.flags) ? 8 : 0)))
703707
return;
704708
}
709+
710+
if ( this.flags === 16 ) {
711+
this.state = 10;
712+
return;
713+
}
705714
}
706715
if (8 === this.state) {
707716
let body = this.body;
@@ -741,7 +750,7 @@ function server(message, value, etc) {
741750
}
742751
finally {
743752
this.server.connections.splice(this.server.connections.indexOf(this), 1);
744-
this.close();
753+
if ( this.flags !== 16 ) this.close();
745754
}
746755
}
747756
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright (c) 2016-2021 Moddable Tech, Inc.
3+
* Copyright (c) Wilberforce
4+
*
5+
* This file is part of the Moddable SDK.
6+
*
7+
* This work is licensed under the
8+
* Creative Commons Attribution 4.0 International License.
9+
* To view a copy of this license, visit
10+
* <http://creativecommons.org/licenses/by/4.0>.
11+
* or send a letter to Creative Commons, PO Box 1866,
12+
* Mountain View, CA 94042, USA.
13+
*
14+
*/
15+
16+
import { Middleware, Server } from "middleware/server";
17+
import Net from "net"
18+
19+
const hotspot = new Map;
20+
21+
// iOS 8/9
22+
hotspot.set("/library/test/success.html",{status: 302,body: "Success"});
23+
hotspot.set("/hotspot-detect.html",{status: 302,body: "Success"});
24+
25+
// Windows
26+
hotspot.set("/ncsi.txt",{status: 302,body: "Microsoft NCSI"});
27+
hotspot.set("/connecttest.txt",{status: 302,body: "Microsoft Connect Test"});
28+
hotspot.set("/redirect",{status: 302,body: ""}); // Win 10
29+
30+
// Android
31+
hotspot.set("/mobile/status.php", {status:302}); // Android 8.0 (Samsung s9+)
32+
hotspot.set("/generate_204", {status:302}); // Android actual redirect
33+
hotspot.set("/gen_204", {status:204}); // Android 9.0
34+
35+
export class MiddlewareHotspot extends Middleware {
36+
constructor() {
37+
super();
38+
}
39+
handler(req, message, value, etc) {
40+
switch (message) {
41+
case Server.status:
42+
43+
req.redirect=hotspot.get(value); // value is path
44+
if ( req.redirect) return; // Hotspot url match
45+
delete req.redirect;
46+
return this.next?.handler(req, message, value, etc);
47+
case Server.header: {
48+
if ( "host" === value ) {
49+
req.host=etc;
50+
trace(`MiddlewareHotspot: http://${req.host}${req.path}\n`);
51+
}
52+
return this.next?.handler(req, message, value, etc);
53+
}
54+
case Server.prepareResponse:
55+
56+
if( req.redirect) {
57+
let apIP=Net.get("IP", "ap");
58+
let redirect={
59+
headers: [ "Content-type", "text/plain", "Location",`http://${apIP}`],
60+
...req.redirect
61+
};
62+
trace(`Hotspot match: http://${req.host}${req.path}\n`);
63+
trace(JSON.stringify(redirect),'\n');
64+
65+
return redirect;
66+
}
67+
}
68+
return this.next?.handler(req, message, value, etc);
69+
}
70+
}
71+
Object.freeze(hotspot);
72+
73+
/* TO DO
74+
add dns constructor flag. then becomes self contained.
75+
*/
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"modules": {
3+
"*": [
4+
"$(MODULES)/files/zip/*",
5+
"$(MODULES)/network/http/*",
6+
"$(MODULES)/data/base64/*",
7+
"$(MODULES)/data/hex/*",
8+
"$(MODULES)/data/logical/*"
9+
10+
],
11+
"dns/server": "$(MODULES)/network/dns/dnsserver",
12+
"websocket/websocket": "$(MODULES)/network/websocket/websocket"
13+
},
14+
"preload": [
15+
"http",
16+
"dns/server",
17+
"websocket/websocket",
18+
"base64",
19+
"hex",
20+
"logical",
21+
"websocket"
22+
]
23+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (c) 2016-2021 Moddable Tech, Inc.
3+
* Copyright (c) Wilberforce
4+
*
5+
* This file is part of the Moddable SDK.
6+
*
7+
* This work is licensed under the
8+
* Creative Commons Attribution 4.0 International License.
9+
* To view a copy of this license, visit
10+
* <http://creativecommons.org/licenses/by/4.0>.
11+
* or send a letter to Creative Commons, PO Box 1866,
12+
* Mountain View, CA 94042, USA.
13+
*
14+
*/
15+
16+
import { Middleware, Server } from "middleware/server";
17+
18+
// Single Page application. Map page requests back to /
19+
20+
export class MiddlewareRewriteSPA extends Middleware {
21+
#routes
22+
constructor(routes) {
23+
super();
24+
this.#routes=routes
25+
}
26+
27+
handler(req, message, value, etc) {
28+
29+
switch (message) {
30+
case Server.status:
31+
if ( this.#routes.includes(value) ) { // To do: possibly use Set?
32+
trace(`Rewrite: ${value}\n`);
33+
value='/';
34+
trace(`Rewrite now: ${value}\n`);
35+
}
36+
break;
37+
}
38+
return this.next?.handler(req, message, value, etc);
39+
}
40+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright (c) 2016-2021 Moddable Tech, Inc.
3+
* Copyright (c) Wilberforce
4+
*
5+
* This file is part of the Moddable SDK.
6+
*
7+
* This work is licensed under the
8+
* Creative Commons Attribution 4.0 International License.
9+
* To view a copy of this license, visit
10+
* <http://creativecommons.org/licenses/by/4.0>.
11+
* or send a letter to Creative Commons, PO Box 1866,
12+
* Mountain View, CA 94042, USA.
13+
*
14+
*/
15+
16+
import {Server} from "http"
17+
18+
class Middleware {
19+
#next;
20+
constructor() {
21+
this.#next = null
22+
}
23+
get next() {
24+
return this.#next;
25+
}
26+
set next(n) {
27+
this.#next = n;
28+
}
29+
}
30+
31+
class WebServer extends Server {
32+
#last;
33+
#first;
34+
35+
constructor(options) {
36+
super(options);
37+
38+
this.#first = null;
39+
this.#last = null;
40+
}
41+
42+
use( handler ) {
43+
// chaining here, join new to previous
44+
if( this.#first === null ) {
45+
this.#first=this.#last=handler;
46+
} else {
47+
this.#last.next=handler;
48+
this.#last=handler;
49+
}
50+
// Advise middleware of http parent - needed by websocket for connection management
51+
if ( 'parent' in handler )
52+
handler.parent=this;
53+
}
54+
55+
callback(message, value, etc) {
56+
switch (message) {
57+
case Server.status:
58+
this.path=value;
59+
this.method = etc;
60+
break;
61+
case Server.header:
62+
value=value.toLowerCase(); // Header field names are case-insensitive - force lower for easy compare
63+
}
64+
return this.server.#first?.handler(this, message, value, etc);
65+
}
66+
67+
close() {
68+
if ( this.connections )
69+
super.close(); // Close any http connections
70+
}
71+
}
72+
73+
Object.freeze(WebServer.prototype);
74+
75+
export { WebServer as Server, Middleware };
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright (c) 2016-2021 Moddable Tech, Inc.
3+
* Copyright (c) Wilberforce
4+
*
5+
* This file is part of the Moddable SDK.
6+
*
7+
* This work is licensed under the
8+
* Creative Commons Attribution 4.0 International License.
9+
* To view a copy of this license, visit
10+
* <http://creativecommons.org/licenses/by/4.0>.
11+
* or send a letter to Creative Commons, PO Box 1866,
12+
* Mountain View, CA 94042, USA.
13+
*
14+
*/
15+
16+
import { Middleware, Server } from "middleware/server";
17+
import {ZIP} from "zip"
18+
19+
const mime = new Map;
20+
mime.set("js", "application/javascript");
21+
mime.set("css", "text/css");
22+
mime.set("ico", "image/vnd.microsoft.icon");
23+
mime.set("txt", "text/plain");
24+
mime.set("htm", "text/html");
25+
mime.set("html", "text/html");
26+
mime.set("svg", "image/svg+xml");
27+
mime.set("png", "image/png");
28+
mime.set("gif", "image/gif");
29+
mime.set("webp", "image/webp");
30+
mime.set("jpg", "image/jpeg");
31+
mime.set("jpeg", "image/jpeg");
32+
33+
export class MiddlewareStaticZip extends Middleware {
34+
constructor(archive) {
35+
super();
36+
37+
this.archive = new ZIP(archive);
38+
}
39+
40+
handler(req, message, value, etc) {
41+
switch (message) {
42+
case Server.status:
43+
// redirect home page
44+
if (value === '/') value='/index.html';
45+
req.path = value;
46+
try {
47+
req.data = this.archive.file(req.path.slice(1)); // drop leading / to match zip content
48+
req.etag = "mod-" + req.data.crc.toString(16);
49+
}
50+
catch {
51+
delete req.data;
52+
delete req.etag;
53+
return this.next?.handler(req, message, value, etc);
54+
}
55+
break;
56+
57+
case Server.header:
58+
req.match ||= ("if-none-match" === value) && (req.etag === etc);
59+
return this.next?.handler(req, message, value, etc);
60+
61+
case Server.prepareResponse:
62+
if (req.match) {
63+
return {
64+
status: 304,
65+
headers: [
66+
"ETag", req.etag,
67+
]
68+
};
69+
}
70+
if (!req.data) {
71+
trace(`prepareResponse: missing file ${req.path}\n`);
72+
73+
return this.next?.handler(req, message, value, etc);
74+
}
75+
76+
req.data.current = 0;
77+
const result = {
78+
headers: [
79+
"Content-type", mime.get(req.path.split('.').pop()) ?? "text/plain",
80+
"Content-length", req.data.length,
81+
"ETag", req.etag,
82+
"Cache-Control", "max-age=60"
83+
],
84+
body: true
85+
}
86+
if (8 === req.data.method) // Compression Method
87+
result.headers.push("Content-Encoding", "deflate");
88+
return result;
89+
90+
case Server.responseFragment:
91+
if (req.data.current >= req.data.length)
92+
return;
93+
94+
const chunk = req.data.read(ArrayBuffer, (value > 1536) ? 1536 : value);
95+
req.data.current += chunk.byteLength;
96+
return chunk;
97+
}
98+
}
99+
}
100+
101+
Object.freeze(mime);

0 commit comments

Comments
 (0)