Merge pull request #255 from hybridgroup/feature/extract-out-api
Extract API from core
This commit is contained in:
commit
25f12345b9
123
lib/api.js
123
lib/api.js
|
@ -1,123 +0,0 @@
|
|||
/*
|
||||
* Cylon API
|
||||
* cylonjs.com
|
||||
*
|
||||
* Copyright (c) 2013-2015 The Hybrid Group
|
||||
* Licensed under the Apache 2.0 license.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var fs = require("fs"),
|
||||
path = require("path");
|
||||
|
||||
var express = require("express"),
|
||||
bodyParser = require("body-parser");
|
||||
|
||||
var Logger = require("./logger"),
|
||||
_ = require("./lodash");
|
||||
|
||||
var API = module.exports = function API(opts) {
|
||||
if (opts == null) {
|
||||
opts = {};
|
||||
}
|
||||
|
||||
_.forEach(this.defaults, function(def, name) {
|
||||
this[name] = _.has(opts, name) ? opts[name] : def;
|
||||
}, this);
|
||||
|
||||
this.createServer();
|
||||
|
||||
this.express.set("title", "Cylon API Server");
|
||||
|
||||
this.express.use(this.setupAuth());
|
||||
|
||||
this.express.use(bodyParser.json());
|
||||
this.express.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
this.express.use(express["static"](__dirname + "/../node_modules/robeaux/"));
|
||||
|
||||
// set CORS headers for API requests
|
||||
this.express.use(function(req, res, next) {
|
||||
res.set("Access-Control-Allow-Origin", this.CORS || "*");
|
||||
res.set("Access-Control-Allow-Headers", "Content-Type");
|
||||
res.set("Content-Type", "application/json");
|
||||
return next();
|
||||
}.bind(this));
|
||||
|
||||
// extracts command params from request
|
||||
this.express.use(function(req, res, next) {
|
||||
req.commandParams = _.values(req.body);
|
||||
return next();
|
||||
});
|
||||
|
||||
// load route definitions
|
||||
this.express.use("/api", require("./api/routes"));
|
||||
|
||||
// error handling
|
||||
this.express.use(function(err, req, res, next) {
|
||||
res.status(500).json({ error: err.message || "An error occured."});
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
API.prototype.defaults = {
|
||||
host: "127.0.0.1",
|
||||
port: "3000",
|
||||
auth: false,
|
||||
CORS: "",
|
||||
ssl: {
|
||||
key: path.normalize(__dirname + "/api/ssl/server.key"),
|
||||
cert: path.normalize(__dirname + "/api/ssl/server.crt")
|
||||
}
|
||||
};
|
||||
|
||||
API.prototype.createServer = function createServer() {
|
||||
this.express = express();
|
||||
|
||||
//configure ssl if requested
|
||||
if (this.ssl && typeof(this.ssl) === "object") {
|
||||
var https = require("https");
|
||||
|
||||
this.server = https.createServer({
|
||||
key: fs.readFileSync(this.ssl.key),
|
||||
cert: fs.readFileSync(this.ssl.cert)
|
||||
}, this.express);
|
||||
} else {
|
||||
var str = "API using insecure connection. ";
|
||||
str += "We recommend using an SSL certificate with Cylon.";
|
||||
|
||||
Logger.warn(str);
|
||||
this.server = this.express;
|
||||
}
|
||||
};
|
||||
|
||||
API.prototype.setupAuth = function setupAuth() {
|
||||
var authfn = function auth(req, res, next) { next(); };
|
||||
|
||||
if (!!this.auth && typeof(this.auth) === "object" && this.auth.type) {
|
||||
var type = this.auth.type,
|
||||
module = "./api/auth/" + type,
|
||||
filename = path.normalize(__dirname + "/" + module + ".js"),
|
||||
exists = fs.existsSync(filename);
|
||||
|
||||
if (exists) {
|
||||
authfn = require(filename)(this.auth);
|
||||
}
|
||||
}
|
||||
|
||||
return authfn;
|
||||
};
|
||||
|
||||
API.prototype.listen = function() {
|
||||
this.server.listen(this.port, this.host, null, function() {
|
||||
var title = this.express.get("title");
|
||||
var protocol = this.ssl ? "https" : "http",
|
||||
str;
|
||||
|
||||
str = "Listening at " + protocol + "://" + this.host + ":" + this.port;
|
||||
|
||||
Logger.info(title + " is now online.");
|
||||
Logger.info(str);
|
||||
}.bind(this));
|
||||
};
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* Cylon API - Basic Auth
|
||||
* cylonjs.com
|
||||
*
|
||||
* Copyright (c) 2013-2015 The Hybrid Group
|
||||
* Licensed under the Apache 2.0 license.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var http = require("http");
|
||||
|
||||
var unauthorized = function unauthorized(res) {
|
||||
res.statusCode = 401;
|
||||
res.setHeader("WWW-Authenticate", "Basic realm=\"Authorization Required\"");
|
||||
res.end("Unauthorized");
|
||||
};
|
||||
|
||||
var error = function error(code, msg){
|
||||
var err = new Error(msg || http.STATUS_CODES[code]);
|
||||
err.status = code;
|
||||
return err;
|
||||
};
|
||||
|
||||
module.exports = function(config) {
|
||||
var user = config.user,
|
||||
pass = config.pass;
|
||||
|
||||
return function auth(req, res, next) {
|
||||
var authorization = req.headers.authorization;
|
||||
|
||||
if (!authorization) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
// malformed
|
||||
var parts = authorization.split(" ");
|
||||
|
||||
if ("basic" !== parts[0].toLowerCase() || !parts[1]) {
|
||||
return next(error(400));
|
||||
}
|
||||
|
||||
authorization = parts[1];
|
||||
|
||||
// credentials
|
||||
authorization = new Buffer(authorization, "base64").toString();
|
||||
authorization = authorization.match(/^([^:]+):(.+)$/);
|
||||
|
||||
if (!authorization) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
if (authorization[1] === user && authorization[2] === pass) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return unauthorized(res);
|
||||
};
|
||||
};
|
|
@ -1,187 +0,0 @@
|
|||
/* jshint maxlen: false */
|
||||
|
||||
/*
|
||||
* Cylon API - Route Definitions
|
||||
* cylonjs.com
|
||||
*
|
||||
* Copyright (c) 2013-2015 The Hybrid Group
|
||||
* Licensed under the Apache 2.0 license.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var Cylon = require("../cylon");
|
||||
|
||||
var router = module.exports = require("express").Router();
|
||||
|
||||
var eventHeaders = {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Connection": "keep-alive",
|
||||
"Cache-Control": "no-cache"
|
||||
};
|
||||
|
||||
// Loads up the appropriate Robot/Device/Connection instances, if they are
|
||||
// present in the route params.
|
||||
var load = function load(req, res, next) {
|
||||
var robot = req.params.robot,
|
||||
device = req.params.device,
|
||||
connection = req.params.connection;
|
||||
|
||||
if (robot) {
|
||||
req.robot = Cylon.robots[robot];
|
||||
if (!req.robot) {
|
||||
return res.status(404).json({
|
||||
error: "No Robot found with the name " + robot
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (device) {
|
||||
req.device = req.robot.devices[device];
|
||||
if (!req.device) {
|
||||
return res.status(404).json({
|
||||
error: "No device found with the name " + device
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (connection) {
|
||||
req.connection = req.robot.connections[connection];
|
||||
if (!req.connection) {
|
||||
return res.status(404).json({
|
||||
error: "No connection found with the name " + connection
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
router.get("/", function(req, res) {
|
||||
res.json({ MCP: Cylon.toJSON() });
|
||||
});
|
||||
|
||||
router.get("/events", function(req, res) {
|
||||
res.json({ events: Cylon.events });
|
||||
});
|
||||
|
||||
router.get("/events/:event", function(req, res) {
|
||||
var event = req.params.event;
|
||||
|
||||
res.writeHead(200, eventHeaders);
|
||||
|
||||
var writeData = function(data) {
|
||||
res.write("data: " + JSON.stringify(data) + "\n\n");
|
||||
};
|
||||
|
||||
Cylon.on(event, writeData);
|
||||
|
||||
res.on("close", function() {
|
||||
Cylon.removeListener(event, writeData);
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/commands", function(req, res) {
|
||||
res.json({ commands: Object.keys(Cylon.commands) });
|
||||
});
|
||||
|
||||
router.post("/commands/:command", function(req, res) {
|
||||
var command = Cylon.commands[req.params.command];
|
||||
router.runCommand(req, res, Cylon, command);
|
||||
});
|
||||
|
||||
router.get("/robots", function(req, res) {
|
||||
res.json({ robots: Cylon.toJSON().robots });
|
||||
});
|
||||
|
||||
router.get("/robots/:robot", load, function(req, res) {
|
||||
res.json({ robot: req.robot });
|
||||
});
|
||||
|
||||
router.get("/robots/:robot/commands", load, function(req, res) {
|
||||
res.json({ commands: Object.keys(req.robot.commands) });
|
||||
});
|
||||
|
||||
router.all("/robots/:robot/commands/:command", load, function(req, res) {
|
||||
var command = req.robot.commands[req.params.command];
|
||||
router.runCommand(req, res, req.robot, command);
|
||||
});
|
||||
|
||||
router.get("/robots/:robot/events", load, function(req, res) {
|
||||
res.json({ events: req.robot.events });
|
||||
});
|
||||
|
||||
router.all("/robots/:robot/events/:event", load, function(req, res) {
|
||||
var event = req.params.event;
|
||||
|
||||
res.writeHead(200, eventHeaders);
|
||||
|
||||
var writeData = function(data) {
|
||||
res.write("data: " + JSON.stringify(data) + "\n\n");
|
||||
};
|
||||
|
||||
req.robot.on(event, writeData);
|
||||
|
||||
res.on("close", function() {
|
||||
req.robot.removeListener(event, writeData);
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/robots/:robot/devices", load, function(req, res) {
|
||||
res.json({ devices: req.robot.toJSON().devices });
|
||||
});
|
||||
|
||||
router.get("/robots/:robot/devices/:device", load, function(req, res) {
|
||||
res.json({ device: req.device });
|
||||
});
|
||||
|
||||
router.get("/robots/:robot/devices/:device/events", load, function(req, res) {
|
||||
res.json({ events: req.device.events });
|
||||
});
|
||||
|
||||
router.get("/robots/:robot/devices/:device/events/:event", load, function(req, res) {
|
||||
var event = req.params.event;
|
||||
|
||||
res.writeHead(200, eventHeaders);
|
||||
|
||||
var writeData = function(data) {
|
||||
res.write("data: " + JSON.stringify(data) + "\n\n");
|
||||
};
|
||||
|
||||
req.device.on(event, writeData);
|
||||
|
||||
res.on("close", function() {
|
||||
req.device.removeListener(event, writeData);
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/robots/:robot/devices/:device/commands", load, function(req, res) {
|
||||
res.json({ commands: Object.keys(req.device.commands) });
|
||||
});
|
||||
|
||||
router.all("/robots/:robot/devices/:device/commands/:command", load, function(req, res) {
|
||||
var command = req.device.commands[req.params.command];
|
||||
router.runCommand(req, res, req.device, command);
|
||||
});
|
||||
|
||||
router.get("/robots/:robot/connections", load, function(req, res) {
|
||||
res.json({ connections: req.robot.toJSON().connections });
|
||||
});
|
||||
|
||||
router.get("/robots/:robot/connections/:connection", load, function(req, res) {
|
||||
res.json({ connection: req.connection });
|
||||
});
|
||||
|
||||
// Run an MCP, Robot, or Device command. Process the results immediately,
|
||||
// or asynchronously if the result is a Promise.
|
||||
router.runCommand = function(req, res, my, command) {
|
||||
var result = command.apply(my, req.commandParams);
|
||||
if (typeof result.then === "function") {
|
||||
result
|
||||
.then(function(result) {return res.json({result: result});})
|
||||
.catch(function(err) {return res.status(500).json({error: err});});
|
||||
}
|
||||
else {
|
||||
res.json({ result: result });
|
||||
}
|
||||
};
|
|
@ -1,22 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDnDCCAoQCCQDMuSNl5mThYDANBgkqhkiG9w0BAQUFADCBjzELMAkGA1UEBhMC
|
||||
VVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFDASBgNVBAcTC0xvcyBBbmdlbGVzMRkw
|
||||
FwYDVQQKExBUaGUgSHlicmlkIEdyb3VwMRIwEAYDVQQDEwlsb2NhbGhvc3QxJjAk
|
||||
BgkqhkiG9w0BCQEWF2N5bG9uanNAaHlicmlkZ3JvdXAuY29tMB4XDTE0MDQwMzIx
|
||||
MjczM1oXDTE1MDQwMzIxMjczM1owgY8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpD
|
||||
YWxpZm9ybmlhMRQwEgYDVQQHEwtMb3MgQW5nZWxlczEZMBcGA1UEChMQVGhlIEh5
|
||||
YnJpZCBHcm91cDESMBAGA1UEAxMJbG9jYWxob3N0MSYwJAYJKoZIhvcNAQkBFhdj
|
||||
eWxvbmpzQGh5YnJpZGdyb3VwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
|
||||
AQoCggEBALhobzDaWlLBwPUFBZRxQt5fsvxFzkGJ8TxD8Uhp4r5P4Idqa2DKaz7q
|
||||
oIob/l96t9Szcxi+nib4Cykxt7rw4mmDJFCS+9aV8+u/aFCXjEicCSPkM95f6NOD
|
||||
a3JvHWeFfYeQOZq0uDS9PTccXpomtvX7ufdmYmPpDNzUcJD+FA8+5tVdtbvoF5aV
|
||||
su4Ufb7CcoE9KuyeWm7UQpjaKuoYVAa/9eCHQfptwf0iPPlW4NcS5JG0CJOqyyrF
|
||||
YC30MIlE6/tol8TCPFrbA4HLdtqBKMkOfhSYor12OBKazVWWqk+AkuDQFfXA/OFk
|
||||
L3VGVFjnUh/RUjCoZQ+CeKAM7grUyUcCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEA
|
||||
S4knU+SUXmCw+V8ZggGoLW3OelI6vMq+7ThhGeS+ge4ZkQRuqBdJxK7HytC6bysC
|
||||
L9qQuJsKoEPSDoi48ml7XqRH+kXD4lTpRtDSF8WetLvIjh6lJM5I9xLmevBZYeJT
|
||||
9a+gm6eS0oZBm3/cHpUJnQAw8M8wYmHB/d/WdNu7fV9m5+PBzvwhxVlVb3yaOdsH
|
||||
vRh0BZU58NmFcscfV/pTSqTlp59CQGMynHsaaLiKiLje04v+b+wwob/7kuvZvQ9D
|
||||
kYQkg1EnCGMFIk1Slj1GIS/ewD90JTATJa38wshg2VAQiX8sxqsUPdye/MjheWhF
|
||||
hxMSOB6xNhHjBiOQRwtGtw==
|
||||
-----END CERTIFICATE-----
|
|
@ -1,18 +0,0 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIC9jCCAd4CAQAwgY8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh
|
||||
MRQwEgYDVQQHEwtMb3MgQW5nZWxlczEZMBcGA1UEChMQVGhlIEh5YnJpZCBHcm91
|
||||
cDESMBAGA1UEAxMJbG9jYWxob3N0MSYwJAYJKoZIhvcNAQkBFhdjeWxvbmpzQGh5
|
||||
YnJpZGdyb3VwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALho
|
||||
bzDaWlLBwPUFBZRxQt5fsvxFzkGJ8TxD8Uhp4r5P4Idqa2DKaz7qoIob/l96t9Sz
|
||||
cxi+nib4Cykxt7rw4mmDJFCS+9aV8+u/aFCXjEicCSPkM95f6NODa3JvHWeFfYeQ
|
||||
OZq0uDS9PTccXpomtvX7ufdmYmPpDNzUcJD+FA8+5tVdtbvoF5aVsu4Ufb7CcoE9
|
||||
KuyeWm7UQpjaKuoYVAa/9eCHQfptwf0iPPlW4NcS5JG0CJOqyyrFYC30MIlE6/to
|
||||
l8TCPFrbA4HLdtqBKMkOfhSYor12OBKazVWWqk+AkuDQFfXA/OFkL3VGVFjnUh/R
|
||||
UjCoZQ+CeKAM7grUyUcCAwEAAaAhMB8GCSqGSIb3DQEJAjESExBUaGUgSHlicmlk
|
||||
IEdyb3VwMA0GCSqGSIb3DQEBBQUAA4IBAQC1ap6IIWrAg6ZGmMev4Ef7qGLNSAYV
|
||||
8jg5pG63AHGkZLCMw1ZcT1iZPjOZC1mBzJBy0z9C8fo0ekGvMmmQbQ40i4JgmYH4
|
||||
bmzL4+ulySgiR45DokHkRtyZOa2f2/nOcvVpIDF3EwH7L2yb4AFrCHDwfh8TKl2x
|
||||
oMpy00F23vn7loDxyMkIcR8It+VL1NiJM+TJN9OzGIh0FkJHs2lbRawS30xyxIYy
|
||||
z0yImdzHG5rXGlPS1aeeVue4H9DfinCICSfd0Bx9iJCqzUD98cmK8x4hVezD9r32
|
||||
/6jm88UzF2WgJvuxcQ8EQRiPGCFcLHGB2AFryTWdt6ibCW6gsoRinnvZ
|
||||
-----END CERTIFICATE REQUEST-----
|
|
@ -1,27 +0,0 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAuGhvMNpaUsHA9QUFlHFC3l+y/EXOQYnxPEPxSGnivk/gh2pr
|
||||
YMprPuqgihv+X3q31LNzGL6eJvgLKTG3uvDiaYMkUJL71pXz679oUJeMSJwJI+Qz
|
||||
3l/o04Nrcm8dZ4V9h5A5mrS4NL09Nxxemia29fu592ZiY+kM3NRwkP4UDz7m1V21
|
||||
u+gXlpWy7hR9vsJygT0q7J5abtRCmNoq6hhUBr/14IdB+m3B/SI8+Vbg1xLkkbQI
|
||||
k6rLKsVgLfQwiUTr+2iXxMI8WtsDgct22oEoyQ5+FJiivXY4EprNVZaqT4CS4NAV
|
||||
9cD84WQvdUZUWOdSH9FSMKhlD4J4oAzuCtTJRwIDAQABAoIBAQCwIqgZrGXbZ88q
|
||||
+On0eB4bkqK9zNsNxHjTTD35IZH+nwLhtObtI0o+ZRKD9+sGPYu6sNA9kUw0AnV+
|
||||
mktYVl6b0zPrdgjvVHkP8tnrKGVIsSkVzBEy1L7o0Dzfp3wZdeqJgltTBkxvq1T9
|
||||
/63oZRQabZ6ZzIQr09yCTLNb+iMkzxuJSbdwVAws+oKqSTVoUKY/Gee66MsJjKkr
|
||||
FfqwS/tVfTFWXp9CamWz/zBDyUXnrtUhzNxE+3LPbeZ/sMq/V/f1F5jiQNjenMGl
|
||||
K/qD3HyNbR0P4xWYCe/4/QLSWjsiZHg51d4Nioegii2775ya9qDvtTfUeNNdvCf6
|
||||
HMcvUIPxAoGBAOF5pSl/9VqRc1FXS+Kr8cs7DORZ6EPXKzUZL59V0NHoMgKbk+sM
|
||||
JZv4TeDx26eBoecT2UYWSYMbKLl7sM9YZDJJhgwMeqa47PHk5Cua/Dpfus/pDl67
|
||||
S5FT/gyA2tpSpULml2G7YegdPnTRRNQdppcrY5v9LQ9CSPhbSAPY/FspAoGBANFf
|
||||
gs6JEq46SOMBw+66svwaT2DQJNchwtCKE+2m7OdNufrtTSAVlKlNpGQQxElwYYg1
|
||||
xkEA+l7MycXIkSWOGC4JfLSRYlBgYOFzCQCME9rTkboAXn8Iru9p9vnRnfsEOUk4
|
||||
Vr8/ehzv4gcf90ZJcnPkQNu5lTkr+s/bNDjAof7vAoGARN3zvU4w8V21nCWOrwgX
|
||||
jRxXHrP7RiVFNC2iJwd+BW7nP3anYkZOgmn/13HnxizI95xPY6HRCDNWZ/jIkzwL
|
||||
NnTQdYOmPqAC9wsTSeJHoci1dWVYl0SbmyLNWKJOthpCEcH+gMJL8CpmdiWo4STB
|
||||
SjDddrqIdb2oLfsrbslqoqkCgYAUJV6OxP25Kf6NaUQTGn/SZi2xIRYKZUM7ka2t
|
||||
NlyhPQdiL6c2KR1u1Pu2bS6V6mxYEOSMqK1upcHceBoPRQbqlxsavMp69WsdBlad
|
||||
aN0YNzdUcGinTIyYmNec3iCXYKaqdvNR36e+VQ6opNjEOJj8sb/T5J2JLMQrb+os
|
||||
c8yinQKBgQCcww6Mr0Wnbw/E9RrwYJqheFWXjdrvbhs5leudzHknL/WXfroOccFr
|
||||
cduywWcHkGq9ig0fay/3iVS5WRAlVqDOGmf60It60ZE0CEDVsHd3w9BEAeJxkq+6
|
||||
vnMZ73dIUKmOozNg0+xK7XsY2YCNdYUaFtKH/ENfsCWfu7r2Kli46A==
|
||||
-----END RSA PRIVATE KEY-----
|
40
lib/cylon.js
40
lib/cylon.js
|
@ -30,7 +30,7 @@ Cylon.IO = {
|
|||
Utils: require("./io/utils")
|
||||
};
|
||||
|
||||
Cylon.api_instance = null;
|
||||
Cylon.apiInstances = [];
|
||||
|
||||
Cylon.robots = {};
|
||||
Cylon.commands = {};
|
||||
|
@ -70,20 +70,42 @@ Cylon.robot = function robot(opts) {
|
|||
return bot;
|
||||
};
|
||||
|
||||
// Public: Creates a new API based on passed options
|
||||
// Public: Initializes an API instance based on provided options.
|
||||
//
|
||||
// Returns nothing
|
||||
Cylon.api = function api(opts) {
|
||||
if (typeof opts === "object") {
|
||||
this.config({ api: opts });
|
||||
Cylon.api = function api(Server, opts) {
|
||||
// if only passed options (or nothing), assume HTTP server
|
||||
if (Server == null || _.isObject(Server) && !_.isFunction(Server)) {
|
||||
opts = Server;
|
||||
Server = "http";
|
||||
}
|
||||
|
||||
var API = require("./api");
|
||||
opts = opts || {};
|
||||
|
||||
var config = Utils.fetch(Config, "api", {});
|
||||
if (_.isString(Server)) {
|
||||
var req = "cylon-api-" + Server;
|
||||
|
||||
this.api_instance = new API(config);
|
||||
this.api_instance.listen();
|
||||
try {
|
||||
Server = require(req);
|
||||
} catch (e) {
|
||||
if (e.code === "MODULE_NOT_FOUND") {
|
||||
Logger.error("Cannot find the " + req + " API module.");
|
||||
Logger.error(
|
||||
"You may be able to install it: `npm install " + req + "`"
|
||||
);
|
||||
|
||||
return;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
opts.mcp = this;
|
||||
|
||||
var instance = new Server(opts);
|
||||
this.apiInstances.push(instance);
|
||||
instance.listen();
|
||||
};
|
||||
|
||||
// Public: Starts up the API and the robots
|
||||
|
|
|
@ -44,9 +44,6 @@
|
|||
|
||||
"dependencies": {
|
||||
"async": "0.9.0",
|
||||
"express": "4.10.5",
|
||||
"body-parser": "1.10.0",
|
||||
"robeaux": "0.3.0",
|
||||
"lodash": "2.4.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,179 +0,0 @@
|
|||
/* jshint expr:true */
|
||||
"use strict";
|
||||
|
||||
var https = require("https"),
|
||||
path = require("path");
|
||||
|
||||
var API = source("api"),
|
||||
Logger = source("logger");
|
||||
|
||||
var MockRequest = require("../support/mock_request"),
|
||||
MockResponse = require("../support/mock_response");
|
||||
|
||||
describe("API", function() {
|
||||
var api;
|
||||
|
||||
beforeEach(function() {
|
||||
api = new API();
|
||||
});
|
||||
|
||||
describe("constructor", function() {
|
||||
var mod;
|
||||
|
||||
beforeEach(function() {
|
||||
mod = new API({
|
||||
host: "0.0.0.0",
|
||||
port: "1234"
|
||||
});
|
||||
});
|
||||
|
||||
it("sets @express to an Express instance", function() {
|
||||
expect(api.express.listen).to.be.a("function");
|
||||
});
|
||||
|
||||
it("sets default values", function() {
|
||||
expect(api.host).to.be.eql("127.0.0.1");
|
||||
expect(api.port).to.be.eql("3000");
|
||||
});
|
||||
|
||||
it("overrides default values if passed to constructor", function() {
|
||||
expect(mod.host).to.be.eql("0.0.0.0");
|
||||
expect(mod.port).to.be.eql("1234");
|
||||
});
|
||||
|
||||
it("sets the server title", function() {
|
||||
var title = api.express.get("title");
|
||||
expect(title).to.be.eql("Cylon API Server");
|
||||
});
|
||||
});
|
||||
|
||||
describe("default", function() {
|
||||
var d = API.prototype.defaults;
|
||||
|
||||
it("host", function() {
|
||||
expect(d.host).to.be.eql("127.0.0.1");
|
||||
});
|
||||
|
||||
it("port", function() {
|
||||
expect(d.port).to.be.eql("3000");
|
||||
});
|
||||
|
||||
it("auth", function() {
|
||||
expect(d.auth).to.be.eql(false);
|
||||
});
|
||||
|
||||
it("CORS", function() {
|
||||
expect(d.CORS).to.be.eql("");
|
||||
});
|
||||
|
||||
it("ssl", function() {
|
||||
var sslDir = path.normalize(__dirname + "/../../lib/api/ssl/");
|
||||
expect(d.ssl.key).to.be.eql(sslDir + "server.key");
|
||||
expect(d.ssl.cert).to.be.eql(sslDir + "server.crt");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#createServer", function() {
|
||||
it("sets @express to an express server", function() {
|
||||
api.express = null;
|
||||
api.createServer();
|
||||
expect(api.express).to.be.a("function");
|
||||
});
|
||||
|
||||
context("if SSL is configured", function() {
|
||||
it("sets @server to a https.Server instance", function() {
|
||||
api.createServer();
|
||||
expect(api.server).to.be.an.instanceOf(https.Server);
|
||||
});
|
||||
});
|
||||
|
||||
context("if SSL is not configured", function() {
|
||||
beforeEach(function() {
|
||||
api.ssl = false;
|
||||
stub(Logger, "warn");
|
||||
api.createServer();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
Logger.warn.restore();
|
||||
});
|
||||
|
||||
it("logs that the API is insecure", function() {
|
||||
expect(Logger.warn).to.be.calledWithMatch("insecure connection");
|
||||
});
|
||||
|
||||
it("sets @server to @express", function() {
|
||||
expect(api.server).to.be.eql(api.express);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#setupAuth", function() {
|
||||
context("when auth.type is basic", function() {
|
||||
beforeEach(function() {
|
||||
api.auth = { type: "basic", user: "user", pass: "pass" };
|
||||
});
|
||||
|
||||
it("returns a basic auth middleware function", function() {
|
||||
var fn = api.setupAuth(),
|
||||
req = new MockRequest(),
|
||||
res = new MockResponse(),
|
||||
next = spy();
|
||||
|
||||
var auth = "Basic " + new Buffer("user:pass").toString("base64");
|
||||
|
||||
req.headers.authorization = auth;
|
||||
|
||||
fn(req, res, next);
|
||||
expect(next).to.be.called;
|
||||
});
|
||||
});
|
||||
|
||||
context("when auth is null", function() {
|
||||
beforeEach(function() {
|
||||
api.auth = null;
|
||||
});
|
||||
|
||||
it("returns a pass-through middleware function", function() {
|
||||
var fn = api.setupAuth(),
|
||||
next = spy();
|
||||
|
||||
fn(null, null, next);
|
||||
expect(next).to.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#listen", function() {
|
||||
beforeEach(function() {
|
||||
// we create a plain HTTP server to avoid a log message from Node
|
||||
api.ssl = false;
|
||||
api.createServer();
|
||||
api.express.set("title", "Cylon API Server");
|
||||
|
||||
stub(api.server, "listen").yields();
|
||||
stub(Logger, "info");
|
||||
|
||||
api.listen();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
api.server.listen.restore();
|
||||
Logger.info.restore();
|
||||
});
|
||||
|
||||
it("listens on the configured host and port", function() {
|
||||
expect(api.server.listen).to.be.calledWith("3000", "127.0.0.1");
|
||||
});
|
||||
|
||||
context("when the server is running", function() {
|
||||
it("logs that it's online and listening", function() {
|
||||
var online = "Cylon API Server is now online.",
|
||||
listening = "Listening at http://127.0.0.1:3000";
|
||||
|
||||
expect(Logger.info).to.be.calledWith(online);
|
||||
expect(Logger.info).to.be.calledWith(listening);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,130 +0,0 @@
|
|||
/* jshint expr:true */
|
||||
"use strict";
|
||||
|
||||
var basic = source("api/auth/basic");
|
||||
|
||||
var MockRequest = require("../../../support/mock_request"),
|
||||
MockResponse = require("../../../support/mock_response");
|
||||
|
||||
describe("Basic Auth", function() {
|
||||
var opts = { user: "user", pass: "pass" },
|
||||
req,
|
||||
res,
|
||||
next;
|
||||
|
||||
basic = basic(opts);
|
||||
|
||||
beforeEach(function() {
|
||||
req = new MockRequest();
|
||||
res = new MockResponse();
|
||||
next = spy();
|
||||
|
||||
var auth = new Buffer("user:pass", "utf8").toString("base64");
|
||||
req.headers = { authorization: "Basic " + auth };
|
||||
});
|
||||
|
||||
var checkUnauthorized = function() {
|
||||
var result;
|
||||
|
||||
beforeEach(function() {
|
||||
result = basic(req, res, next);
|
||||
});
|
||||
|
||||
it("returns false", function() {
|
||||
expect(result).to.be.falsy;
|
||||
});
|
||||
|
||||
it("sends a 401 error", function() {
|
||||
expect(res.statusCode).to.be.eql(401);
|
||||
expect(res.end).to.be.calledWith("Unauthorized");
|
||||
});
|
||||
};
|
||||
|
||||
var checkError = function() {
|
||||
var result;
|
||||
|
||||
beforeEach(function() {
|
||||
result = basic(req, res, next);
|
||||
});
|
||||
|
||||
it("triggers next with an error", function() {
|
||||
expect(next).to.be.called;
|
||||
});
|
||||
};
|
||||
|
||||
context("with a valid request", function() {
|
||||
var result;
|
||||
|
||||
beforeEach(function() {
|
||||
result = basic(req, res, next);
|
||||
});
|
||||
|
||||
it("returns true", function() {
|
||||
expect(result).to.be.truthy;
|
||||
});
|
||||
|
||||
it("doesn't modify the response", function() {
|
||||
expect(res.end).to.not.be.called;
|
||||
});
|
||||
});
|
||||
|
||||
context("if the user/pass don't match", function() {
|
||||
beforeEach(function() {
|
||||
var auth = new Buffer("bad:wrong", "utf8").toString("base64");
|
||||
req.headers = { authorization: "Basic " + auth };
|
||||
});
|
||||
|
||||
checkUnauthorized();
|
||||
});
|
||||
|
||||
context("if there is already an authorized user", function() {
|
||||
var result;
|
||||
|
||||
beforeEach(function() {
|
||||
req.user = "user";
|
||||
result = basic(req, res, next);
|
||||
});
|
||||
|
||||
it("returns true", function() {
|
||||
expect(result).to.be.truthy;
|
||||
});
|
||||
|
||||
it("doesn't modify the response", function() {
|
||||
expect(res.end).to.not.be.called;
|
||||
});
|
||||
});
|
||||
|
||||
context("if there are no authorization headers", function() {
|
||||
beforeEach(function() {
|
||||
delete req.headers.authorization;
|
||||
});
|
||||
|
||||
checkUnauthorized();
|
||||
});
|
||||
|
||||
context("the authorization type isn't Basic", function() {
|
||||
beforeEach(function() {
|
||||
var auth = new Buffer("user:pass", "utf8").toString("base64");
|
||||
req.headers = { authorization: "Digest " + auth };
|
||||
});
|
||||
|
||||
checkError();
|
||||
});
|
||||
|
||||
context("the authorization header is missing content", function() {
|
||||
beforeEach(function() {
|
||||
req.headers = { authorization: "Basic" };
|
||||
});
|
||||
|
||||
checkError();
|
||||
});
|
||||
|
||||
context("if the authorization header isn't formatted correctly", function() {
|
||||
beforeEach(function() {
|
||||
var auth = new Buffer("user-pass", "utf8").toString("base64");
|
||||
req.headers = { authorization: "Basic " + auth };
|
||||
});
|
||||
|
||||
checkUnauthorized();
|
||||
});
|
||||
});
|
|
@ -1,211 +0,0 @@
|
|||
/* jshint expr:true */
|
||||
"use strict";
|
||||
|
||||
var Cylon = source("cylon"),
|
||||
router = source("api/routes");
|
||||
|
||||
var MockRequest = require("../../support/mock_request"),
|
||||
MockResponse = require("../../support/mock_response");
|
||||
|
||||
function findRoute(path) {
|
||||
var routes = router.stack.filter(function(m) {
|
||||
return m.regexp.test(path);
|
||||
});
|
||||
return routes[0];
|
||||
}
|
||||
|
||||
function findFinalHandler(path) {
|
||||
var handlers = findRoute(path).route.stack.map(function(m) {
|
||||
return m.handle;
|
||||
});
|
||||
return handlers[handlers.length - 1];
|
||||
}
|
||||
|
||||
function MockPromise() {
|
||||
var my = this;
|
||||
my.resolve = function() {
|
||||
var args = arguments;
|
||||
process.nextTick(function() {
|
||||
my.thenCallback.apply(null, args);
|
||||
});
|
||||
return my;
|
||||
};
|
||||
my.reject = function() {
|
||||
var args = arguments;
|
||||
process.nextTick(function() {
|
||||
my.errorCallback.apply(null, args);
|
||||
});
|
||||
return my;
|
||||
};
|
||||
my.then = function(thenCallback) {
|
||||
my.thenCallback = thenCallback;
|
||||
return my;
|
||||
};
|
||||
my.catch = function(errorCallback) {
|
||||
my.errorCallback = errorCallback;
|
||||
return my;
|
||||
};
|
||||
my.deferred = my;
|
||||
}
|
||||
|
||||
describe("API routes", function() {
|
||||
var routes = [
|
||||
["GET", "/"],
|
||||
["GET", "/commands"],
|
||||
["POST", "/commands/command"],
|
||||
["GET", "/robots"],
|
||||
["GET", "/robots/TestBot"],
|
||||
["GET", "/robots/TestBot/commands"],
|
||||
["POST", "/robots/TestBot/commands/cmd"],
|
||||
["GET", "/robots/TestBot/devices"],
|
||||
["GET", "/robots/TestBot/devices/ping"],
|
||||
["GET", "/robots/TestBot/devices/ping/commands"],
|
||||
["POST", "/robots/TestBot/devices/ping/commands/ping"],
|
||||
["GET", "/robots/TestBot/connections"],
|
||||
["GET", "/robots/TestBot/connections/loopback"]
|
||||
];
|
||||
|
||||
routes.forEach(function(route) {
|
||||
var method = route[0],
|
||||
path = route[1];
|
||||
|
||||
it("defines a " + method + " route for " + path, function() {
|
||||
expect(findRoute(path)).to.exist();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe("API commands", function() {
|
||||
|
||||
var req, res;
|
||||
beforeEach(function() {
|
||||
Cylon.commands.ping = function() { return "pong"; };
|
||||
Cylon.commands.pingAsync = function() {
|
||||
var promise = new MockPromise();
|
||||
return promise.resolve("immediate pong");
|
||||
};
|
||||
req = new MockRequest();
|
||||
res = new MockResponse();
|
||||
res.status = function(statusCode) {
|
||||
res.statusCode = statusCode;
|
||||
return res;
|
||||
};
|
||||
req.device = {
|
||||
name: "testDevice",
|
||||
commands: {
|
||||
announce: function(){return "im here";},
|
||||
announceAsync: function() {
|
||||
var promise = new MockPromise();
|
||||
process.nextTick(function(){
|
||||
return promise.reject("sorry, sore throat");
|
||||
});
|
||||
return promise.deferred;
|
||||
}
|
||||
}
|
||||
};
|
||||
req.robot = {
|
||||
name: "fred",
|
||||
commands: {
|
||||
speak: function(){return "ahem";},
|
||||
speakAsync: function() {
|
||||
var promise = new MockPromise();
|
||||
process.nextTick(function(){
|
||||
return promise.resolve("see ya in another cycle");
|
||||
});
|
||||
return promise.deferred;
|
||||
}
|
||||
},
|
||||
devices: {
|
||||
testDevice: req.device
|
||||
}
|
||||
};
|
||||
});
|
||||
afterEach(function() {
|
||||
delete Cylon.commands.ping;
|
||||
delete Cylon.commands.pingAsync;
|
||||
});
|
||||
|
||||
it("returns the list of MCP commands", function() {
|
||||
res.json = function(obj){
|
||||
expect(obj.commands).to.exist();
|
||||
expect(obj.commands.length).to.equal(2);
|
||||
expect(obj.commands[0]).to.equal("ping");
|
||||
};
|
||||
findFinalHandler("/commands")(req, res);
|
||||
});
|
||||
|
||||
it("invokes an MCP command", function() {
|
||||
req.params = {command:"ping"};
|
||||
res.json = function(obj){
|
||||
expect(obj.result).to.equal("pong");
|
||||
};
|
||||
findFinalHandler("/commands/ping")(req, res);
|
||||
});
|
||||
|
||||
it("returns an immediate MCP async command", function() {
|
||||
req.params = {command:"pingAsync"};
|
||||
res.json = function(obj){
|
||||
expect(obj.result).to.equal("immediate pong");
|
||||
};
|
||||
findFinalHandler("/commands/pingAsync")(req, res);
|
||||
});
|
||||
|
||||
it("returns the list of robot commands", function() {
|
||||
req.params = {robot: "fred"};
|
||||
res.json = function(obj){
|
||||
expect(obj.commands).to.exist();
|
||||
expect(obj.commands.length).to.equal(2);
|
||||
expect(obj.commands[0]).to.equal("speak");
|
||||
};
|
||||
findFinalHandler("/robots/fred/commands")(req, res);
|
||||
});
|
||||
|
||||
it("invokes a robot command", function() {
|
||||
req.params = {robot: "fred", command:"speak"};
|
||||
res.json = function(obj){
|
||||
expect(obj.result).to.equal("ahem");
|
||||
};
|
||||
findFinalHandler("/robots/fred/commands/speak")(req, res);
|
||||
});
|
||||
|
||||
it("invokes an asynchronous robot command", function() {
|
||||
req.params = {robot: "fred", command:"speakAsync"};
|
||||
res.json = function(obj){
|
||||
expect(obj.result).to.equal("see ya in another cycle");
|
||||
};
|
||||
findFinalHandler("/robots/fred/commands/speakAsync")(req, res);
|
||||
});
|
||||
|
||||
it("returns the list of device commands", function() {
|
||||
req.params = {robot: "fred", device: "testDevice" };
|
||||
res.json = function(obj){
|
||||
expect(obj.commands).to.exist();
|
||||
expect(obj.commands.length).to.equal(2);
|
||||
expect(obj.commands[0]).to.equal("announce");
|
||||
};
|
||||
var path = "/robots/fred/devices/testDevice/commands";
|
||||
findFinalHandler(path)(req, res);
|
||||
});
|
||||
|
||||
it("invokes a device command", function() {
|
||||
req.params = {robot: "fred", device: "testDevice", command:"announce"};
|
||||
res.json = function(obj){
|
||||
expect(obj.result).to.equal("im here");
|
||||
};
|
||||
var path = "/robots/fred/devices/testDevice/commands/announce";
|
||||
findFinalHandler(path)(req, res);
|
||||
});
|
||||
|
||||
it("returns correctly for an async device command that errored", function() {
|
||||
req.params = {robot: "fred", device: "testDevice", command:"announceAsync"};
|
||||
res.json = function(obj){
|
||||
expect(obj.error).to.equal("sorry, sore throat");
|
||||
expect(res.statusCode).to.equal(500);
|
||||
};
|
||||
var path = "/robots/fred/devices/testDevice/commands/announceAsync";
|
||||
findFinalHandler(path)(req, res);
|
||||
});
|
||||
|
||||
});
|
|
@ -4,8 +4,7 @@
|
|||
var Cylon = source("cylon"),
|
||||
Robot = source("robot");
|
||||
|
||||
var API = source("api"),
|
||||
Logger = source("logger"),
|
||||
var Logger = source("logger"),
|
||||
Adaptor = source("adaptor"),
|
||||
Driver = source("driver"),
|
||||
Config = source("config");
|
||||
|
@ -24,8 +23,8 @@ describe("Cylon", function() {
|
|||
expect(Cylon.Driver).to.be.eql(Driver);
|
||||
});
|
||||
|
||||
it("sets @api_instance to null by default", function() {
|
||||
expect(Cylon.api_instance).to.be.eql(null);
|
||||
it("sets @apiInstances to an empty array by default", function() {
|
||||
expect(Cylon.apiInstances).to.be.eql([]);
|
||||
});
|
||||
|
||||
it("sets @robots to an empty object by default", function() {
|
||||
|
@ -60,23 +59,37 @@ describe("Cylon", function() {
|
|||
});
|
||||
|
||||
describe("#api", function() {
|
||||
beforeEach(function() {
|
||||
stub(API.prototype, "listen");
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
API.prototype.listen.restore();
|
||||
Cylon.apiInstances = [];
|
||||
});
|
||||
|
||||
it("creates a new API instance", function() {
|
||||
Cylon.api();
|
||||
expect(Cylon.api_instance).to.be.an.instanceOf(API);
|
||||
context("with a provided API server and opts", function() {
|
||||
var API, opts, instance;
|
||||
|
||||
beforeEach(function() {
|
||||
instance = { listen: spy() };
|
||||
opts = { https: false };
|
||||
API = stub().returns(instance);
|
||||
|
||||
Cylon.api(API, opts);
|
||||
});
|
||||
|
||||
it("passes configuration to the API constructor", function() {
|
||||
Cylon.config({ api: { port: "1234" }});
|
||||
Cylon.api();
|
||||
expect(Cylon.api_instance.port).to.be.eql("1234");
|
||||
it("creates an API instance", function() {
|
||||
expect(API).to.be.calledWithNew;
|
||||
expect(API).to.be.calledWith(opts);
|
||||
});
|
||||
|
||||
it("passes Cylon through to the instance as opts.mcp", function() {
|
||||
expect(opts.mcp).to.be.eql(Cylon);
|
||||
});
|
||||
|
||||
it("stores the API instance in @apiInstances", function() {
|
||||
expect(Cylon.apiInstances).to.be.eql([instance]);
|
||||
});
|
||||
|
||||
it("tells the API instance to start listening", function() {
|
||||
expect(instance.listen).to.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var sinon = require('sinon'),
|
||||
spy = sinon.spy,
|
||||
stub = sinon.stub;
|
||||
|
||||
// A mock version of the http.ClientRequest class
|
||||
var MockRequest = module.exports = function MockRequest(opts) {
|
||||
if (opts == null) {
|
||||
opts = {};
|
||||
}
|
||||
|
||||
this.url = "/";
|
||||
|
||||
this.headers = {};
|
||||
|
||||
for (var opt in opts) {
|
||||
this[opt] = opts[opt];
|
||||
}
|
||||
};
|
|
@ -1,16 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var sinon = require('sinon'),
|
||||
spy = sinon.spy,
|
||||
stub = sinon.stub;
|
||||
|
||||
// A mock version of http.ServerResponse to be used in tests
|
||||
var MockResponse = module.exports = function MockResponse() {
|
||||
this.end = spy();
|
||||
|
||||
this.headers = {};
|
||||
};
|
||||
|
||||
MockResponse.prototype.setHeader = function setHeader(name, value) {
|
||||
this.headers[name] = value;
|
||||
};
|
Loading…
Reference in New Issue