Commit 70b15521 authored by Dave Raggett's avatar Dave Raggett

Added first MVP for wot testing tools and web hub implementation

parent 6c30783b
node_modules
npm-debug.log
FROM node:10
# Create app directory
WORKDIR /usr/src/app
# Install app dependencies
# A wildcard is used to ensure both package.json AND package-lock.json are copied
# where available (npm@5+)
COPY package*.json ./
RUN npm install
# If you are building your code for production
# RUN npm install --only=production
# Bundle app source
COPY . .
EXPOSE 8888
CMD [ "npm", "start" ]
This diff is collapsed.
-----BEGIN CERTIFICATE-----
MIIC/zCCAeegAwIBAgIJAKEPILX8t2aQMA0GCSqGSIb3DQEBCwUAMCgxJjAkBgNV
BAMTHW9yY2hlc3RyYXRvci5kZXYuZi1pbnRlcm9wLmV1MB4XDTE4MTAyNDE0NDQx
NloXDTI4MTAyMTE0NDQxNlowKDEmMCQGA1UEAxMdb3JjaGVzdHJhdG9yLmRldi5m
LWludGVyb3AuZXUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqB2eT
oQR0gfbv5wOjnZEOmzcZ962qlg4+h6+DaKw1taKfCAzgRpULMTcix6sNs03lz0wr
LP8ue8xdE80aGhzWMnPRhii4fQ2Gx0pMPg4+T5t19o2UB5ikhw0hOBrUQLoGkjgu
AmoE1qDECdc1sUg44fXOYjGI3H3TbCxzbNCzZ5AYdT8GLN4r0aAVXXSGamf9u7xs
BRBXx4XLS6dB8nn902zNHouHH2UR+nAkKXDauqXQSjpNk3LWMZVGv6xrGb823s3D
BKG5o8AoUh4tNhDhaCxCBFP1TEuMK0FoADIG8bR1FPxwVUGsfTDR9ZH6jQBNkt5z
IWrP6J8/rT5YHzqVAgMBAAGjLDAqMCgGA1UdEQQhMB+CHW9yY2hlc3RyYXRvci5k
ZXYuZi1pbnRlcm9wLmV1MA0GCSqGSIb3DQEBCwUAA4IBAQCXadKP1nNF2KVvRufe
Jz5FQwkBQrNZvSsTFmF2ZmwWtdivXpF47jBB6GzLQvQIcd+flGXsu5IyplZZXVwp
JN8BIO83JYtBreR0r2rZJMqpBFPafKcxaYG0Fd36dJsbbdEMI0vD/Yxr4LgzzujT
ZL3rcTXbGK+ISv8bZS2DiSl1K3+sLI4kIA2xD2eJbzCkmMpcbaFj0IpNAA2XzYiJ
NWsKFv97YyZoMEAohRAFx5U1y/gHMcuh+drTduXLaj6IztL3FYwVQs4MVG8YWAKc
soeiCJgq2RaeoCfIbArFlOQ2KF2HR09Rvxjc704QhseW1aVAglA+T6NcHkiIYloi
CXHh
-----END CERTIFICATE-----
// simple HTTP server - put resources in ./www/
// only supports a limited set of file extensions
// could be easily extended to handle request body
var port = 8282;
var base = 'http://localhost:' + port + '/'; // base URI for models on this server
var http = require("http");
var url = require('url');
var path = require('path');
var fs = require('fs');
var mime_types = {
"html": "text/html",
"txt": "text/plain",
"js": "text/javascript",
"json": "application/json",
"css": "text/css",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"png": "image/png",
"gif": "image/gif",
"ico": "image/x-icon",
"pdf": "application/pdf",
"ttl": "text/turtle",
"rdf": "text/turtle",
"crl": "text/crl",
"csv": "text/plain"
};
http.createServer(function(request, response) {
//console.log('http request');
var uri = url.parse(url.resolve(base, request.url));
//console.log('HTTP request: ' + request.method + ' ' + uri.path);
if (request.method === "GET" || request.method === 'HEAD') {
var prefix = __dirname + '/www';
//console.log('request for "' + uri.path + '"');
// assume it is a request for a file
if (uri.path[uri.path.length-1] === '/')
uri.path += 'index.html';
var filename = decodeURI(prefix + uri.path);
var gzipped = false;
//console.log('filename: ' + filename);
fs.stat(filename, function(error, stat) {
var ext = path.extname(filename);
var mime = null;
if (ext === ".gz") {
gzipped = true;
ext = path.extname(filename.substr(0, filename.length-3));
}
if (ext.length > 1)
mime = mime_types[ext.split(".")[1]];
if (error || !mime) {
console.log("unable to serve " + filename);
console.log("current path is: " + __dirname)
var body = "404 not found: " + request.url;
response.writeHead(404, {
'Content-Type': 'text/plain',
'Content-Length': body.length
});
if (request.method === "GET")
response.write(body);
response.end();
} else {
if (gzipped) {
console.log("filename has mime type " + mime);
response.writeHead(200, {
'Content-Type': mime,
'Content-Encoding': 'gzip',
'Pragma': 'no-cache',
'Cache-Control': 'no-cache',
'Access-Control-Allow-Origin': '*',
'Content-Length': stat.size
});
} else {
response.writeHead(200, {
'Content-Type': mime,
'Pragma': 'no-cache',
'Cache-Control': 'no-cache',
'Access-Control-Allow-Origin': '*',
'Content-Length': stat.size
});
}
if (request.method === "GET") {
var stream = fs.createReadStream(filename);
stream.pipe(response);
} else
response.end();
}
});
} else { // unimplemented HTTP Method
var body = "501: not implemented " + request.method + " " + request.url;
response.writeHead(501, {
'Content-Type': 'text/plain',
'Content-Length': body.length
});
response.write(body);
response.end();
}
}).listen(port);
console.log('started http server on port ' + port + ' at path ' + __dirname);
// test application for Arena Web Hub
let webhub = require('./webhub.js')({
port: 8888,
accountPath: '/account',
accountManager: accountManager,
validateJWT: validateJWT
});
// helper for http server
function fail(status, description, response) {
let body = status + ' ' + description;
console.log(body);
response.writeHead(status, {
'Content-Type': 'text/plain',
'Access-Control-Allow-Origin': '*',
'Content-Length': body.length
});
response.write(body);
response.end();
}
// helper for http server
function succeed(status, description, response) {
let body = status + ' ' + description;
console.log(body);
response.writeHead(status, {
'Content-Type': 'text/plain',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true',
'Content-Length': body.length
});
response.write(body);
response.end();
}
// helper for HTTP request authorisation
function authorised(request) {
// ask app to validate JWT authorisation token
let token = request.headers.authorization;
return (!token || validateJWT(token, request.url));
}
// manager for user accounts and security tokens
function accountManager (request, response) {
// handles all HTTPS requests to the accountPath:
// responsible for adding/removing user accounts,
// logging in and generation of JWT tokens, and
// logging out and handling forgotten passwords
return fail(500, "missing account manager", response);
}
function validateJWT (token, url) {
// dummy function to be replaced by code that
// tests the validity of the JWT token given
// the HTTPS or WSS request's URL
console.log('verifying JWT: "' + token + '" for ' + url);
return true;
}
// expose a test thing for debugging ...
webhub.produce({
name: "test",
types: {
brightness: {
type: "integer",
value: 50,
minimum: 0,
maximum: 100,
description: "brightness %"
}
},
properties: {
on: {
type: "boolean",
value: false,
description: "on/off switch"
},
temperature: {
type: "number",
value: 20,
units: "celsius",
writeable: false,
description: "room temperature"
},
light1: {
type: "brightness",
description: "hall light",
},
light2: {
type: "brightness",
description: "porch light",
},
list: {
type: "array",
minItems: 2,
maxItems: 3,
items: {
type: "number"
}
},
object: {
type: "object",
properties: {
name: {
type: "string"
},
quantity: {
type: "integer"
}
},
required: ["name"]
},
date: {
type: "string",
regex: "([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))",
value: "2018-10-20"
},
counter: {
type: "integer",
value: 0
},
any: {
}
},
actions: {
testInput: {
input: {
type: "number"
}
},
testAlarm: {
},
testSync: {
input: {
type: "object",
properties: {
name: {
type: "string"
},
value: {
type: "number"
}
}
}
},
testAction: {
},
testEmit: {
output: {
type: "boolean"
}
},
testSpeed: {
input: {
type: "integer"
}
}
},
events: {
alarm: {
type: "string"
}
}
}).then(thing => {
console.log("produced thing: " + thing.name);
// configure actions
thing.addActionHandler('testAlarm', input => {
let act = (resolve, reject) => {
let data = 'whoop whoop!';
if (input)
data = input;
thing.events.alarm.emit(data)
resolve(null);
}
return new Promise(function (resolve, reject) {
act(resolve, reject);
});
});
thing.addActionHandler('testSync', input => {
let act = (resolve, reject) => {
let property = thing.properties[input.name];
property.write(input.value);
resolve(null);
}
return new Promise(function (resolve, reject) {
act(resolve, reject);
});
});
thing.addActionHandler('testAction', input => {
let act = (resolve, reject) => {
let action = thing.actions["testInput"];
action.invoke(input)
.then(output => {
resolve(output)
}).catch(err => {
reject(err);
});
}
return new Promise(function (resolve, reject) {
act(resolve, reject);
});
});
thing.addActionHandler('testEmit', input => {
let act = (resolve, reject) => {
let event = thing.events["alarm"];
try {
event.emit(input);
console.log('emit succeeded');
resolve(true);
} catch (err) {
console.log('emit failed');
resolve(false);
}
}
return new Promise(function (resolve, reject) {
act(resolve, reject);
});
});
thing.addActionHandler('testSpeed', input => {
console.log('test speed with ' + input + ' events');
let act = async (resolve, reject) => {
try {
let count = input;
console.log('starting speed test');
if (Number.isInteger(count) && count > 0) {
while (count--) {
await thing.properties['counter'].write(count);
}
}
resolve(null);
} catch(err) {
reject('handler failed: ' + err);
}
}
return new Promise(function (resolve, reject) {
act(resolve, reject);
});
});
/*
// send 3 events at four seconds apart
let count = 1;
let interval = setInterval(function() {
thing.emitEvent('alarm', 'alarm ' + (count++));
if (count > 3)
clearInterval(interval);
}, 4000);
*/
})
.catch(err => console.log("failed to produce thing: " + err));
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqB2eToQR0gfbv
5wOjnZEOmzcZ962qlg4+h6+DaKw1taKfCAzgRpULMTcix6sNs03lz0wrLP8ue8xd
E80aGhzWMnPRhii4fQ2Gx0pMPg4+T5t19o2UB5ikhw0hOBrUQLoGkjguAmoE1qDE
Cdc1sUg44fXOYjGI3H3TbCxzbNCzZ5AYdT8GLN4r0aAVXXSGamf9u7xsBRBXx4XL
S6dB8nn902zNHouHH2UR+nAkKXDauqXQSjpNk3LWMZVGv6xrGb823s3DBKG5o8Ao
Uh4tNhDhaCxCBFP1TEuMK0FoADIG8bR1FPxwVUGsfTDR9ZH6jQBNkt5zIWrP6J8/
rT5YHzqVAgMBAAECggEAZn6EL3f2yYy2oLMvfGe1U4q5Uov9QkGmYIdGg/6LIO1X
FGqz6FZj7hVC2VJniKC3qnqlvbkoMosqDEmtb7ih/XT4YCtxTJUFnGNyJDecOm+e
lSOFAOD7YOKQRaAefChwexmViBaodjYzPzl3Y8R0duWvWTPUDF5t8w7YVNQZkqs1
SR1ZR5N8mplS6vGBxlpGVQg0S/vWo/q1Y15kPFgWbv5lIxZRpQopubcvhMk7sart
SXhaQ3ISvdn9UnkEEqPsGq5KEBERUc/tkCjrDxhdV3FStsYXLYGp+HvF6GMF3mMb
Mg8FkSz9tlhyRww8ikxFxmrSw0337B0i5fT3T5mCmQKBgQDcOXg5z+/qHXQqhn+m
BX6KpocApGKsQHi2CR5jvwSX2KV1U1srCAhsNboAmqt/C4vW0MFJy/9EXWcloqOF
nz9LzmnZpT3D2Utw9MDeA41MClG5H7D3eez4dBUyZPqrEsUYw8FeJIWLxWwApu0I
bbB7kp2Jpa7tZYmPdYDa0jlhTwKBgQDFpnCEkHlYljMsdiy94HMZLoVZc13/KMhA
Wt+mBBzsYG11CTi5uwf/j74Z8vS/DUiN1Rskwy4yl/fJ56SDg8hQmztXYAV0bXjN
5fxBAUrbhYNGOhOB4yNZTQ46bU0NU9bNZmCRX6mdfXsEFM2ziZB0ly9H7q9eSPDD
2YTb3dBE2wKBgFz9Y1e+BscrgrbGLjZTUZiIMq9BumyTmKT8+rkRmoXntA3zkaZ8
8NmIYi8JIGs32+dsJIHdwr8CVaCdqUCt+pMu6KE/VfJR2borjxjwFQTLwrBRwm7t
K/PJSH4MB5CfD1yipA71ivJ2/WDVG3eYoZG8WgsaS9/wJQLPkgZUCGifAoGAMlBD
Nku8yCM8FaZjj1ZSlmd0RKgMloagK1m2swE7B6UoV/GoAgetao4B24MwcG4GOSy1
gy84VGLBDiGsjFoApRxPB9gGq3Oum+NeyoF0t9sN5tOj4Z2bgwENjSwDwE/GT3Uv
QfJzMAcgSJKvJnPvVO6jd5E4DS1ONNzraDjX6p8CgYEAqjlP4OCp0qTY3HWw4r6U
JcKC/W+8qedqaX4XaXoOrgxoVpJ5hfqSI7mcGFIOeUbUNtZXKnkCjIA2zZg2c3MJ
285KFq+YUrJeYR5JUf9d6mBjVCGXYiqX/QcqXlKPAy+XAj+pe+kevC4lDevXEsNB
s3Lx/2BlGFiUNNooj9nRMjg=
-----END PRIVATE KEY-----
{
"name": "arena-webhub",
"version": "0.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"jshashes": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/jshashes/-/jshashes-1.0.7.tgz",
"integrity": "sha1-vtjJeg6WMv0FE5FvVfdt1Uhr5Z8="
}
}
}
{
"name": "arena-webhub",
"version": "0.1.0",
"description": "Compact Web Hub for the Web of Things",
"main": "interop.js",
"scripts": {
"start": "node interop.js"
},
"author": {
"name": "Dave Raggett",
"email": "dave.raggett@gmail.com"
},
"license": "MIT",
"dependencies": {
"jshashes": "1.0.7"
}
}
This diff is collapsed.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Web Hub demo</title>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<!--
<script type="text/javascript" src="../../wot.js"></script>
-->
<script type="text/javascript" src="wot.js"></script>
</head>
<body>
<h1>Web Hub demo</h1>
<p>Uses HTTP for messaging along with Server-Sent Events.</p>
<h2>Properties</h2>
<ul id="plist">
</ul>
<h2>Thing Description</h2>
<pre id="model">
</pre>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<title>Web Hub demo</title>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="wot.js"></script>
<script type="text/javascript" src="interop.js"></script>
<style>
h1 img { height: 2em; position: relative; top: 0.6em; }
pre b { color: #A00 }
</style>
</head>
<body>
<h1><img src="f-interop.png" alt="logo"> F-Interop Interoperability Tests</h1>
<p>This is a test coordinator you can run against different Web of Things platforms</p>
<form name="platform">
<label><input name="hub" type="radio" value="ArenaWebHub-SSE" checked="true">Arena Web Hub with server-sent events</label><br>
<label><input name="hub" type="radio" value="ArenaWebHub-WSS">Arena Web Hub with web sockets</label><br>
<label><input name="hub" type="radio" value="ThingWeb-WS">Siemens ThingWeb with web sockets for events</label><br>
<!--<label><input name="hub" type="radio" value="ThingWeb-LP">Siemens ThingWeb with long polling for events</label><br>-->
<label><input name="hub" type="radio" value="ThingsGateway">Mozilla Things Gateway using web sockets</label>
<p><button name="start">execute tests</button></p>
</form>
<h2>Test Log</h2>
<pre id="log"></pre>
<h2>Model</h2>
<pre id="model"></pre>
<h2>Properties</h2>
<ul id="plist">
</ul>
</body>
</html>
This diff is collapsed.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Web Hub Login Page</title>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="login.js"></script>
<style>
body {
margin: 4em;
font-family: sans-serif;
background-image: url("texture1.jpg");
background-repeat: repeat;
}
h2 {
color: #888;
}
.person {
height: 2.3ex;
vertical-align: bottom;
}
label {
color: #888;
}
input {
width: 42ex;
height: 3ex;
font-size: 2ex;
color: #AAA;
background: #EEE;
}
input:active, input:focus {
border-color: #CFC;
color: black;
}
button {
height: 3ex;
border-radius: 8px;
border: solid 1px #DDE;
font-size: 2ex;
background: #EEF;
}
div {
position: static;
}
</style>
</head>
<body>
<h1>Web Hub Login Page</h1>
<p>Login or create a new account for yourself</p>
<div style="width:50%;background-color:rgb(245,245,250);padding:1em;min-width:45ex;max-width:45ex">
<div>
<form name="login">
<h2><img class="person" src="person.png" alt=""> LOGIN</h2>
<p><label>EMAIL ADDRESS<br>
<input name="email" value="mary.smith@example.org"></label></p>
<p><label>PASSWORD<br>
<input name="password" value="password - may contain spaces"></label></p>
<p><button name="login" title="to create and store the authorisation token">Login</button>&emsp;&emsp;&emsp;&emsp;
<button name="forgot" title="to send you an email with a link for changing your password">Forgot your password?</button>&emsp;&emsp;&emsp;&emsp;
<button name="logout" title="to remove the authorisation token">Logout</button></p>
</form>
</div>
<div>
<h2><img class="person" src="person-plus.png" alt=""> ADD USER?</h2>
<button id="add_user" title="you will be asked for your credentials">Add new account</button>
<form name="register">
<p><label>Full Name<br>
<input name="fullname" value="Mary Smith"></label></p>
<p><label>EMAIL ADDRESS<br>
<input name="email" value="mary.smith@example.org"></label></p>
<p><label>PASSWORD<br>
<input name="password" value="password - may contain spaces"></label></p>
<p><button name="register" title="to send you an email with a link to activate the new account">Register</button>&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;
<button name="cancel">Cancel</button></p>
</form>
<?
</div>
<div id="drop_user">
<h2><img class="person" src="person-minus.png" alt=""> FORGET USER?</h2>
<button id="remove" title="along with the authorisation token">Remove account</button>
</div>
</div>
</body>
</html>
/* Web Hub login & create new account */
const webhub = "https://localhost:8888";
let login = {
start: () => {
let login_form = document.forms['login'];
let register_form = document.forms['register'];
let add_user = document.getElementById('add_user');
let drop_user = document.getElementById('drop_user');
let remove = document.getElementById('remove');
show(register_form, false);
let inputs = document.getElementsByTagName('input');
for (let i = 0; i < inputs.length; ++i) {
let input = inputs[i];
input.guide = input.value;
input.addEventListener('focus', got_focus);
input.addEventListener('blur', lost_focus);
}
login_form.login.onclick = (e) => {
console.log('login');
e.preventDefault();
};
login_form.logout.onclick = (e) => {
console.log('logout');
e.preventDefault();
};
login_form.forgot.onclick = (e) => {
console.log('forgot');
e.preventDefault();
};
register_form.register.onclick = (e) => {
console.log('register');
e.preventDefault();
};
remove.onclick = (e) => {
console.log('remove');
e.preventDefault();
};
add_user.onclick = () => {
show(register_form, true);
show(login_form, false);
show(add_user, false);
show(drop_user, false);
};
register_form.cancel.onclick = (e) => {
show(register_form, false);
show(login_form, true);
show(add_user, true);
show(drop_user, true);
e.preventDefault();
};
},
};
function show(element, visible) {
if (visible) {
console.log('hide ' + element.tagName);
element.style.visibility = "visible";
element.style.display = "";
} else {
console.log('show ' + element.tagName);
element.style.visibility = "hidden";
element.style.display = "none";
}
}