Skip to content

Commit

Permalink
Add http middleware and websocket upgrade on existing listener #755
Browse files Browse the repository at this point in the history
  • Loading branch information
wilberforce authored and mkellner committed Mar 24, 2022
1 parent e515f6c commit 5954fc3
Show file tree
Hide file tree
Showing 20 changed files with 941 additions and 1 deletion.
1 change: 1 addition & 0 deletions contributed/httpbridge/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
33 changes: 33 additions & 0 deletions contributed/httpbridge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Moddable http bridge example

This example shows how to use the http bridge components.

It starts a bi-directional websocket between the Moddable server and a browser. If you start another browser instance, changes on one browser reflect in the other.

### build the zip file
```
cd site
npm install
npm run build
```

### build moddable
From the site folder:
```
npm run mcconfig
```

This will build the `site.zip` and launch the simulator

open browser `http://localhost`

### front end development
```
`npm run dev`
or
`wmr`
open browser `http://localhost:8080`
Edit any ts or css file, and on save the browser will auto-update with changes, and the `websocket` is connected to the simulator Modable server
112 changes: 112 additions & 0 deletions contributed/httpbridge/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright (c) 2016-2021 Moddable Tech, Inc.
* Copyright (c) Wilberforce
*
* This file is part of the Moddable SDK.
*
* This work is licensed under the
* Creative Commons Attribution 4.0 International License.
* To view a copy of this license, visit
* <http://creativecommons.org/licenses/by/4.0>.
* or send a letter to Creative Commons, PO Box 1866,
* Mountain View, CA 94042, USA.
*
*/
import { WebServer as HTTPServer } from "bridge/webserver";
import { BridgeWebsocket } from "bridge/websocket";
import { BridgeHttpZip } from "bridge/httpzip";
import Preference from "preference";

const http = new HTTPServer({
port: 80,
});

let ws = http.use(new BridgeWebsocket("/api"));
http.use(new BridgeHttpZip("site.zip"));

