Skip to content

Add http middleware and websocket upgrade on existing listener #755

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: public
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
]
}
Loading