class App {
#preference_domain = "bridge";
#model;

constructor(m) {
this.#model = m;
}

get model() {
return this.#model;
}

set model(m) {
this.#model = m;
}

minus(value) {
this.model.satisfaction = Math.max(0, this.model.satisfaction - 1);
return this.model;
}
plus(value) {
this.model.satisfaction = Math.min(10, this.model.satisfaction + 1);
return this.model;
}
shutdown() {
ws.close();
http.close();
}
language() {
this.model.language = value.language;
return this.model;
}
restore() {
let keys = Preference.keys(this.#preference_domain);
for (let key of keys) {
let pref_settings = Preference.get(this.#preference_domain, key);
if (pref_settings) {
Object.assign(this.model, JSON.parse(pref_settings));
}
}
return this.model;
}
save() {
Preference.set(
this.#preference_domain,
"settings",
JSON.stringify(this.model)
);
}
}

import { _model } from "model";
const app = new App({ ..._model });
app.restore();

ws.callback = function cb(websock, message, value) {
switch (message) {
case BridgeWebsocket.connect:
break;

case BridgeWebsocket.handshake:
websock.broadcast(app.model);
break;

case BridgeWebsocket.receive:
try {
trace(`Main WebSocket receive: ${value}\n`);
value = JSON.parse(value);

let action = value?.action;

if (typeof app[action] === "function") {
value = app[action](value);
} else {
if (value.hasOwnProperty('language')) {
Object.assign(app.model,value)
} else {
trace("No matching action found\n");
}
}
if (value) websock.broadcast(value);
} catch (e) {
trace(`WebSocket parse received data error: ${e}\n`);
}
}
};
24 changes: 24 additions & 0 deletions contributed/httpbridge/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"include": [
"$(MODDABLE)/examples/manifest_base.json",
"$(MODDABLE)/examples/manifest_net.json",
"$(MODULES)/network/http/manifest.json",
"$(MODULES)/network/websocket/manifest.json",
"$(MODULES)/files/zip/manifest.json",
"$(MODULES)/files/preference/manifest.json"
],
"modules": {
"*": [
"./main"
],
"bridge/*": "$(MODDABLE)/contributed/httpbridge/modules/*",
"model":"site/public/model"
},
"preload": [
"model",
"bridge/httpzip"
],
"data": {
"*": "./site/dist/site"
}
}
75 changes: 75 additions & 0 deletions contributed/httpbridge/modules/hotspot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright (c) 2016-2021 Moddable Tech, Inc.
* Copyright (c) Wilberforce
*
* This file is part of the Moddable SDK.
*
* This work is licensed under the
* Creative Commons Attribution 4.0 International License.
* To view a copy of this license, visit
* <http://creativecommons.org/licenses/by/4.0>.
* or send a letter to Creative Commons, PO Box 1866,
* Mountain View, CA 94042, USA.
*
*/

import { Bridge, HTTPServer } from "bridge/webserver";
import Net from "net"

const hotspot = new Map;

// iOS 8/9
hotspot.set("/library/test/success.html",{status: 302,body: "Success"});
hotspot.set("/hotspot-detect.html",{status: 302,body: "Success"});

// Windows
hotspot.set("/ncsi.txt",{status: 302,body: "Microsoft NCSI"});
hotspot.set("/connecttest.txt",{status: 302,body: "Microsoft Connect Test"});
hotspot.set("/redirect",{status: 302,body: ""}); // Win 10

// Android
hotspot.set("/mobile/status.php", {status:302}); // Android 8.0 (Samsung s9+)
hotspot.set("/generate_204", {status:302}); // Android actual redirect
hotspot.set("/gen_204", {status:204}); // Android 9.0

export class BridgeHotspot extends Bridge {
constructor() {
super();
}
handler(req, message, value, etc) {
switch (message) {
case HTTPServer.status:

req.redirect=hotspot.get(value); // value is path
if ( req.redirect) return; // Hotspot url match
delete req.redirect;
return this.next?.handler(req, message, value, etc);
case HTTPServer.header: {
if ( "host" === value ) {
req.host=etc;
trace(`BridgeHotspot: http://${req.host}${req.path}\n`);
}
return this.next?.handler(req, message, value, etc);
}
case HTTPServer.prepareResponse:

if( req.redirect) {
let apIP=Net.get("IP", "ap");
let redirect={
headers: [ "Content-type", "text/plain", "Location",`http://${apIP}`],
...req.redirect
};
trace(`Hotspot match: http://${req.host}${req.path}\n`);
trace(JSON.stringify(redirect),'\n');

return redirect;
}
}
return this.next?.handler(req, message, value, etc);
}
}
Object.freeze(hotspot);

/* TO DO
add dns constructor flag. then becomes self contained.
*/
102 changes: 102 additions & 0 deletions contributed/httpbridge/modules/httpzip.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright (c) 2016-2021 Moddable Tech, Inc.
* Copyright (c) Wilberforce
*
* This file is part of the Moddable SDK.
*
* This work is licensed under the
* Creative Commons Attribution 4.0 International License.
* To view a copy of this license, visit
* <http://creativecommons.org/licenses/by/4.0>.
* or send a letter to Creative Commons, PO Box 1866,
* Mountain View, CA 94042, USA.
*
*/

import { Bridge, HTTPServer } from "bridge/webserver";
import Resource from "Resource";
import {ZIP} from "zip"

const mime = new Map;
mime.set("js", "application/javascript");
mime.set("css", "text/css");
mime.set("ico", "image/vnd.microsoft.icon");
mime.set("txt", "text/plain");
mime.set("htm", "text/html");
mime.set("html", "text/html");
mime.set("svg", "image/svg+xml");
mime.set("png", "image/png");
mime.set("gif", "image/gif");
mime.set("webp", "image/webp");
mime.set("jpg", "image/jpeg");
mime.set("jpeg", "image/jpeg");

export class BridgeHttpZip extends Bridge {
constructor(resource) {
super();

this.archive = new ZIP(new Resource(resource));
}

handler(req, message, value, etc) {
switch (message) {
case HTTPServer.status:
// redirect home page
if (value === '/') value='/index.html';
req.path = value;
try {
req.data = this.archive.file(req.path.slice(1)); // drop leading / to match zip content
req.etag = "mod-" + req.data.crc.toString(16);
}
catch {
delete req.data;
delete req.etag;
return this.next?.handler(req, message, value, etc);
}
break;

case HTTPServer.header:
req.match ||= ("if-none-match" === value) && (req.etag === etc);
return this.next?.handler(req, message, value, etc);

case HTTPServer.prepareResponse:
if (req.match) {
return {
status: 304,
headers: [
"ETag", req.etag,
]
};
}
if (!req.data) {
trace(`prepareResponse: missing file ${req.path}\n`);

return this.next?.handler(req, message, value, etc);
}

req.data.current = 0;
const result = {
headers: [
"Content-type", mime.get(req.path.split('.').pop()) ?? "text/plain",
"Content-length", req.data.length,
"ETag", req.etag,
"Cache-Control", "max-age=60"
],
body: true
}
if (8 === req.data.method) // Compression Method
result.headers.push("Content-Encoding", "deflate");
return result;

case HTTPServer.responseFragment:
if (req.data.current >= req.data.length)
return;

const chunk = req.data.read(ArrayBuffer, (value > 1536) ? 1536 : value);
req.data.current += chunk.byteLength;
return chunk;
}
}
}

Object.freeze(mime);
25 changes: 25 additions & 0 deletions contributed/httpbridge/modules/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"include": [
"$(MODDABLE)/examples/manifest_base.json",
"$(MODDABLE)/examples/manifest_net.json",
"$(MODULES)/network/http/manifest.json",
"$(MODULES)/network/websocket/manifest.json",
"$(MODULES)/files/zip/manifest.json"
],
"modules": {
"*": [
"$(MODULES)/files/resource/*"
],
"dns/server": "$(MODULES)/network/dns/dnsserver"
},
"preload": [
"http",
"dns/server",
"websocket/websocket",
"base64",
"hex",
"logical",
"resource",
"zip"
]
}

0 comments on commit 5954fc3

Please sign in to comment.