From 41f306a7f0906891fdf571d5e8ef9fea1a3a2ae0 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Wed, 2 Dec 2015 18:18:31 +0900 Subject: [PATCH 01/37] Add basic api unit. This unit will be responsible for providing all stf restful apis. --- lib/cli.js | 41 ++++++++++++++++++++++++++ lib/units/api/config/default.yaml | 38 ++++++++++++++++++++++++ lib/units/api/controllers/user.js | 10 +++++++ lib/units/api/index.js | 27 +++++++++++++++++ lib/units/api/swagger/api_v1.yaml | 48 +++++++++++++++++++++++++++++++ lib/units/poorxy/index.js | 7 +++++ package.json | 1 + 7 files changed, 172 insertions(+) create mode 100644 lib/units/api/config/default.yaml create mode 100644 lib/units/api/controllers/user.js create mode 100644 lib/units/api/index.js create mode 100644 lib/units/api/swagger/api_v1.yaml diff --git a/lib/cli.js b/lib/cli.js index d668062094..d9532099c3 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -758,6 +758,9 @@ program .option('-u, --app-url ' , 'URL to app' , String) + .option('-u, --api-url ' + , 'URL to api' + , String) .option('-a, --auth-url ' , 'URL to auth client' , String) @@ -780,6 +783,9 @@ program if (!options.authUrl) { this.missingArgument('--auth-url') } + if (!options.apiUrl) { + this.missingArgument('--api-url') + } if (!options.websocketUrl) { this.missingArgument('--websocket-url') } @@ -796,6 +802,7 @@ program require('./units/poorxy')({ port: options.port , appUrl: options.appUrl + , apiUrl: options.apiUrl , authUrl: options.authUrl , websocketUrl: options.websocketUrl , storageUrl: options.storageUrl @@ -849,6 +856,28 @@ program }) }) + program + .command('api') + .description('start api') + .option('-p, --port ' + , 'port (or $PORT)' + , Number + , process.env.PORT || 7106) + .option('-s, --secret ' + , 'secret (or $SECRET)' + , String + , process.env.SECRET) + .action(function(options) { + if (!options.secret) { + this.missingArgument('--secret') + } + + require('./units/api')({ + port: options.port + , secret: options.secret + }) + }) + program .command('websocket') .description('start websocket') @@ -1110,6 +1139,10 @@ program , 'app port' , Number , 7105) + .option('--api-port ' + , 'api port' + , Number + , 7106) .option('--websocket-port ' , 'websocket port' , Number @@ -1285,6 +1318,12 @@ program return extra })())) + // api + , procutil.fork(__filename, [ + 'api' + , '--port', options.apiPort + , '--secret', options.authSecret + ]) // websocket , procutil.fork(__filename, [ 'websocket' @@ -1326,6 +1365,8 @@ program , util.format('http://localhost:%d/', options.appPort) , '--auth-url' , util.format('http://localhost:%d/', options.authPort) + , '--api-url' + , util.format('http://localhost:%d/', options.apiPort) , '--websocket-url' , util.format('http://localhost:%d/', options.websocketPort) , '--storage-url' diff --git a/lib/units/api/config/default.yaml b/lib/units/api/config/default.yaml new file mode 100644 index 0000000000..0cd0513fdb --- /dev/null +++ b/lib/units/api/config/default.yaml @@ -0,0 +1,38 @@ +# swagger configuration file + +# values in the swagger hash are system configuration for swagger-node +swagger: + + fittingsDirs: [ fittings ] + defaultPipe: null + swaggerControllerPipe: swagger_controllers # defines the standard processing pipe for controllers + swagger: 'swagger/api_v1.yaml' + + # values defined in the bagpipes key are the bagpipes pipes and fittings definitions + # (see https://github.com/apigee-127/bagpipes) + bagpipes: + + _router: + name: swagger_router + mockMode: false + mockControllersDirs: [ mocks ] + controllersDirs: [ controllers ] + + _swagger_validate: + name: swagger_validator + validateResponse: true + + # pipe for all swagger-node controllers + swagger_controllers: + - onError: json_error_handler + - cors + - swagger_security + - _swagger_validate + - express_compatibility + - _router + + # pipe to serve swagger (endpoint is in swagger.yaml) + swagger_raw: + name: swagger_raw + +# any other values in this file are just loaded into the config for application access... diff --git a/lib/units/api/controllers/user.js b/lib/units/api/controllers/user.js new file mode 100644 index 0000000000..7ad9756941 --- /dev/null +++ b/lib/units/api/controllers/user.js @@ -0,0 +1,10 @@ +module.exports = { + getCurrentUser: getCurrentUser +}; + +function getCurrentUser(req, res) { + res.json({ + success: true + , user: {"name": "dummy"} + }) +} diff --git a/lib/units/api/index.js b/lib/units/api/index.js new file mode 100644 index 0000000000..23fd539716 --- /dev/null +++ b/lib/units/api/index.js @@ -0,0 +1,27 @@ +var http = require('http') +var path = require('path') + +var express = require('express') +var SwaggerExpress = require('swagger-express-mw') + +var logger = require('../../util/logger') + +module.exports = function(options) { + var log = logger.createLogger('api') + , app = express() + , server = http.createServer(app) + + var config = { + appRoot: __dirname + , swaggerFile: path.resolve(__dirname, 'swagger', 'api_v1.yaml') + }; + + SwaggerExpress.create(config, function(err, swaggerExpress) { + if (err) { throw err; } + + swaggerExpress.register(app); + }) + + server.listen(options.port) + log.info('Listening on port %d', options.port) +} diff --git a/lib/units/api/swagger/api_v1.yaml b/lib/units/api/swagger/api_v1.yaml new file mode 100644 index 0000000000..fd5cbd8f10 --- /dev/null +++ b/lib/units/api/swagger/api_v1.yaml @@ -0,0 +1,48 @@ +swagger: "2.0" +info: + version: "1.0.10" + title: Smartphone Test Farm + description: Control and manager real Smartphone devices from browser and apis + license: + name: Apache-2.0 + url: http://www.apache.org/licenses/LICENSE-2.0 + contact: + url: http://openstf.io/ + email: contact@openstf.io +basePath: /api/v1/ +schemes: + - http + - https +consumes: + - application/json +produces: + - application/json +paths: + /me: + x-swagger-router-controller: user + get: + summary: User Profile + description: The User Profile endpoint returns information about current authorized user. + operationId: getCurrentUser + responses: + "200": + description: Current User Profile information + schema: + $ref: "#/definitions/UserResponse" + default: + description: Unexpected Error + schema: + $ref: "#/definitions/ErrorResponse" +definitions: + UserResponse: + required: + - user + properties: + user: + type: object + ErrorResponse: + required: + - message + properties: + message: + type: string diff --git a/lib/units/poorxy/index.js b/lib/units/poorxy/index.js index 1141d1b12a..74f44fc594 100644 --- a/lib/units/poorxy/index.js +++ b/lib/units/poorxy/index.js @@ -51,6 +51,13 @@ module.exports = function(options) { }) }) + ;['/api/*'].forEach(function(route) { + app.all(route, function(req, res) { + proxy.web(req, res, { + target: options.apiUrl + }) + }) + }) app.use(function(req, res) { proxy.web(req, res, { target: options.appUrl diff --git a/package.json b/package.json index 98ce8ffe41..996134e2b7 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "stf-device-db": "^1.2.0", "stf-syrup": "^1.0.0", "stf-wiki": "^1.0.0", + "swagger-express-mw": "^0.6.0", "temp": "^0.8.1", "transliteration": "^0.1.1", "utf-8-validate": "^1.2.1", From e0a45391abe82dbc7742b19351137218723b72e7 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Wed, 2 Dec 2015 21:14:42 +0900 Subject: [PATCH 02/37] Add auth middleware in api unit. Now only authorized user can access api unit --- lib/cli.js | 18 +++++++++ lib/db/api.js | 4 ++ lib/units/api/index.js | 14 +++++++ lib/units/api/middleware/auth.js | 65 +++++++++++++++++++++++++++++++ lib/units/api/swagger/api_v1.yaml | 1 + 5 files changed, 102 insertions(+) create mode 100644 lib/units/api/middleware/auth.js diff --git a/lib/cli.js b/lib/cli.js index d9532099c3..b7a8345608 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -863,18 +863,30 @@ program , 'port (or $PORT)' , Number , process.env.PORT || 7106) + .option('-i, --ssid ' + , 'session SSID (or $SSID)' + , String + , process.env.SSID || 'ssid') .option('-s, --secret ' , 'secret (or $SECRET)' , String , process.env.SECRET) + .option('-a, --auth-url ' + , 'URL to auth client' + , String) .action(function(options) { if (!options.secret) { this.missingArgument('--secret') } + if (!options.authUrl) { + this.missingArgument('--auth-url') + } require('./units/api')({ port: options.port + , ssid: options.ssid , secret: options.secret + , authUrl: options.authUrl }) }) @@ -1323,6 +1335,12 @@ program 'api' , '--port', options.apiPort , '--secret', options.authSecret + , '--auth-url', options.authUrl || util.format( + 'http://%s:%d/auth/%s/' + , options.publicIp + , options.poorxyPort + , ({oauth2: 'oauth'}[options.authType]) || options.authType + ) ]) // websocket , procutil.fork(__filename, [ diff --git a/lib/db/api.js b/lib/db/api.js index 59c801f000..65c5978fcd 100644 --- a/lib/db/api.js +++ b/lib/db/api.js @@ -329,4 +329,8 @@ dbapi.loadAccessTokens = function(email) { })) } +dbapi.loadAccessToken = function(id) { + return db.run(r.table('accessTokens').get(id)) +} + module.exports = dbapi diff --git a/lib/units/api/index.js b/lib/units/api/index.js index 23fd539716..10a3eb2c7b 100644 --- a/lib/units/api/index.js +++ b/lib/units/api/index.js @@ -3,9 +3,12 @@ var path = require('path') var express = require('express') var SwaggerExpress = require('swagger-express-mw') +var cookieSession = require('cookie-session') var logger = require('../../util/logger') +var auth = require('./middleware/auth') + module.exports = function(options) { var log = logger.createLogger('api') , app = express() @@ -22,6 +25,17 @@ module.exports = function(options) { swaggerExpress.register(app); }) + // TODO: Remove this once frontend is stateless + app.use(cookieSession({ + name: options.ssid + , keys: [options.secret] + })) + + app.use(auth({ + secret: options.secret + , authUrl: options.authUrl + })) + server.listen(options.port) log.info('Listening on port %d', options.port) } diff --git a/lib/units/api/middleware/auth.js b/lib/units/api/middleware/auth.js new file mode 100644 index 0000000000..742c5884e2 --- /dev/null +++ b/lib/units/api/middleware/auth.js @@ -0,0 +1,65 @@ +var jwtutil = require('../../../util/jwtutil') +var urlutil = require('../../../util/urlutil') +var logger = require('../../../util/logger') +var dbapi = require('../../../db/api') + +module.exports = function(options) { + return function(req, res, next) { + + var log = logger.createLogger('api:auth') + + if (req.headers.authorization) { + var tokenId = req.headers.authorization.split(" ")[1] + + if (tokenId) { + dbapi.loadAccessToken(tokenId) + .then(function(token) { + var jwt = token.jwt + , data = jwtutil.decode(jwt, options.secret) + + if (data) { + dbapi.loadUser(data.email) + .then(function(user) { + if (user) { + req.user = user + next() + } + }) + } + }) + .catch(function(err) { + log.error('Failed to load token: ', err.stack) + res.json(500, { + success: false, + description: "Bad Access Token" + }) + }) + } else { + log.error("Bad Access Token") + res.json(500, { + success: false, + description: "Bad Access Token Header" + }) + } + } + // TODO: Remove this once frontend become stateless + else if (req.session && req.session.jwt) { + dbapi.loadUser(req.session.jwt.email) + .then(function(user) { + if (user) { + req.user = user + next() + } + else { + // We no longer have the user in the database + res.redirect(options.authUrl) + } + }) + .catch(next) + } + else { + // No session, forward to auth client + res.redirect(options.authUrl) + } + } +} diff --git a/lib/units/api/swagger/api_v1.yaml b/lib/units/api/swagger/api_v1.yaml index fd5cbd8f10..0821c842e0 100644 --- a/lib/units/api/swagger/api_v1.yaml +++ b/lib/units/api/swagger/api_v1.yaml @@ -33,6 +33,7 @@ paths: description: Unexpected Error schema: $ref: "#/definitions/ErrorResponse" + definitions: UserResponse: required: From 0e8b308b6c1ee7d5d208c3236af47037a73854ba Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Wed, 2 Dec 2015 21:23:57 +0900 Subject: [PATCH 03/37] change /app/api/v1/user -> /api/v1/me --- lib/units/api/controllers/user.js | 2 +- lib/units/app/index.js | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/units/api/controllers/user.js b/lib/units/api/controllers/user.js index 7ad9756941..f82ce2b059 100644 --- a/lib/units/api/controllers/user.js +++ b/lib/units/api/controllers/user.js @@ -5,6 +5,6 @@ module.exports = { function getCurrentUser(req, res) { res.json({ success: true - , user: {"name": "dummy"} + , user: req.user }) } diff --git a/lib/units/app/index.js b/lib/units/app/index.js index e355bd903b..81cbcc243b 100644 --- a/lib/units/app/index.js +++ b/lib/units/app/index.js @@ -124,13 +124,6 @@ module.exports = function(options) { res.send('var GLOBAL_APPSTATE = ' + JSON.stringify(state)) }) - app.get('/app/api/v1/user', function(req, res) { - res.json({ - success: true - , user: req.user - }) - }) - app.get('/app/api/v1/group', function(req, res) { dbapi.loadGroup(req.user.email) .then(function(cursor) { From d6f37681ce8afbe929d84ddacd3da711b3d8ee44 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Wed, 2 Dec 2015 22:56:35 +0900 Subject: [PATCH 04/37] move devices endpoint from app unit to api unit --- lib/units/api/config/default.yaml | 1 + lib/units/api/controllers/device.js | 61 +++++++++++++++++++ lib/units/api/swagger/api_v1.yaml | 52 +++++++++++++++- lib/units/app/index.js | 47 -------------- .../components/stf/device/device-service.js | 4 +- 5 files changed, 115 insertions(+), 50 deletions(-) create mode 100644 lib/units/api/controllers/device.js diff --git a/lib/units/api/config/default.yaml b/lib/units/api/config/default.yaml index 0cd0513fdb..d289e53567 100644 --- a/lib/units/api/config/default.yaml +++ b/lib/units/api/config/default.yaml @@ -26,6 +26,7 @@ swagger: swagger_controllers: - onError: json_error_handler - cors + - swagger_params_parser - swagger_security - _swagger_validate - express_compatibility diff --git a/lib/units/api/controllers/device.js b/lib/units/api/controllers/device.js new file mode 100644 index 0000000000..b830fd4f44 --- /dev/null +++ b/lib/units/api/controllers/device.js @@ -0,0 +1,61 @@ +var Promise = require('bluebird') + +var dbapi = require('../../../db/api') +var logger = require('../../../util/logger') +var datautil = require('../../../util/datautil') + +var log = logger.createLogger('api:contoller:device') + +module.exports = { + getDevices: getDevices +, getDeviceBySerial: getDeviceBySerial +}; + +function getDevices(req, res) { + dbapi.loadDevices() + .then(function(cursor) { + return Promise.promisify(cursor.toArray, cursor)() + .then(function(list) { + list.forEach(function(device) { + datautil.normalize(device, req.user) + }) + + res.json({ + success: true + , devices: list + }) + }) + }) + .catch(function(err) { + log.error('Failed to load device list: ', err.stack) + res.json(500, { + success: false + }) + }) +} + +function getDeviceBySerial(req, res) { + var serial = req.swagger.params.serial.value + + dbapi.loadDevice(serial) + .then(function(device) { + if (device) { + datautil.normalize(device, req.user) + res.json({ + success: true + , device: device + }) + } + else { + res.json(404, { + success: false + }) + } + }) + .catch(function(err) { + log.error('Failed to load device "%s": ', req.params.serial, err.stack) + res.json(500, { + success: false + }) + }) +} diff --git a/lib/units/api/swagger/api_v1.yaml b/lib/units/api/swagger/api_v1.yaml index 0821c842e0..07cee6b458 100644 --- a/lib/units/api/swagger/api_v1.yaml +++ b/lib/units/api/swagger/api_v1.yaml @@ -9,7 +9,7 @@ info: contact: url: http://openstf.io/ email: contact@openstf.io -basePath: /api/v1/ +basePath: /api/v1 schemes: - http - https @@ -33,6 +33,42 @@ paths: description: Unexpected Error schema: $ref: "#/definitions/ErrorResponse" + /devices: + x-swagger-router-controller: device + get: + summary: Device List + description: List of all the STF devices including Disconnected and Offline + operationId: getDevices + responses: + "200": + description: List of Devices + schema: + $ref: "#/definitions/DeviceListResponse" + default: + description: Unexpected Error + schema: + $ref: "#/definitions/ErrorResponse" + /devices/{serial}: + x-swagger-router-controller: device + get: + summary: Device Information + description: Device Information + operationId: getDeviceBySerial + parameters: + - name: serial + in: path + description: Device Serial + required: true + type: string + responses: + "200": + description: Device Information + schema: + $ref: "#/definitions/DeviceResponse" + default: + description: Unexpected Error + schema: + $ref: "#/definitions/ErrorResponse" definitions: UserResponse: @@ -41,6 +77,20 @@ definitions: properties: user: type: object + DeviceListResponse: + required: + - devices + properties: + devices: + type: array + items: + type: object + DeviceResponse: + required: + - device + properties: + device: + type: object ErrorResponse: required: - message diff --git a/lib/units/app/index.js b/lib/units/app/index.js index 81cbcc243b..ac95ebe3ff 100644 --- a/lib/units/app/index.js +++ b/lib/units/app/index.js @@ -146,53 +146,6 @@ module.exports = function(options) { }) }) - app.get('/app/api/v1/devices', function(req, res) { - dbapi.loadDevices() - .then(function(cursor) { - return Promise.promisify(cursor.toArray, cursor)() - .then(function(list) { - list.forEach(function(device) { - datautil.normalize(device, req.user) - }) - - res.json({ - success: true - , devices: list - }) - }) - }) - .catch(function(err) { - log.error('Failed to load device list: ', err.stack) - res.json(500, { - success: false - }) - }) - }) - - app.get('/app/api/v1/devices/:serial', function(req, res) { - dbapi.loadDevice(req.params.serial) - .then(function(device) { - if (device) { - datautil.normalize(device, req.user) - res.json({ - success: true - , device: device - }) - } - else { - res.json(404, { - success: false - }) - } - }) - .catch(function(err) { - log.error('Failed to load device "%s": ', req.params.serial, err.stack) - res.json(500, { - success: false - }) - }) - }) - app.get('/app/api/v1/accessTokens', function(req, res) { dbapi.loadAccessTokens(req.user.email) .then(function(cursor) { diff --git a/res/app/components/stf/device/device-service.js b/res/app/components/stf/device/device-service.js index 95aeb63254..0ab5b6b262 100644 --- a/res/app/components/stf/device/device-service.js +++ b/res/app/components/stf/device/device-service.js @@ -165,7 +165,7 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi , digest: false }) - oboe('/app/api/v1/devices') + oboe('/api/v1/devices') .node('devices[*]', function(device) { tracker.add(device) }) @@ -190,7 +190,7 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi } deviceService.load = function(serial) { - return $http.get('/app/api/v1/devices/' + serial) + return $http.get('/api/v1/devices/' + serial) .then(function(response) { return response.data.device }) From 5729095acb4bee386436f336ab98a423180a2857 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Wed, 2 Dec 2015 23:06:14 +0900 Subject: [PATCH 05/37] move group endpoint from app unit to api unit --- lib/units/api/controllers/user.js | 31 +++++++++++++++++++ lib/units/api/swagger/api_v1.yaml | 24 ++++++++++++++ lib/units/app/index.js | 22 ------------- .../components/stf/device/device-service.js | 4 +-- 4 files changed, 57 insertions(+), 24 deletions(-) diff --git a/lib/units/api/controllers/user.js b/lib/units/api/controllers/user.js index f82ce2b059..537e0099e9 100644 --- a/lib/units/api/controllers/user.js +++ b/lib/units/api/controllers/user.js @@ -1,5 +1,14 @@ +var Promise = require('bluebird') + +var dbapi = require('../../../db/api') +var logger = require('../../../util/logger') +var datautil = require('../../../util/datautil') + +var log = logger.createLogger('api:contoller:user') + module.exports = { getCurrentUser: getCurrentUser +, getCurrentUserGroup: getCurrentUserGroup }; function getCurrentUser(req, res) { @@ -8,3 +17,25 @@ function getCurrentUser(req, res) { , user: req.user }) } + +function getCurrentUserGroup(req, res) { + dbapi.loadGroup(req.user.email) + .then(function(cursor) { + return Promise.promisify(cursor.toArray, cursor)() + .then(function(list) { + list.forEach(function(device) { + datautil.normalize(device, req.user) + }) + res.json({ + success: true + , devices: list + }) + }) + }) + .catch(function(err) { + log.error('Failed to load group: ', err.stack) + res.json(500, { + success: false + }) + }) +} diff --git a/lib/units/api/swagger/api_v1.yaml b/lib/units/api/swagger/api_v1.yaml index 07cee6b458..20ade953b3 100644 --- a/lib/units/api/swagger/api_v1.yaml +++ b/lib/units/api/swagger/api_v1.yaml @@ -33,6 +33,22 @@ paths: description: Unexpected Error schema: $ref: "#/definitions/ErrorResponse" +# TODO: Change group endpoint with something more easy to understandable endpoint + /group: + x-swagger-router-controller: user + get: + summary: User Group + description: The User Group endpoint returns information about user group of current authorized user. + operationId: getCurrentUserGroup + responses: + "200": + description: Current User Group information + schema: + $ref: "#/definitions/GroupResponse" + default: + description: Unexpected Error + schema: + $ref: "#/definitions/ErrorResponse" /devices: x-swagger-router-controller: device get: @@ -77,6 +93,14 @@ definitions: properties: user: type: object + GroupResponse: + required: + - devices + properties: + devices: + type: array + items: + type: object DeviceListResponse: required: - devices diff --git a/lib/units/app/index.js b/lib/units/app/index.js index ac95ebe3ff..53ef848918 100644 --- a/lib/units/app/index.js +++ b/lib/units/app/index.js @@ -124,28 +124,6 @@ module.exports = function(options) { res.send('var GLOBAL_APPSTATE = ' + JSON.stringify(state)) }) - app.get('/app/api/v1/group', function(req, res) { - dbapi.loadGroup(req.user.email) - .then(function(cursor) { - return Promise.promisify(cursor.toArray, cursor)() - .then(function(list) { - list.forEach(function(device) { - datautil.normalize(device, req.user) - }) - res.json({ - success: true - , devices: list - }) - }) - }) - .catch(function(err) { - log.error('Failed to load group: ', err.stack) - res.json(500, { - success: false - }) - }) - }) - app.get('/app/api/v1/accessTokens', function(req, res) { dbapi.loadAccessTokens(req.user.email) .then(function(cursor) { diff --git a/res/app/components/stf/device/device-service.js b/res/app/components/stf/device/device-service.js index 0ab5b6b262..a01df982bc 100644 --- a/res/app/components/stf/device/device-service.js +++ b/res/app/components/stf/device/device-service.js @@ -181,8 +181,8 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi , digest: true }) - oboe('/app/api/v1/group') - .node('devices[*]', function(device) { + oboe('/api/v1/group') + .node('devices[*]', function (device) { tracker.add(device) }) From 0ec03aa2c00a1e4a7a48c960c38c02d91fb07a19 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Wed, 2 Dec 2015 23:13:46 +0900 Subject: [PATCH 06/37] move accessTokens endpoint from app unit to api unit --- lib/units/api/controllers/token.js | 33 +++++++++++++++++++ lib/units/api/swagger/api_v1.yaml | 23 +++++++++++++ lib/units/app/index.js | 23 ------------- .../stf/tokens/access-token-service.js | 2 +- 4 files changed, 57 insertions(+), 24 deletions(-) create mode 100644 lib/units/api/controllers/token.js diff --git a/lib/units/api/controllers/token.js b/lib/units/api/controllers/token.js new file mode 100644 index 0000000000..d83dca2a3c --- /dev/null +++ b/lib/units/api/controllers/token.js @@ -0,0 +1,33 @@ +var Promise = require('bluebird') + +var dbapi = require('../../../db/api') +var logger = require('../../../util/logger') + +var log = logger.createLogger('api:contoller:token') + +module.exports = { + getAccessTokens: getAccessTokens +}; + +function getAccessTokens(req, res) { + dbapi.loadAccessTokens(req.user.email) + .then(function(cursor) { + return Promise.promisify(cursor.toArray, cursor)() + .then(function(list) { + var titles = [] + list.forEach(function(token) { + titles.push(token.title) + }) + res.json({ + success: true + , titles: titles + }) + }) + }) + .catch(function(err) { + log.error('Failed to load tokens: ', err.stack) + res.json(500, { + success: false + }) + }) +} diff --git a/lib/units/api/swagger/api_v1.yaml b/lib/units/api/swagger/api_v1.yaml index 20ade953b3..4be476608d 100644 --- a/lib/units/api/swagger/api_v1.yaml +++ b/lib/units/api/swagger/api_v1.yaml @@ -49,6 +49,21 @@ paths: description: Unexpected Error schema: $ref: "#/definitions/ErrorResponse" + /accessTokens: + x-swagger-router-controller: token + get: + summary: Access Tokens + description: Return Current User Access Tokens titles + operationId: getAccessTokens + responses: + "200": + description: Access Tokens titles + schema: + $ref: "#/definitions/AccessTokensResponse" + default: + description: Unexpected Error + schema: + $ref: "#/definitions/ErrorResponse" /devices: x-swagger-router-controller: device get: @@ -101,6 +116,14 @@ definitions: type: array items: type: object + AccessTokensResponse: + required: + - tokens + properties: + tokens: + type: array + items: + type: string DeviceListResponse: required: - devices diff --git a/lib/units/app/index.js b/lib/units/app/index.js index 53ef848918..89f4731e88 100644 --- a/lib/units/app/index.js +++ b/lib/units/app/index.js @@ -124,29 +124,6 @@ module.exports = function(options) { res.send('var GLOBAL_APPSTATE = ' + JSON.stringify(state)) }) - app.get('/app/api/v1/accessTokens', function(req, res) { - dbapi.loadAccessTokens(req.user.email) - .then(function(cursor) { - return Promise.promisify(cursor.toArray, cursor)() - .then(function(list) { - var titles = [] - list.forEach(function(token) { - titles.push(token.title) - }) - res.json({ - success: true - , titles: titles - }) - }) - }) - .catch(function(err) { - log.error('Failed to load tokens: ', err.stack) - res.json(500, { - success: false - }) - }) - }) - server.listen(options.port) log.info('Listening on port %d', options.port) } diff --git a/res/app/components/stf/tokens/access-token-service.js b/res/app/components/stf/tokens/access-token-service.js index 38b9db6f15..a815b828a9 100644 --- a/res/app/components/stf/tokens/access-token-service.js +++ b/res/app/components/stf/tokens/access-token-service.js @@ -6,7 +6,7 @@ module.exports = function AccessTokenServiceFactory( var AccessTokenService = {} AccessTokenService.getAccessTokens = function() { - return $http.get('/app/api/v1/accessTokens') + return $http.get('/api/v1/accessTokens') } AccessTokenService.generateAccessToken = function(title) { From f10ae51b5349059571ced2ace0def1f9ae57a709 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Thu, 3 Dec 2015 10:02:03 +0900 Subject: [PATCH 07/37] add swagger.json endpoint --- lib/units/api/swagger/api_v1.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/units/api/swagger/api_v1.yaml b/lib/units/api/swagger/api_v1.yaml index 4be476608d..e44c2b958e 100644 --- a/lib/units/api/swagger/api_v1.yaml +++ b/lib/units/api/swagger/api_v1.yaml @@ -100,7 +100,8 @@ paths: description: Unexpected Error schema: $ref: "#/definitions/ErrorResponse" - + /swagger.json: + x-swagger-pipe: swagger_raw definitions: UserResponse: required: From 221bc78e493c5db3b84da1da2cf7c5577405c8a9 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Thu, 3 Dec 2015 17:33:53 +0900 Subject: [PATCH 08/37] use security handler instead of express middleware for access token validation. --- lib/units/api/config/default.yaml | 6 +- lib/units/api/helpers/securityHandlers.js | 70 +++++++++++++++++++++++ lib/units/api/index.js | 14 ++--- lib/units/api/middleware/auth.js | 65 --------------------- lib/units/api/swagger/api_v1.yaml | 46 ++++++++++++--- 5 files changed, 120 insertions(+), 81 deletions(-) create mode 100644 lib/units/api/helpers/securityHandlers.js delete mode 100644 lib/units/api/middleware/auth.js diff --git a/lib/units/api/config/default.yaml b/lib/units/api/config/default.yaml index d289e53567..360e0a8433 100644 --- a/lib/units/api/config/default.yaml +++ b/lib/units/api/config/default.yaml @@ -6,7 +6,6 @@ swagger: fittingsDirs: [ fittings ] defaultPipe: null swaggerControllerPipe: swagger_controllers # defines the standard processing pipe for controllers - swagger: 'swagger/api_v1.yaml' # values defined in the bagpipes key are the bagpipes pipes and fittings definitions # (see https://github.com/apigee-127/bagpipes) @@ -22,10 +21,13 @@ swagger: name: swagger_validator validateResponse: true + _swagger_security: + name: swagger_security + securityHandlersModule: helpers/securityHandlers + # pipe for all swagger-node controllers swagger_controllers: - onError: json_error_handler - - cors - swagger_params_parser - swagger_security - _swagger_validate diff --git a/lib/units/api/helpers/securityHandlers.js b/lib/units/api/helpers/securityHandlers.js new file mode 100644 index 0000000000..9dee2bd9df --- /dev/null +++ b/lib/units/api/helpers/securityHandlers.js @@ -0,0 +1,70 @@ +var jwtutil = require('../../../util/jwtutil') +var urlutil = require('../../../util/urlutil') +var logger = require('../../../util/logger') +var dbapi = require('../../../db/api') + +var log = logger.createLogger('api:auth') + +module.exports = { + accessTokenAuth: accessTokenAuth +} + +function accessTokenAuth(req, res, next) { + if (req.headers.authorization) { + var tokenId = req.headers.authorization.split(" ")[1] + + if (tokenId) { + dbapi.loadAccessToken(tokenId) + .then(function(token) { + var jwt = token.jwt + , data = jwtutil.decode(jwt, req.options.secret) + + if (data) { + dbapi.loadUser(data.email) + .then(function(user) { + if (user) { + req.user = user + next() + } + }) + } + }) + .catch(function(err) { + log.error('Failed to load token: ', err.stack) + res.json(500, { + success: false, + description: "Bad Access Token" + }) + }) + } else { + log.error("Bad Access Token Header") + res.json(500, { + success: false, + description: "Bad Access Token Header" + }) + } + } + // TODO: Remove this once frontend become stateless + else if (req.session && req.session.jwt) { + dbapi.loadUser(req.session.jwt.email) + .then(function(user) { + if (user) { + req.user = user + next() + } + else { + res.json(500, { + success: false, + description: "Bad Request" + }) + } + }) + .catch(next) + } + else { + res.json(500, { + success: false, + description: "Request does not have Authorization header" + }) + } +} diff --git a/lib/units/api/index.js b/lib/units/api/index.js index 10a3eb2c7b..dd9f51a4ce 100644 --- a/lib/units/api/index.js +++ b/lib/units/api/index.js @@ -7,8 +7,6 @@ var cookieSession = require('cookie-session') var logger = require('../../util/logger') -var auth = require('./middleware/auth') - module.exports = function(options) { var log = logger.createLogger('api') , app = express() @@ -25,17 +23,19 @@ module.exports = function(options) { swaggerExpress.register(app); }) + // Adding options in request, so that swagger controller + // can use it. + app.use(function(req, res, next) { + req.options = options + next() + }) + // TODO: Remove this once frontend is stateless app.use(cookieSession({ name: options.ssid , keys: [options.secret] })) - app.use(auth({ - secret: options.secret - , authUrl: options.authUrl - })) - server.listen(options.port) log.info('Listening on port %d', options.port) } diff --git a/lib/units/api/middleware/auth.js b/lib/units/api/middleware/auth.js deleted file mode 100644 index 742c5884e2..0000000000 --- a/lib/units/api/middleware/auth.js +++ /dev/null @@ -1,65 +0,0 @@ -var jwtutil = require('../../../util/jwtutil') -var urlutil = require('../../../util/urlutil') -var logger = require('../../../util/logger') -var dbapi = require('../../../db/api') - -module.exports = function(options) { - return function(req, res, next) { - - var log = logger.createLogger('api:auth') - - if (req.headers.authorization) { - var tokenId = req.headers.authorization.split(" ")[1] - - if (tokenId) { - dbapi.loadAccessToken(tokenId) - .then(function(token) { - var jwt = token.jwt - , data = jwtutil.decode(jwt, options.secret) - - if (data) { - dbapi.loadUser(data.email) - .then(function(user) { - if (user) { - req.user = user - next() - } - }) - } - }) - .catch(function(err) { - log.error('Failed to load token: ', err.stack) - res.json(500, { - success: false, - description: "Bad Access Token" - }) - }) - } else { - log.error("Bad Access Token") - res.json(500, { - success: false, - description: "Bad Access Token Header" - }) - } - } - // TODO: Remove this once frontend become stateless - else if (req.session && req.session.jwt) { - dbapi.loadUser(req.session.jwt.email) - .then(function(user) { - if (user) { - req.user = user - next() - } - else { - // We no longer have the user in the database - res.redirect(options.authUrl) - } - }) - .catch(next) - } - else { - // No session, forward to auth client - res.redirect(options.authUrl) - } - } -} diff --git a/lib/units/api/swagger/api_v1.yaml b/lib/units/api/swagger/api_v1.yaml index e44c2b958e..23df5f0d0a 100644 --- a/lib/units/api/swagger/api_v1.yaml +++ b/lib/units/api/swagger/api_v1.yaml @@ -1,14 +1,15 @@ swagger: "2.0" info: - version: "1.0.10" + version: "2.0.0" title: Smartphone Test Farm - description: Control and manager real Smartphone devices from browser and apis + description: Control and manager real Smartphone devices from browser and restful apis license: name: Apache-2.0 url: http://www.apache.org/licenses/LICENSE-2.0 contact: - url: http://openstf.io/ + name: STF Support email: contact@openstf.io + url: http://openstf.io/ basePath: /api/v1 schemes: - http @@ -17,6 +18,11 @@ consumes: - application/json produces: - application/json +tags: + - name: user + description: User Operations + - name: device + description: Device Operations paths: /me: x-swagger-router-controller: user @@ -24,6 +30,8 @@ paths: summary: User Profile description: The User Profile endpoint returns information about current authorized user. operationId: getCurrentUser + tags: + - user responses: "200": description: Current User Profile information @@ -33,6 +41,8 @@ paths: description: Unexpected Error schema: $ref: "#/definitions/ErrorResponse" + security: + - accessTokenAuth: [] # TODO: Change group endpoint with something more easy to understandable endpoint /group: x-swagger-router-controller: user @@ -40,21 +50,27 @@ paths: summary: User Group description: The User Group endpoint returns information about user group of current authorized user. operationId: getCurrentUserGroup + tags: + - user responses: "200": - description: Current User Group information + description: Current User's Group information schema: $ref: "#/definitions/GroupResponse" default: description: Unexpected Error schema: $ref: "#/definitions/ErrorResponse" + security: + - accessTokenAuth: [] /accessTokens: x-swagger-router-controller: token get: summary: Access Tokens - description: Return Current User Access Tokens titles + description: The Access Tokens endpoints returns titles of all the valid access tokens. operationId: getAccessTokens + tags: + - user responses: "200": description: Access Tokens titles @@ -64,12 +80,16 @@ paths: description: Unexpected Error schema: $ref: "#/definitions/ErrorResponse" + security: + - accessTokenAuth: [] /devices: x-swagger-router-controller: device get: summary: Device List - description: List of all the STF devices including Disconnected and Offline + description: The devices enpoint return list of all the STF devices including Disconnected and Offline operationId: getDevices + tags: + - device responses: "200": description: List of Devices @@ -79,12 +99,16 @@ paths: description: Unexpected Error schema: $ref: "#/definitions/ErrorResponse" + security: + - accessTokenAuth: [] /devices/{serial}: x-swagger-router-controller: device get: summary: Device Information - description: Device Information + description: The device enpoint return information about a single device. operationId: getDeviceBySerial + tags: + - device parameters: - name: serial in: path @@ -100,6 +124,8 @@ paths: description: Unexpected Error schema: $ref: "#/definitions/ErrorResponse" + security: + - accessTokenAuth: [] /swagger.json: x-swagger-pipe: swagger_raw definitions: @@ -145,3 +171,9 @@ definitions: properties: message: type: string + +securityDefinitions: + accessTokenAuth: + type: apiKey + name: accessTokenAuth + in: header From 536e469e6fca2b0dcfbce883f3cafa887b36bdaa Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Fri, 4 Dec 2015 16:42:11 +0900 Subject: [PATCH 09/37] remove unnecessary options from api unit. Improve error messages... etc etc --- lib/cli.js | 13 ------------ lib/units/api/controllers/device.js | 4 ++-- lib/units/api/controllers/token.js | 4 ++-- lib/units/api/controllers/user.js | 4 ++-- lib/units/api/helpers/securityHandlers.js | 25 ++++++++++++++--------- 5 files changed, 21 insertions(+), 29 deletions(-) diff --git a/lib/cli.js b/lib/cli.js index b7a8345608..74909998c1 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -871,22 +871,15 @@ program , 'secret (or $SECRET)' , String , process.env.SECRET) - .option('-a, --auth-url ' - , 'URL to auth client' - , String) .action(function(options) { if (!options.secret) { this.missingArgument('--secret') } - if (!options.authUrl) { - this.missingArgument('--auth-url') - } require('./units/api')({ port: options.port , ssid: options.ssid , secret: options.secret - , authUrl: options.authUrl }) }) @@ -1335,12 +1328,6 @@ program 'api' , '--port', options.apiPort , '--secret', options.authSecret - , '--auth-url', options.authUrl || util.format( - 'http://%s:%d/auth/%s/' - , options.publicIp - , options.poorxyPort - , ({oauth2: 'oauth'}[options.authType]) || options.authType - ) ]) // websocket , procutil.fork(__filename, [ diff --git a/lib/units/api/controllers/device.js b/lib/units/api/controllers/device.js index b830fd4f44..16ef755597 100644 --- a/lib/units/api/controllers/device.js +++ b/lib/units/api/controllers/device.js @@ -4,12 +4,12 @@ var dbapi = require('../../../db/api') var logger = require('../../../util/logger') var datautil = require('../../../util/datautil') -var log = logger.createLogger('api:contoller:device') +var log = logger.createLogger('api:controllers:device') module.exports = { getDevices: getDevices , getDeviceBySerial: getDeviceBySerial -}; +} function getDevices(req, res) { dbapi.loadDevices() diff --git a/lib/units/api/controllers/token.js b/lib/units/api/controllers/token.js index d83dca2a3c..8452f84eef 100644 --- a/lib/units/api/controllers/token.js +++ b/lib/units/api/controllers/token.js @@ -3,11 +3,11 @@ var Promise = require('bluebird') var dbapi = require('../../../db/api') var logger = require('../../../util/logger') -var log = logger.createLogger('api:contoller:token') +var log = logger.createLogger('api:controllers:token') module.exports = { getAccessTokens: getAccessTokens -}; +} function getAccessTokens(req, res) { dbapi.loadAccessTokens(req.user.email) diff --git a/lib/units/api/controllers/user.js b/lib/units/api/controllers/user.js index 537e0099e9..01a3ee5235 100644 --- a/lib/units/api/controllers/user.js +++ b/lib/units/api/controllers/user.js @@ -4,12 +4,12 @@ var dbapi = require('../../../db/api') var logger = require('../../../util/logger') var datautil = require('../../../util/datautil') -var log = logger.createLogger('api:contoller:user') +var log = logger.createLogger('api:controllers:user') module.exports = { getCurrentUser: getCurrentUser , getCurrentUserGroup: getCurrentUserGroup -}; +} function getCurrentUser(req, res) { res.json({ diff --git a/lib/units/api/helpers/securityHandlers.js b/lib/units/api/helpers/securityHandlers.js index 9dee2bd9df..7843ab8c5d 100644 --- a/lib/units/api/helpers/securityHandlers.js +++ b/lib/units/api/helpers/securityHandlers.js @@ -3,7 +3,7 @@ var urlutil = require('../../../util/urlutil') var logger = require('../../../util/logger') var dbapi = require('../../../db/api') -var log = logger.createLogger('api:auth') +var log = logger.createLogger('api:helpers:securityHandlers') module.exports = { accessTokenAuth: accessTokenAuth @@ -27,24 +27,30 @@ function accessTokenAuth(req, res, next) { next() } }) + } else { + res.json(500, { + success: false + }) } }) .catch(function(err) { log.error('Failed to load token: ', err.stack) - res.json(500, { + res.json(401, { success: false, - description: "Bad Access Token" + description: 'Bad credentials' }) }) } else { - log.error("Bad Access Token Header") - res.json(500, { + log.error('Bad Access Token Header') + res.json(401, { success: false, - description: "Bad Access Token Header" + description: 'Bad credentials' }) } } + // Request is coming from browser app // TODO: Remove this once frontend become stateless + // and start sending request without session else if (req.session && req.session.jwt) { dbapi.loadUser(req.session.jwt.email) .then(function(user) { @@ -54,17 +60,16 @@ function accessTokenAuth(req, res, next) { } else { res.json(500, { - success: false, - description: "Bad Request" + success: false }) } }) .catch(next) } else { - res.json(500, { + res.json(401, { success: false, - description: "Request does not have Authorization header" + description: 'Requires authentication' }) } } From 04d410bc1d6721006a4929569067479cb4ca7eb3 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Fri, 4 Dec 2015 16:59:14 +0900 Subject: [PATCH 10/37] Update DEPLOYMENT doc with api unit --- doc/DEPLOYMENT.md | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/doc/DEPLOYMENT.md b/doc/DEPLOYMENT.md index c306daeeb6..d9c1b65961 100644 --- a/doc/DEPLOYMENT.md +++ b/doc/DEPLOYMENT.md @@ -55,6 +55,7 @@ The app role can contain any of the following units. You may distribute them as * [stf-triproxy-app.service](#stf-triproxy-appservice) * [stf-triproxy-dev.service](#stf-triproxy-devservice) * [stf-websocket@.service](#stf-websocketservice) +* [stf-api@.service](#stf-apiservice) ### Database role @@ -179,7 +180,7 @@ These units are required for proper operation of STF. Unless mentioned otherwise **Requires** the `rethinkdb-proxy-28015.service` unit on the same host. -The app unit provides the main HTTP server and currently a very, very modest API for the client-side. It also serves all static resources including images, scripts and stylesheets. +The app unit provides the main HTTP server and it serves all static resources including images, scripts and stylesheets. This is a template unit, meaning that you'll need to start it with an instance identifier. In this example configuration the identifier is used to specify the exposed port number (i.e. `stf-app@3100.service` runs on port 3100). You can have multiple instances running on the same host by using different ports. @@ -653,6 +654,37 @@ ExecStart=/usr/bin/docker run --rm \ ExecStop=/usr/bin/docker stop -t 10 %p-%i ``` +### `stf-api@.service` + +**Requires** the `rethinkdb-proxy-28015.service` unit on the same host. + +The api unit provides all the major RESTful APIs for STF. Users can generate their personal access token from STF UI and can use that token to access these api from any interface. + +This is a template unit, meaning that you'll need to start it with an instance identifier. In this example configuration the identifier is used to specify the exposed port number (i.e. `stf-api@3700.service` runs on port 3700). You can have multiple instances running on the same host by using different ports. + +```ini +[Unit] +Description=STF api +After=rethinkdb-proxy-28015.service +BindsTo=rethinkdb-proxy-28015.service + +[Service] +EnvironmentFile=/etc/environment +TimeoutStartSec=0 +Restart=always +ExecStartPre=/usr/bin/docker pull openstf/stf:latest +ExecStartPre=-/usr/bin/docker kill %p-%i +ExecStartPre=-/usr/bin/docker rm %p-%i +ExecStart=/usr/bin/docker run --rm \ + --name %p-%i \ + --link rethinkdb-proxy-28015:rethinkdb \ + -e "SECRET=YOUR_SESSION_SECRET_HERE" \ + -p %i:3000 \ + openstf/stf:latest \ + stf app --port 3000 +ExecStop=-/usr/bin/docker stop -t 10 %p-%i +``` + ## Optional units These units are optional and don't affect the way STF works in any way. @@ -836,6 +868,10 @@ http { server 192.168.255.100:3600 max_fails=0; } + upstream stf_api { + server 192.168.255.100:3700 max_fails=0; + } + types { application/javascript js; image/gif gif; @@ -904,6 +940,10 @@ http { proxy_pass http://stf_auth/auth/; } + location /api/ { + proxy_pass http://stf_api/api/; + } + location /s/image/ { proxy_pass http://stf_storage_image; } From dccacea43ff81a621aea7f38a896be1d21681a97 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Thu, 3 Dec 2015 19:55:26 +0900 Subject: [PATCH 11/37] add new device-reserve and device-release endpoints --- lib/cli.js | 10 +++ lib/units/api/controllers/device.js | 102 ++++++++++++++++++++++++++++ lib/units/api/index.js | 63 +++++++++++------ lib/units/api/swagger/api_v1.yaml | 52 ++++++++++++++ 4 files changed, 207 insertions(+), 20 deletions(-) diff --git a/lib/cli.js b/lib/cli.js index 74909998c1..25e323e275 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -871,15 +871,24 @@ program , 'secret (or $SECRET)' , String , process.env.SECRET) + .option('-c, --connect-push ' + , 'push endpoint' + , cliutil.list) .action(function(options) { if (!options.secret) { this.missingArgument('--secret') } + if (!options.connectPush) { + this.missingArgument('--connect-push') + } require('./units/api')({ port: options.port , ssid: options.ssid , secret: options.secret + , endpoints: { + push: options.connectPush + } }) }) @@ -1328,6 +1337,7 @@ program 'api' , '--port', options.apiPort , '--secret', options.authSecret + , '--connect-push', options.bindAppPull ]) // websocket , procutil.fork(__filename, [ diff --git a/lib/units/api/controllers/device.js b/lib/units/api/controllers/device.js index 16ef755597..79473c0f4f 100644 --- a/lib/units/api/controllers/device.js +++ b/lib/units/api/controllers/device.js @@ -5,12 +5,18 @@ var logger = require('../../../util/logger') var datautil = require('../../../util/datautil') var log = logger.createLogger('api:controllers:device') +var wire = require('../../../wire') +var wireutil = require('../../../wire/util') module.exports = { getDevices: getDevices , getDeviceBySerial: getDeviceBySerial +, reserveDeviceBySerial: reserveDeviceBySerial +, releaseDeviceBySerial: releaseDeviceBySerial } +var log = logger.createLogger('api:contoller:device') + function getDevices(req, res) { dbapi.loadDevices() .then(function(cursor) { @@ -59,3 +65,99 @@ function getDeviceBySerial(req, res) { }) }) } + +function reserveDeviceBySerial(req, res) { + var serial = req.swagger.params.serial.value + + dbapi.loadDevice(serial) + .then(function(device) { + if (device) { + datautil.normalize(device, req.user) + if(!device.using) { + + var requirements = { + 'serial': { + 'value': serial, + 'match': 'exact' + } + } + + req.options.push.send([ + device.channel + , wireutil.envelope( + new wire.GroupMessage( + new wire.OwnerMessage( + req.user.email + , req.user.name + , req.user.group + ) + , null + , wireutil.toDeviceRequirements(requirements) + ) + ) + ]) + + res.json({ + success: true + , device: device + }) + + } else { + res.json(500, { + success: false + , description: 'Device is being used' + }) + } + } else { + res.json(500, { + success: false + , description: 'Bad device serial' + }) + } + }) +} + +function releaseDeviceBySerial(req, res) { + var serial = req.swagger.params.serial.value + + dbapi.loadDevice(serial) + .then(function(device) { + if (device) { + datautil.normalize(device, req.user) + if(device.using && device.owner.email == req.user.email) { + + var requirements = { + 'serial': { + 'value': serial, + 'match': 'exact' + } + } + + req.options.push.send([ + device.channel + , wireutil.envelope( + new wire.UngroupMessage( + wireutil.toDeviceRequirements(requirements) + ) + ) + ]) + + res.json({ + success: true + , device: device + }) + + } else { + res.json(500, { + success: false + , description: 'You cannot kick this device' + }) + } + } else { + res.json(500, { + success: false + , description: 'Bad device serial' + }) + } + }) +} diff --git a/lib/units/api/index.js b/lib/units/api/index.js index dd9f51a4ce..16fc171105 100644 --- a/lib/units/api/index.js +++ b/lib/units/api/index.js @@ -4,38 +4,61 @@ var path = require('path') var express = require('express') var SwaggerExpress = require('swagger-express-mw') var cookieSession = require('cookie-session') +var Promise = require('bluebird') +var _ = require('lodash') var logger = require('../../util/logger') +var zmqutil = require('../../util/zmqutil') +var srv = require('../../util/srv') +var lifecycle = require('../../util/lifecycle') module.exports = function(options) { var log = logger.createLogger('api') , app = express() , server = http.createServer(app) - var config = { - appRoot: __dirname - , swaggerFile: path.resolve(__dirname, 'swagger', 'api_v1.yaml') - }; + var push = zmqutil.socket('push') + Promise.map(options.endpoints.push, function(endpoint) { + return srv.resolve(endpoint).then(function(records) { + return srv.attempt(records, function(record) { + log.info('Sending output to "%s"', record.url) + push.connect(record.url) + return Promise.resolve(true) + }) + }) + }) + .catch(function(err) { + log.fatal('Unable to connect to push endpoint', err) + lifecycle.fatal() + }) - SwaggerExpress.create(config, function(err, swaggerExpress) { - if (err) { throw err; } + var config = { + appRoot: __dirname + , swaggerFile: path.resolve(__dirname, 'swagger', 'api_v1.yaml') + }; - swaggerExpress.register(app); - }) + SwaggerExpress.create(config, function(err, swaggerExpress) { + if (err) { throw err; } + swaggerExpress.register(app); + }) - // Adding options in request, so that swagger controller - // can use it. - app.use(function(req, res, next) { - req.options = options - next() + // Adding options in request, so that swagger controller + // can use it. + app.use(function(req, res, next) { + var reqOptions = _.merge(options, { + 'push': push }) - // TODO: Remove this once frontend is stateless - app.use(cookieSession({ - name: options.ssid - , keys: [options.secret] - })) + req.options = reqOptions + next() + }) + + // TODO: Remove this once frontend is stateless + app.use(cookieSession({ + name: options.ssid + , keys: [options.secret] + })) - server.listen(options.port) - log.info('Listening on port %d', options.port) + server.listen(options.port) + log.info('Listening on port %d', options.port) } diff --git a/lib/units/api/swagger/api_v1.yaml b/lib/units/api/swagger/api_v1.yaml index 23df5f0d0a..0af4f604ba 100644 --- a/lib/units/api/swagger/api_v1.yaml +++ b/lib/units/api/swagger/api_v1.yaml @@ -128,6 +128,58 @@ paths: - accessTokenAuth: [] /swagger.json: x-swagger-pipe: swagger_raw + /devices/{serial}/reserve: + x-swagger-router-controller: device + put: + summary: Reseve Device + description: The device reserve enpoint will reserve a device if device it usable + operationId: reserveDeviceBySerial + tags: + - device + parameters: + - name: serial + in: path + description: Device Serial + required: true + type: string + responses: + "200": + description: Device Information + schema: + $ref: "#/definitions/DeviceResponse" + default: + description: Unexpected Error + schema: + $ref: "#/definitions/ErrorResponse" + security: + - accessTokenAuth: [] + /devices/{serial}/release: + x-swagger-router-controller: device + put: + summary: Device Information + description: The device reserve enpoint will release a device + operationId: releaseDeviceBySerial + tags: + - device + parameters: + - name: serial + in: path + description: Device Serial + required: true + type: string + responses: + "200": + description: Device Information + schema: + $ref: "#/definitions/DeviceResponse" + default: + description: Unexpected Error + schema: + $ref: "#/definitions/ErrorResponse" + security: + - accessTokenAuth: [] + /swagger.json: + x-swagger-pipe: swagger_raw definitions: UserResponse: required: From bca180d284059686c802d303e413e7f16259c928 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Mon, 7 Dec 2015 17:49:34 +0900 Subject: [PATCH 12/37] refactor reserve and release endpoints in HTTP POST and DELETE methods --- lib/db/api.js | 2 +- lib/units/api/controllers/device.js | 107 +------------ lib/units/api/controllers/user.js | 107 ++++++++++++- lib/units/api/swagger/api_v1.yaml | 143 +++++++++--------- .../components/stf/device/device-service.js | 2 +- .../stf/tokens/access-token-service.js | 2 +- 6 files changed, 189 insertions(+), 174 deletions(-) diff --git a/lib/db/api.js b/lib/db/api.js index 65c5978fcd..85389bf78d 100644 --- a/lib/db/api.js +++ b/lib/db/api.js @@ -121,7 +121,7 @@ dbapi.lookupUserByVncAuthResponse = function(response, serial) { }) } -dbapi.loadGroup = function(email) { +dbapi.loadUserDevices = function(email) { return db.run(r.table('devices').getAll(email, { index: 'owner' })) diff --git a/lib/units/api/controllers/device.js b/lib/units/api/controllers/device.js index 79473c0f4f..a7190c392a 100644 --- a/lib/units/api/controllers/device.js +++ b/lib/units/api/controllers/device.js @@ -1,18 +1,15 @@ var Promise = require('bluebird') +var _ = require('lodash') var dbapi = require('../../../db/api') var logger = require('../../../util/logger') var datautil = require('../../../util/datautil') var log = logger.createLogger('api:controllers:device') -var wire = require('../../../wire') -var wireutil = require('../../../wire/util') module.exports = { getDevices: getDevices , getDeviceBySerial: getDeviceBySerial -, reserveDeviceBySerial: reserveDeviceBySerial -, releaseDeviceBySerial: releaseDeviceBySerial } var log = logger.createLogger('api:contoller:device') @@ -42,11 +39,17 @@ function getDevices(req, res) { function getDeviceBySerial(req, res) { var serial = req.swagger.params.serial.value + var fields = req.swagger.params.fields.value dbapi.loadDevice(serial) .then(function(device) { if (device) { datautil.normalize(device, req.user) + + if(fields) { + device = _.pick(device, fields.split(',')) + } + res.json({ success: true , device: device @@ -65,99 +68,3 @@ function getDeviceBySerial(req, res) { }) }) } - -function reserveDeviceBySerial(req, res) { - var serial = req.swagger.params.serial.value - - dbapi.loadDevice(serial) - .then(function(device) { - if (device) { - datautil.normalize(device, req.user) - if(!device.using) { - - var requirements = { - 'serial': { - 'value': serial, - 'match': 'exact' - } - } - - req.options.push.send([ - device.channel - , wireutil.envelope( - new wire.GroupMessage( - new wire.OwnerMessage( - req.user.email - , req.user.name - , req.user.group - ) - , null - , wireutil.toDeviceRequirements(requirements) - ) - ) - ]) - - res.json({ - success: true - , device: device - }) - - } else { - res.json(500, { - success: false - , description: 'Device is being used' - }) - } - } else { - res.json(500, { - success: false - , description: 'Bad device serial' - }) - } - }) -} - -function releaseDeviceBySerial(req, res) { - var serial = req.swagger.params.serial.value - - dbapi.loadDevice(serial) - .then(function(device) { - if (device) { - datautil.normalize(device, req.user) - if(device.using && device.owner.email == req.user.email) { - - var requirements = { - 'serial': { - 'value': serial, - 'match': 'exact' - } - } - - req.options.push.send([ - device.channel - , wireutil.envelope( - new wire.UngroupMessage( - wireutil.toDeviceRequirements(requirements) - ) - ) - ]) - - res.json({ - success: true - , device: device - }) - - } else { - res.json(500, { - success: false - , description: 'You cannot kick this device' - }) - } - } else { - res.json(500, { - success: false - , description: 'Bad device serial' - }) - } - }) -} diff --git a/lib/units/api/controllers/user.js b/lib/units/api/controllers/user.js index 01a3ee5235..26d5310c86 100644 --- a/lib/units/api/controllers/user.js +++ b/lib/units/api/controllers/user.js @@ -3,12 +3,16 @@ var Promise = require('bluebird') var dbapi = require('../../../db/api') var logger = require('../../../util/logger') var datautil = require('../../../util/datautil') +var wire = require('../../../wire') +var wireutil = require('../../../wire/util') var log = logger.createLogger('api:controllers:user') module.exports = { getCurrentUser: getCurrentUser -, getCurrentUserGroup: getCurrentUserGroup +, getCurrentUserDevices: getCurrentUserDevices +, addDeviceToUser: addDeviceToUser +, deleteDeviceFromUser: deleteDeviceFromUser } function getCurrentUser(req, res) { @@ -18,8 +22,8 @@ function getCurrentUser(req, res) { }) } -function getCurrentUserGroup(req, res) { - dbapi.loadGroup(req.user.email) +function getCurrentUserDevices(req, res) { + dbapi.loadUserDevices(req.user.email) .then(function(cursor) { return Promise.promisify(cursor.toArray, cursor)() .then(function(list) { @@ -39,3 +43,100 @@ function getCurrentUserGroup(req, res) { }) }) } + +function addDeviceToUser(req, res) { + var serial = req.body.serial + var timeout = req.body.timeout || null + + dbapi.loadDevice(serial) + .then(function(device) { + if (device) { + datautil.normalize(device, req.user) + if(device.ready && !device.using) { + + var requirements = { + 'serial': { + 'value': serial, + 'match': 'exact' + } + } + + req.options.push.send([ + device.channel + , wireutil.envelope( + new wire.GroupMessage( + new wire.OwnerMessage( + req.user.email + , req.user.name + , req.user.group + ) + , timeout + , wireutil.toDeviceRequirements(requirements) + ) + ) + ]) + + res.json(202, { + success: true + , description: 'Device Add request is accepted' + }) + + } else { + res.json(500, { + success: false + , description: 'Device is being used or not available' + }) + } + } else { + res.json(500, { + success: false + , description: 'Bad device serial' + }) + } + }) +} + +function deleteDeviceFromUser(req, res) { + var serial = req.body.serial + + dbapi.loadDevice(serial) + .then(function(device) { + if (device) { + datautil.normalize(device, req.user) + if(device.using && device.owner.email == req.user.email) { + + var requirements = { + 'serial': { + 'value': serial, + 'match': 'exact' + } + } + + req.options.push.send([ + device.channel + , wireutil.envelope( + new wire.UngroupMessage( + wireutil.toDeviceRequirements(requirements) + ) + ) + ]) + + res.json(202, { + success: true + , description: 'Device Release request is accepted' + }) + + } else { + res.json(500, { + success: false + , description: 'You cannot kick this device' + }) + } + } else { + res.json(500, { + success: false + , description: 'Bad device serial' + }) + } + }) +} diff --git a/lib/units/api/swagger/api_v1.yaml b/lib/units/api/swagger/api_v1.yaml index 0af4f604ba..dc6138afdc 100644 --- a/lib/units/api/swagger/api_v1.yaml +++ b/lib/units/api/swagger/api_v1.yaml @@ -24,7 +24,7 @@ tags: - name: device description: Device Operations paths: - /me: + /user: x-swagger-router-controller: user get: summary: User Profile @@ -43,27 +43,70 @@ paths: $ref: "#/definitions/ErrorResponse" security: - accessTokenAuth: [] -# TODO: Change group endpoint with something more easy to understandable endpoint - /group: + /user/devices: x-swagger-router-controller: user get: - summary: User Group - description: The User Group endpoint returns information about user group of current authorized user. - operationId: getCurrentUserGroup + summary: List devices owned by current user + description: The User Devices endpoint returns information about user group of current authorized user. + operationId: getCurrentUserDevices tags: - user responses: "200": - description: Current User's Group information + description: Current User Devices information schema: - $ref: "#/definitions/GroupResponse" + $ref: "#/definitions/DeviceListResponse" + default: + description: Unexpected Error + schema: + $ref: "#/definitions/ErrorResponse" + security: + - accessTokenAuth: [] + post: + summary: Add device to a user + description: The User Devices endpoint will request stf server for a new device. It will return request accepted if device is usable. + operationId: addDeviceToUser + tags: + - user + parameters: + - name: device + in: body + description: Device to add + required: true + schema: + $ref: "#/definitions/DeviceAddPayload" + responses: + "202": + description: Device Add Request Status + default: + description: Unexpected Error + schema: + $ref: "#/definitions/ErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Release device from user + description: The User Devices endpoint will request for device release from stf server. + operationId: deleteDeviceFromUser + tags: + - user + parameters: + - name: device + in: body + description: Device to add + required: true + schema: + $ref: "#/definitions/DeviceDeletePayload" + responses: + "202": + description: Device Release Request Status default: description: Unexpected Error schema: $ref: "#/definitions/ErrorResponse" security: - accessTokenAuth: [] - /accessTokens: + /user/accessTokens: x-swagger-router-controller: token get: summary: Access Tokens @@ -115,57 +158,10 @@ paths: description: Device Serial required: true type: string - responses: - "200": - description: Device Information - schema: - $ref: "#/definitions/DeviceResponse" - default: - description: Unexpected Error - schema: - $ref: "#/definitions/ErrorResponse" - security: - - accessTokenAuth: [] - /swagger.json: - x-swagger-pipe: swagger_raw - /devices/{serial}/reserve: - x-swagger-router-controller: device - put: - summary: Reseve Device - description: The device reserve enpoint will reserve a device if device it usable - operationId: reserveDeviceBySerial - tags: - - device - parameters: - - name: serial - in: path - description: Device Serial - required: true - type: string - responses: - "200": - description: Device Information - schema: - $ref: "#/definitions/DeviceResponse" - default: - description: Unexpected Error - schema: - $ref: "#/definitions/ErrorResponse" - security: - - accessTokenAuth: [] - /devices/{serial}/release: - x-swagger-router-controller: device - put: - summary: Device Information - description: The device reserve enpoint will release a device - operationId: releaseDeviceBySerial - tags: - - device - parameters: - - name: serial - in: path - description: Device Serial - required: true + - name: fields + in: query + description: Fields query parameter takes a comma seperated list of fields. Only listed field will be return in response + required: false type: string responses: "200": @@ -187,14 +183,6 @@ definitions: properties: user: type: object - GroupResponse: - required: - - devices - properties: - devices: - type: array - items: - type: object AccessTokensResponse: required: - tokens @@ -223,6 +211,25 @@ definitions: properties: message: type: string + DeviceAddPayload: + description: payload object for adding device to user + required: + - serial + properties: + serial: + description: Device Serial + type: string + timeout: + description: Device timeout in ms. If device is kept idle for this period, it will be automatically disconnected. Default is provider group timeout. + type: integer + DeviceDeletePayload: + description: payload object for deleting device from user + required: + - serial + properties: + serial: + description: Device Serial + type: string securityDefinitions: accessTokenAuth: diff --git a/res/app/components/stf/device/device-service.js b/res/app/components/stf/device/device-service.js index a01df982bc..833962ede3 100644 --- a/res/app/components/stf/device/device-service.js +++ b/res/app/components/stf/device/device-service.js @@ -181,7 +181,7 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi , digest: true }) - oboe('/api/v1/group') + oboe('/api/v1/user/devices') .node('devices[*]', function (device) { tracker.add(device) }) diff --git a/res/app/components/stf/tokens/access-token-service.js b/res/app/components/stf/tokens/access-token-service.js index a815b828a9..214aa8cfed 100644 --- a/res/app/components/stf/tokens/access-token-service.js +++ b/res/app/components/stf/tokens/access-token-service.js @@ -6,7 +6,7 @@ module.exports = function AccessTokenServiceFactory( var AccessTokenService = {} AccessTokenService.getAccessTokens = function() { - return $http.get('/api/v1/accessTokens') + return $http.get('/api/v1/user/accessTokens') } AccessTokenService.generateAccessToken = function(title) { From c9a9d3cb067b0d6346bdcc94b7cbb17c764f1098 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Mon, 7 Dec 2015 18:39:06 +0900 Subject: [PATCH 13/37] User path parameter for Delete /user/devices/:device endpoint --- lib/units/api/controllers/user.js | 2 +- lib/units/api/swagger/api_v1.yaml | 19 ++++++------------- 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/lib/units/api/controllers/user.js b/lib/units/api/controllers/user.js index 26d5310c86..779b4a7037 100644 --- a/lib/units/api/controllers/user.js +++ b/lib/units/api/controllers/user.js @@ -97,7 +97,7 @@ function addDeviceToUser(req, res) { } function deleteDeviceFromUser(req, res) { - var serial = req.body.serial + var serial = req.swagger.params.serial.value dbapi.loadDevice(serial) .then(function(device) { diff --git a/lib/units/api/swagger/api_v1.yaml b/lib/units/api/swagger/api_v1.yaml index dc6138afdc..bf7b380fb4 100644 --- a/lib/units/api/swagger/api_v1.yaml +++ b/lib/units/api/swagger/api_v1.yaml @@ -84,6 +84,8 @@ paths: $ref: "#/definitions/ErrorResponse" security: - accessTokenAuth: [] + /user/devices/{serial}: + x-swagger-router-controller: user delete: summary: Release device from user description: The User Devices endpoint will request for device release from stf server. @@ -91,12 +93,11 @@ paths: tags: - user parameters: - - name: device - in: body - description: Device to add + - name: serial + in: path + description: Device Serial required: true - schema: - $ref: "#/definitions/DeviceDeletePayload" + type: string responses: "202": description: Device Release Request Status @@ -222,14 +223,6 @@ definitions: timeout: description: Device timeout in ms. If device is kept idle for this period, it will be automatically disconnected. Default is provider group timeout. type: integer - DeviceDeletePayload: - description: payload object for deleting device from user - required: - - serial - properties: - serial: - description: Device Serial - type: string securityDefinitions: accessTokenAuth: From 232163d290753ffcac1dbae5210caebe88764ac2 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Mon, 7 Dec 2015 19:36:56 +0900 Subject: [PATCH 14/37] add remoteDebugUrl into database so that it is accessible from api --- lib/db/api.js | 16 ++++++++++++++++ lib/units/device/plugins/connect.js | 17 +++++++++++++++++ lib/units/processor/index.js | 6 ++++++ lib/util/datautil.js | 10 ++++++++++ lib/wire/wire.proto | 11 +++++++++++ 5 files changed, 60 insertions(+) diff --git a/lib/db/api.js b/lib/db/api.js index 85389bf78d..c1aa36b8fd 100644 --- a/lib/db/api.js +++ b/lib/db/api.js @@ -151,6 +151,8 @@ dbapi.saveDeviceInitialState = function(serial, device) { , statusChangedAt: r.now() , ready: false , reverseForwards: [] + , remoteConnect: false + , remoteDebugUrl: null } return db.run(r.table('devices').get(serial).update(data)) .then(function(stats) { @@ -163,6 +165,20 @@ dbapi.saveDeviceInitialState = function(serial, device) { }) } +dbapi.setDeviceConnectUrl = function(serial, url) { + return db.run(r.table('devices').get(serial).update({ + remoteDebugUrl: url + , remoteConnect: true + })) +} + +dbapi.unsetDeviceConnectUrl = function(serial, url) { + return db.run(r.table('devices').get(serial).update({ + remoteDebugUrl: null + , remoteConnect: false + })) +} + dbapi.saveDeviceStatus = function(serial, status) { return db.run(r.table('devices').get(serial).update({ status: status diff --git a/lib/units/device/plugins/connect.js b/lib/units/device/plugins/connect.js index d058b176ae..55d93ef6f0 100644 --- a/lib/units/device/plugins/connect.js +++ b/lib/units/device/plugins/connect.js @@ -142,6 +142,15 @@ module.exports = syrup.serial() channel , reply.okay(url) ]) + // Update DB + push.send([ + wireutil.global + , wireutil.envelope(new wire.ConnectStartedMessage( + options.serial + , url + )) + ]) + log.info('Remote Connect Started for device "%s" at "%s"', options.serial, url) }) .catch(function(err) { log.error('Unable to start remote connect service', err.stack) @@ -159,6 +168,14 @@ module.exports = syrup.serial() channel , reply.okay() ]) + // Update DB + push.send([ + wireutil.global + , wireutil.envelope(new wire.ConnectStoppedMessage( + options.serial + )) + ]) + log.info('Remote Connect Stopped for device "%s"', options.serial) }) .catch(function(err) { log.error('Failed to stop connect service', err.stack) diff --git a/lib/units/processor/index.js b/lib/units/processor/index.js index 7b022d91a7..733e68524b 100644 --- a/lib/units/processor/index.js +++ b/lib/units/processor/index.js @@ -165,6 +165,12 @@ module.exports = function(options) { ) }) }) + .on(wire.ConnectStartedMessage, function(channel, message, data) { + dbapi.setDeviceConnectUrl(message.serial, message.url) + }) + .on(wire.ConnectStoppedMessage, function(channel, message, data) { + dbapi.unsetDeviceConnectUrl(message.serial) + }) .on(wire.JoinGroupMessage, function(channel, message, data) { dbapi.setDeviceOwner(message.serial, message.owner) appDealer.send([channel, data]) diff --git a/lib/util/datautil.js b/lib/util/datautil.js index dfbe6b0011..8b04b3687f 100644 --- a/lib/util/datautil.js +++ b/lib/util/datautil.js @@ -54,10 +54,20 @@ datautil.applyOwner = function(device, user) { return device } +// Only owner can see this information +datautil.applyOwnerOnlyInfo = function(device, user) { + if (device.owner && device.owner.email === user.email) { + } else { + device.remoteConnect = false + device.remoteDebugUrl = null + } +} + datautil.normalize = function(device, user) { datautil.applyData(device) datautil.applyBrowsers(device) datautil.applyOwner(device, user) + datautil.applyOwnerOnlyInfo(device, user) if (!device.present) { device.owner = null } diff --git a/lib/wire/wire.proto b/lib/wire/wire.proto index 868e507eef..52215f095d 100644 --- a/lib/wire/wire.proto +++ b/lib/wire/wire.proto @@ -76,6 +76,17 @@ enum MessageType { ReverseForwardsEvent = 72; FileSystemListMessage = 81; FileSystemGetMessage = 82; + ConnectStartedMessage = 92; + ConnectStoppedMessage = 93; +} + +message ConnectStartedMessage { + required string serial = 1; + required string url = 2; +} + +message ConnectStoppedMessage { + required string serial = 1; } message FileSystemListMessage { From 96494247bb8c55adfaa1dc72c3936800e071601a Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Mon, 7 Dec 2015 22:26:11 +0900 Subject: [PATCH 15/37] add remoteConnect endpoint for device --- lib/units/api/controllers/user.js | 131 +++++++++++++++++++++++++++++- lib/units/api/swagger/api_v1.yaml | 72 ++++++++++++++++ 2 files changed, 202 insertions(+), 1 deletion(-) diff --git a/lib/units/api/controllers/user.js b/lib/units/api/controllers/user.js index 779b4a7037..b0cb4df92c 100644 --- a/lib/units/api/controllers/user.js +++ b/lib/units/api/controllers/user.js @@ -13,6 +13,9 @@ module.exports = { , getCurrentUserDevices: getCurrentUserDevices , addDeviceToUser: addDeviceToUser , deleteDeviceFromUser: deleteDeviceFromUser +, getUserDeviceBySerial: getUserDeviceBySerial +, connectDeviceBySerial: connectDeviceBySerial +, disconnectDeviceBySerial: disconnectDeviceBySerial } function getCurrentUser(req, res) { @@ -52,7 +55,7 @@ function addDeviceToUser(req, res) { .then(function(device) { if (device) { datautil.normalize(device, req.user) - if(device.ready && !device.using) { + if(device.present && device.ready && !device.using && !device.owner) { var requirements = { 'serial': { @@ -140,3 +143,129 @@ function deleteDeviceFromUser(req, res) { } }) } + +function getUserDeviceBySerial(req, res) { + var serial = req.swagger.params.serial.value + var fields = req.swagger.params.fields.value + + dbapi.loadDevice(serial) + .then(function(device) { + if (device) { + datautil.normalize(device, req.user) + + if (device.owner && device.owner === req.user.email) { + if(fields) { + device = _.pick(device, fields.split(',')) + } + + res.json({ + success: true + , device: device + }) + } + else { + res.json(404, { + success: false + , description: 'device is not owned by you' + }) + } + } + else { + res.json(404, { + success: false + }) + } + }) + .catch(function(err) { + log.error('Failed to load device "%s": ', req.params.serial, err.stack) + res.json(500, { + success: false + }) + }) +} + +function connectDeviceBySerial(req, res) { + var serial = req.swagger.params.serial.value + + dbapi.loadDevice(serial) + .then(function(device) { + if (device) { + datautil.normalize(device, req.user) + + if (device.present && device.ready && device.using && device.owner.email == req.user.email) { + req.options.push.send([ + device.channel + , wireutil.envelope( + new wire.ConnectStartMessage() + ) + ]) + + res.json(202, { + success: true + , description: 'Device Connect request is accepted' + }) + } + else { + res.json(500, { + success: false + , description: 'Device is not owned by you or is not available' + }) + } + } + else { + res.json(404, { + success: false + , description: 'Bad device serial' + }) + } + }) + .catch(function(err) { + log.error('Failed to load device "%s": ', req.params.serial, err.stack) + res.json(500, { + success: false + }) + }) +} + +function disconnectDeviceBySerial(req, res) { + var serial = req.swagger.params.serial.value + + dbapi.loadDevice(serial) + .then(function(device) { + if (device) { + datautil.normalize(device, req.user) + + if (device.present && device.ready && device.using && device.owner.email == req.user.email) { + req.options.push.send([ + device.channel + , wireutil.envelope( + new wire.ConnectStopMessage() + ) + ]) + + res.json(202, { + success: true + , description: 'Device Disonnect request is accepted' + }) + } + else { + res.json(500, { + success: false + , description: 'Device is not owned by you or is not available' + }) + } + } + else { + res.json(404, { + success: false + , description: 'Bad device serial' + }) + } + }) + .catch(function(err) { + log.error('Failed to load device "%s": ', req.params.serial, err.stack) + res.json(500, { + success: false + }) + }) +} diff --git a/lib/units/api/swagger/api_v1.yaml b/lib/units/api/swagger/api_v1.yaml index bf7b380fb4..49ad296192 100644 --- a/lib/units/api/swagger/api_v1.yaml +++ b/lib/units/api/swagger/api_v1.yaml @@ -86,6 +86,34 @@ paths: - accessTokenAuth: [] /user/devices/{serial}: x-swagger-router-controller: user + get: + summary: Device Information + description: The device enpoint return information about a single device. + operationId: getUserDeviceBySerial + tags: + - user + parameters: + - name: serial + in: path + description: Device Serial + required: true + type: string + - name: fields + in: query + description: Fields query parameter takes a comma seperated list of fields. Only listed field will be return in response + required: false + type: string + responses: + "200": + description: Device Information + schema: + $ref: "#/definitions/DeviceResponse" + default: + description: Unexpected Error + schema: + $ref: "#/definitions/ErrorResponse" + security: + - accessTokenAuth: [] delete: summary: Release device from user description: The User Devices endpoint will request for device release from stf server. @@ -107,6 +135,50 @@ paths: $ref: "#/definitions/ErrorResponse" security: - accessTokenAuth: [] + /user/devices/{serial}/remoteConnect: + x-swagger-router-controller: user + post: + summary: Remote Connect + description: The device connect endpoint will request stf server to connect remotely + operationId: connectDeviceBySerial + tags: + - user + parameters: + - name: serial + in: path + description: Device Serial + required: true + type: string + responses: + "202": + description: Device Connect Request Status + default: + description: Unexpected Error + schema: + $ref: "#/definitions/ErrorResponse" + security: + - accessTokenAuth: [] + delete: + summary: Remote Disconnect + description: The device connect endpoint will request stf server to disconnect remotely + operationId: disconnectDeviceBySerial + tags: + - user + parameters: + - name: serial + in: path + description: Device Serial + required: true + type: string + responses: + "202": + description: Device Disconnect Request Status + default: + description: Unexpected Error + schema: + $ref: "#/definitions/ErrorResponse" + security: + - accessTokenAuth: [] /user/accessTokens: x-swagger-router-controller: token get: From 25cee263b21774133ffbfce5c82d16287ffc8331 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Mon, 7 Dec 2015 23:03:38 +0900 Subject: [PATCH 16/37] Fix express warning for deprecated res.json(stat) message --- lib/units/api/controllers/device.js | 6 ++--- lib/units/api/controllers/token.js | 2 +- lib/units/api/controllers/user.js | 36 ++++++++++++++--------------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/lib/units/api/controllers/device.js b/lib/units/api/controllers/device.js index a7190c392a..703ccb6745 100644 --- a/lib/units/api/controllers/device.js +++ b/lib/units/api/controllers/device.js @@ -31,7 +31,7 @@ function getDevices(req, res) { }) .catch(function(err) { log.error('Failed to load device list: ', err.stack) - res.json(500, { + res.status(500).json({ success: false }) }) @@ -56,14 +56,14 @@ function getDeviceBySerial(req, res) { }) } else { - res.json(404, { + res.status(404).json({ success: false }) } }) .catch(function(err) { log.error('Failed to load device "%s": ', req.params.serial, err.stack) - res.json(500, { + res.status(500).json({ success: false }) }) diff --git a/lib/units/api/controllers/token.js b/lib/units/api/controllers/token.js index 8452f84eef..f123b6d95e 100644 --- a/lib/units/api/controllers/token.js +++ b/lib/units/api/controllers/token.js @@ -26,7 +26,7 @@ function getAccessTokens(req, res) { }) .catch(function(err) { log.error('Failed to load tokens: ', err.stack) - res.json(500, { + res.status(500).json({ success: false }) }) diff --git a/lib/units/api/controllers/user.js b/lib/units/api/controllers/user.js index b0cb4df92c..7a59283697 100644 --- a/lib/units/api/controllers/user.js +++ b/lib/units/api/controllers/user.js @@ -41,7 +41,7 @@ function getCurrentUserDevices(req, res) { }) .catch(function(err) { log.error('Failed to load group: ', err.stack) - res.json(500, { + res.status(500).json({ success: false }) }) @@ -79,19 +79,19 @@ function addDeviceToUser(req, res) { ) ]) - res.json(202, { + res.status(202).json({ success: true , description: 'Device Add request is accepted' }) } else { - res.json(500, { + res.status(500).json({ success: false , description: 'Device is being used or not available' }) } } else { - res.json(500, { + res.status(500).json({ success: false , description: 'Bad device serial' }) @@ -124,19 +124,19 @@ function deleteDeviceFromUser(req, res) { ) ]) - res.json(202, { + res.status(202).json({ success: true , description: 'Device Release request is accepted' }) } else { - res.json(500, { + res.status(500).json({ success: false , description: 'You cannot kick this device' }) } } else { - res.json(500, { + res.status(500).json({ success: false , description: 'Bad device serial' }) @@ -164,21 +164,21 @@ function getUserDeviceBySerial(req, res) { }) } else { - res.json(404, { + res.status(404).json({ success: false , description: 'device is not owned by you' }) } } else { - res.json(404, { + res.status(404).json({ success: false }) } }) .catch(function(err) { log.error('Failed to load device "%s": ', req.params.serial, err.stack) - res.json(500, { + res.status(500).json({ success: false }) }) @@ -200,20 +200,20 @@ function connectDeviceBySerial(req, res) { ) ]) - res.json(202, { + res.status(202).json({ success: true , description: 'Device Connect request is accepted' }) } else { - res.json(500, { + res.status(500).json({ success: false , description: 'Device is not owned by you or is not available' }) } } else { - res.json(404, { + res.status(404).json({ success: false , description: 'Bad device serial' }) @@ -221,7 +221,7 @@ function connectDeviceBySerial(req, res) { }) .catch(function(err) { log.error('Failed to load device "%s": ', req.params.serial, err.stack) - res.json(500, { + res.status(500).json({ success: false }) }) @@ -243,20 +243,20 @@ function disconnectDeviceBySerial(req, res) { ) ]) - res.json(202, { + res.status(202).json({ success: true , description: 'Device Disonnect request is accepted' }) } else { - res.json(500, { + res.status(500).json({ success: false , description: 'Device is not owned by you or is not available' }) } } else { - res.json(404, { + res.status(404).json({ success: false , description: 'Bad device serial' }) @@ -264,7 +264,7 @@ function disconnectDeviceBySerial(req, res) { }) .catch(function(err) { log.error('Failed to load device "%s": ', req.params.serial, err.stack) - res.json(500, { + res.status(500).json({ success: false }) }) From 26ad95a36e2d1b118ac1d0aae5a08e84e95556b9 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Mon, 7 Dec 2015 23:06:54 +0900 Subject: [PATCH 17/37] add comment for going against REST --- lib/units/api/swagger/api_v1.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/units/api/swagger/api_v1.yaml b/lib/units/api/swagger/api_v1.yaml index 49ad296192..ead63bd8cd 100644 --- a/lib/units/api/swagger/api_v1.yaml +++ b/lib/units/api/swagger/api_v1.yaml @@ -135,6 +135,8 @@ paths: $ref: "#/definitions/ErrorResponse" security: - accessTokenAuth: [] + # I do know this is against REST principal to use verb as endpoint. But I feel it is more easy to + # understand in comparision of using PUT/PATCH /user/devices/{serial}/remoteConnect: x-swagger-router-controller: user post: From 9de5ba5faec2fcb7b0f78fdf2ea054e7de572c98 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Tue, 8 Dec 2015 15:58:05 +0900 Subject: [PATCH 18/37] refactoring swagger --- .../api/controllers/{device.js => devices.js} | 17 +- lib/units/api/controllers/token.js | 33 --- lib/units/api/controllers/user.js | 193 ++++++++++++------ lib/units/api/helpers/securityHandlers.js | 27 ++- lib/units/api/swagger/api_v1.yaml | 110 +++++++--- 5 files changed, 233 insertions(+), 147 deletions(-) rename lib/units/api/controllers/{device.js => devices.js} (81%) delete mode 100644 lib/units/api/controllers/token.js diff --git a/lib/units/api/controllers/device.js b/lib/units/api/controllers/devices.js similarity index 81% rename from lib/units/api/controllers/device.js rename to lib/units/api/controllers/devices.js index 703ccb6745..4afc3b9f49 100644 --- a/lib/units/api/controllers/device.js +++ b/lib/units/api/controllers/devices.js @@ -1,31 +1,37 @@ -var Promise = require('bluebird') var _ = require('lodash') +var Promise = require('bluebird') var dbapi = require('../../../db/api') var logger = require('../../../util/logger') var datautil = require('../../../util/datautil') -var log = logger.createLogger('api:controllers:device') +var log = logger.createLogger('api:controllers:devices') module.exports = { getDevices: getDevices , getDeviceBySerial: getDeviceBySerial } -var log = logger.createLogger('api:contoller:device') - function getDevices(req, res) { + var fields = req.swagger.params.fields.value + dbapi.loadDevices() .then(function(cursor) { return Promise.promisify(cursor.toArray, cursor)() .then(function(list) { + var deviceList = [] + list.forEach(function(device) { datautil.normalize(device, req.user) + if (fields) { + device = _.pick(device, fields.split(',')) + } + deviceList.push(device) }) res.json({ success: true - , devices: list + , devices: deviceList }) }) }) @@ -58,6 +64,7 @@ function getDeviceBySerial(req, res) { else { res.status(404).json({ success: false + , description: 'Device not found' }) } }) diff --git a/lib/units/api/controllers/token.js b/lib/units/api/controllers/token.js deleted file mode 100644 index f123b6d95e..0000000000 --- a/lib/units/api/controllers/token.js +++ /dev/null @@ -1,33 +0,0 @@ -var Promise = require('bluebird') - -var dbapi = require('../../../db/api') -var logger = require('../../../util/logger') - -var log = logger.createLogger('api:controllers:token') - -module.exports = { - getAccessTokens: getAccessTokens -} - -function getAccessTokens(req, res) { - dbapi.loadAccessTokens(req.user.email) - .then(function(cursor) { - return Promise.promisify(cursor.toArray, cursor)() - .then(function(list) { - var titles = [] - list.forEach(function(token) { - titles.push(token.title) - }) - res.json({ - success: true - , titles: titles - }) - }) - }) - .catch(function(err) { - log.error('Failed to load tokens: ', err.stack) - res.status(500).json({ - success: false - }) - }) -} diff --git a/lib/units/api/controllers/user.js b/lib/units/api/controllers/user.js index 7a59283697..fcab815589 100644 --- a/lib/units/api/controllers/user.js +++ b/lib/units/api/controllers/user.js @@ -1,4 +1,7 @@ +var util = require('util') + var Promise = require('bluebird') +var _ = require('lodash') var dbapi = require('../../../db/api') var logger = require('../../../util/logger') @@ -9,45 +12,96 @@ var wireutil = require('../../../wire/util') var log = logger.createLogger('api:controllers:user') module.exports = { - getCurrentUser: getCurrentUser -, getCurrentUserDevices: getCurrentUserDevices -, addDeviceToUser: addDeviceToUser -, deleteDeviceFromUser: deleteDeviceFromUser + getUser: getUser +, getUserDevices: getUserDevices +, addUserDevice: addUserDevice , getUserDeviceBySerial: getUserDeviceBySerial -, connectDeviceBySerial: connectDeviceBySerial -, disconnectDeviceBySerial: disconnectDeviceBySerial +, deleteUserDeviceBySerial: deleteUserDeviceBySerial +, remoteConnectUserDeviceBySerial: remoteConnectUserDeviceBySerial +, remoteDisconnectUserDeviceBySerial: remoteDisconnectUserDeviceBySerial +, getUserAccessTokens: getUserAccessTokens } -function getCurrentUser(req, res) { +function getUser(req, res) { res.json({ success: true , user: req.user }) } -function getCurrentUserDevices(req, res) { +function getUserDevices(req, res) { + var fields = req.swagger.params.fields.value + dbapi.loadUserDevices(req.user.email) .then(function(cursor) { return Promise.promisify(cursor.toArray, cursor)() .then(function(list) { + var deviceList = [] + list.forEach(function(device) { datautil.normalize(device, req.user) + if (fields) { + device = _.pick(device, fields.split(',')) + } + deviceList.push(device) }) + res.json({ success: true - , devices: list + , devices: deviceList }) }) }) .catch(function(err) { - log.error('Failed to load group: ', err.stack) + log.error('Failed to load device list: ', err.stack) res.status(500).json({ success: false }) }) } -function addDeviceToUser(req, res) { +function getUserDeviceBySerial(req, res) { + var serial = req.swagger.params.serial.value + var fields = req.swagger.params.fields.value + + dbapi.loadDevice(serial) + .then(function(device) { + if (device) { + datautil.normalize(device, req.user) + + if (device.owner && device.owner.email === req.user.email) { + if(fields) { + device = _.pick(device, fields.split(',')) + } + + res.json({ + success: true + , device: device + }) + } + else { + res.status(401).json({ + success: false + , description: 'Device is not owned by you' + }) + } + } + else { + res.status(404).json({ + success: false + , description: 'Device not found' + }) + } + }) + .catch(function(err) { + log.error('Failed to load device "%s": ', req.params.serial, err.stack) + res.status(500).json({ + success: false + }) + }) +} + +function addUserDevice(req, res) { var serial = req.body.serial var timeout = req.body.timeout || null @@ -81,17 +135,23 @@ function addDeviceToUser(req, res) { res.status(202).json({ success: true - , description: 'Device Add request is accepted' + , description: 'Device Add request is accepted. Check if device is successfully added using pollingUrl' + , pollingUrl: util.format('%s://%s%s/user/devices/%s' + , req.protocol + , req.get('host') + , req.swagger.operation.api.basePath + , serial + ) }) } else { - res.status(500).json({ + res.status(401).json({ success: false , description: 'Device is being used or not available' }) } } else { - res.status(500).json({ + res.status(404).json({ success: false , description: 'Bad device serial' }) @@ -99,7 +159,7 @@ function addDeviceToUser(req, res) { }) } -function deleteDeviceFromUser(req, res) { +function deleteUserDeviceBySerial(req, res) { var serial = req.swagger.params.serial.value dbapi.loadDevice(serial) @@ -126,17 +186,23 @@ function deleteDeviceFromUser(req, res) { res.status(202).json({ success: true - , description: 'Device Release request is accepted' + , description: 'Device Release request is accepted. Check if device is successfully removed using pollingUrl' + , pollingUrl: util.format('%s://%s%s/user/devices/%s' + , req.protocol + , req.get('host') + , req.swagger.operation.api.basePath + , serial + ) }) } else { - res.status(500).json({ + res.status(401).json({ success: false , description: 'You cannot kick this device' }) } } else { - res.status(500).json({ + res.status(404).json({ success: false , description: 'Bad device serial' }) @@ -144,35 +210,44 @@ function deleteDeviceFromUser(req, res) { }) } -function getUserDeviceBySerial(req, res) { +function remoteConnectUserDeviceBySerial(req, res) { var serial = req.swagger.params.serial.value - var fields = req.swagger.params.fields.value dbapi.loadDevice(serial) .then(function(device) { if (device) { datautil.normalize(device, req.user) - if (device.owner && device.owner === req.user.email) { - if(fields) { - device = _.pick(device, fields.split(',')) - } + if (device.present && device.ready && device.using && device.owner.email === req.user.email) { + req.options.push.send([ + device.channel + , wireutil.envelope( + new wire.ConnectStartMessage() + ) + ]) - res.json({ + res.status(202).json({ success: true - , device: device + , description: 'Device Connect request is accepted. Check if device is successfully connected using pollingUrl' + , pollingUrl: util.format('%s://%s%s/user/devices/%s' + , req.protocol + , req.get('host') + , req.swagger.operation.api.basePath + , serial + ) }) } else { - res.status(404).json({ + res.status(401).json({ success: false - , description: 'device is not owned by you' + , description: 'Device is not owned by you or is not available' }) } } else { res.status(404).json({ success: false + , description: 'Device not found' }) } }) @@ -184,7 +259,7 @@ function getUserDeviceBySerial(req, res) { }) } -function connectDeviceBySerial(req, res) { +function remoteDisconnectUserDeviceBySerial(req, res) { var serial = req.swagger.params.serial.value dbapi.loadDevice(serial) @@ -196,17 +271,23 @@ function connectDeviceBySerial(req, res) { req.options.push.send([ device.channel , wireutil.envelope( - new wire.ConnectStartMessage() - ) + new wire.ConnectStopMessage() + ) ]) res.status(202).json({ success: true - , description: 'Device Connect request is accepted' + , description: 'Device Disonnect request is accepted. Check if device is successfully disconnected using pollingUrl' + , pollingUrl: util.format('%s://%s%s/user/devices/%s' + , req.protocol + , req.get('host') + , req.swagger.operation.api.basePath + , serial + ) }) } else { - res.status(500).json({ + res.status(401).json({ success: false , description: 'Device is not owned by you or is not available' }) @@ -215,7 +296,7 @@ function connectDeviceBySerial(req, res) { else { res.status(404).json({ success: false - , description: 'Bad device serial' + , description: 'Device not found' }) } }) @@ -227,43 +308,23 @@ function connectDeviceBySerial(req, res) { }) } -function disconnectDeviceBySerial(req, res) { - var serial = req.swagger.params.serial.value - - dbapi.loadDevice(serial) - .then(function(device) { - if (device) { - datautil.normalize(device, req.user) - - if (device.present && device.ready && device.using && device.owner.email == req.user.email) { - req.options.push.send([ - device.channel - , wireutil.envelope( - new wire.ConnectStopMessage() - ) - ]) - - res.status(202).json({ - success: true - , description: 'Device Disonnect request is accepted' +function getUserAccessTokens(req, res) { + dbapi.loadAccessTokens(req.user.email) + .then(function(cursor) { + return Promise.promisify(cursor.toArray, cursor)() + .then(function(list) { + var titles = [] + list.forEach(function(token) { + titles.push(token.title) }) - } - else { - res.status(500).json({ - success: false - , description: 'Device is not owned by you or is not available' + res.json({ + success: true + , titles: titles }) - } - } - else { - res.status(404).json({ - success: false - , description: 'Bad device serial' }) - } }) .catch(function(err) { - log.error('Failed to load device "%s": ', req.params.serial, err.stack) + log.error('Failed to load tokens: ', err.stack) res.status(500).json({ success: false }) diff --git a/lib/units/api/helpers/securityHandlers.js b/lib/units/api/helpers/securityHandlers.js index 7843ab8c5d..9c5fa3ee9e 100644 --- a/lib/units/api/helpers/securityHandlers.js +++ b/lib/units/api/helpers/securityHandlers.js @@ -1,7 +1,7 @@ +var dbapi = require('../../../db/api') var jwtutil = require('../../../util/jwtutil') var urlutil = require('../../../util/urlutil') var logger = require('../../../util/logger') -var dbapi = require('../../../db/api') var log = logger.createLogger('api:helpers:securityHandlers') @@ -11,7 +11,16 @@ module.exports = { function accessTokenAuth(req, res, next) { if (req.headers.authorization) { - var tokenId = req.headers.authorization.split(" ")[1] + var authHeader = req.headers.authorization.split(' ') + , format = authHeader[0] + , tokenId = authHeader[1] + + if (format !== 'bearer') { + res.status(401).json({ + success: false + , description: 'Authorization header should be in "bearer $AUTH_TOKEN" format' + }) + } if (tokenId) { dbapi.loadAccessToken(tokenId) @@ -28,23 +37,23 @@ function accessTokenAuth(req, res, next) { } }) } else { - res.json(500, { + res.status(500).json({ success: false }) } }) .catch(function(err) { log.error('Failed to load token: ', err.stack) - res.json(401, { + res.status(401).json({ success: false, - description: 'Bad credentials' + description: 'Bad Credentials' }) }) } else { log.error('Bad Access Token Header') - res.json(401, { + res.status(401).json({ success: false, - description: 'Bad credentials' + description: 'Bad Credentials' }) } } @@ -67,9 +76,9 @@ function accessTokenAuth(req, res, next) { .catch(next) } else { - res.json(401, { + res.status(401).json({ success: false, - description: 'Requires authentication' + description: 'Requires Authentication' }) } } diff --git a/lib/units/api/swagger/api_v1.yaml b/lib/units/api/swagger/api_v1.yaml index ead63bd8cd..aa455bdd60 100644 --- a/lib/units/api/swagger/api_v1.yaml +++ b/lib/units/api/swagger/api_v1.yaml @@ -2,12 +2,12 @@ swagger: "2.0" info: version: "2.0.0" title: Smartphone Test Farm - description: Control and manager real Smartphone devices from browser and restful apis + description: Control and manages real Smartphone devices from browser and restful apis license: name: Apache-2.0 url: http://www.apache.org/licenses/LICENSE-2.0 contact: - name: STF Support + name: STF Team email: contact@openstf.io url: http://openstf.io/ basePath: /api/v1 @@ -21,15 +21,15 @@ produces: tags: - name: user description: User Operations - - name: device + - name: devices description: Device Operations paths: /user: x-swagger-router-controller: user get: summary: User Profile - description: The User Profile endpoint returns information about current authorized user. - operationId: getCurrentUser + description: The User Profile endpoint returns information about current authorized user + operationId: getUser tags: - user responses: @@ -47,13 +47,19 @@ paths: x-swagger-router-controller: user get: summary: List devices owned by current user - description: The User Devices endpoint returns information about user group of current authorized user. - operationId: getCurrentUserDevices + description: The User Devices endpoint returns device list owner by current authorized user + operationId: getUserDevices tags: - user + parameters: + - name: fields + in: query + description: Fields query parameter takes a comma seperated list of fields. Only listed field will be return in response + required: false + type: string responses: "200": - description: Current User Devices information + description: Current User Devices List schema: $ref: "#/definitions/DeviceListResponse" default: @@ -63,21 +69,23 @@ paths: security: - accessTokenAuth: [] post: - summary: Add device to a user - description: The User Devices endpoint will request stf server for a new device. It will return request accepted if device is usable. - operationId: addDeviceToUser + summary: Add a device to a user + description: The User Devices endpoint will request stf server for a new device. It will return request accepted if device is usable + operationId: addUserDevice tags: - user parameters: - - name: device + - name: devices in: body description: Device to add required: true schema: - $ref: "#/definitions/DeviceAddPayload" + $ref: "#/definitions/AddUserDevicePayload" responses: "202": - description: Device Add Request Status + description: Add User Device Request Status and polling Url + schema: + $ref: "#/definitions/AddUserDeviceResponse" default: description: Unexpected Error schema: @@ -88,7 +96,7 @@ paths: x-swagger-router-controller: user get: summary: Device Information - description: The device enpoint return information about a single device. + description: The device enpoint return information about device owned by user operationId: getUserDeviceBySerial tags: - user @@ -105,9 +113,9 @@ paths: type: string responses: "200": - description: Device Information + description: Delete User Device Request Status and polling Url schema: - $ref: "#/definitions/DeviceResponse" + $ref: "#/definitions/DeleteUserDeviceBySerialResponse" default: description: Unexpected Error schema: @@ -116,8 +124,8 @@ paths: - accessTokenAuth: [] delete: summary: Release device from user - description: The User Devices endpoint will request for device release from stf server. - operationId: deleteDeviceFromUser + description: The User Devices endpoint will request for device release from stf server. It will return request accepted if device is being used by current user + operationId: deleteUserDeviceBySerial tags: - user parameters: @@ -142,7 +150,7 @@ paths: post: summary: Remote Connect description: The device connect endpoint will request stf server to connect remotely - operationId: connectDeviceBySerial + operationId: remoteConnectUserDeviceBySerial tags: - user parameters: @@ -153,7 +161,9 @@ paths: type: string responses: "202": - description: Device Connect Request Status + description: Remote Connect User Device Request Status + schema: + $ref: "#/definitions/RemoteConnectUserDeviceResponse" default: description: Unexpected Error schema: @@ -163,7 +173,7 @@ paths: delete: summary: Remote Disconnect description: The device connect endpoint will request stf server to disconnect remotely - operationId: disconnectDeviceBySerial + operationId: remoteDisconnectUserDeviceBySerial tags: - user parameters: @@ -174,7 +184,9 @@ paths: type: string responses: "202": - description: Device Disconnect Request Status + description: Remote Disonnect User Device Request Status + schema: + $ref: "#/definitions/RemoteDisconnectUserDeviceResponse" default: description: Unexpected Error schema: @@ -185,8 +197,8 @@ paths: x-swagger-router-controller: token get: summary: Access Tokens - description: The Access Tokens endpoints returns titles of all the valid access tokens. - operationId: getAccessTokens + description: The Access Tokens endpoints returns titles of all the valid access tokens + operationId: getUserAccessTokens tags: - user responses: @@ -201,13 +213,19 @@ paths: security: - accessTokenAuth: [] /devices: - x-swagger-router-controller: device + x-swagger-router-controller: devices get: summary: Device List - description: The devices enpoint return list of all the STF devices including Disconnected and Offline + description: The devices endpoint return list of all the STF devices including Disconnected and Offline operationId: getDevices tags: - - device + - devices + parameters: + - name: fields + in: query + description: Fields query parameter takes a comma seperated list of fields. Only listed field will be return in response + required: false + type: string responses: "200": description: List of Devices @@ -220,13 +238,13 @@ paths: security: - accessTokenAuth: [] /devices/{serial}: - x-swagger-router-controller: device + x-swagger-router-controller: devices get: summary: Device Information - description: The device enpoint return information about a single device. + description: The device enpoint return information about a single device operationId: getDeviceBySerial tags: - - device + - devices parameters: - name: serial in: path @@ -280,13 +298,37 @@ definitions: properties: device: type: object + AddUserDeviceResponse: + required: + - pollingUrl + properties: + pollingUrl: + type: string + DeleteUserDeviceBySerialResponse: + required: + - pollingUrl + properties: + pollingUrl: + type: string + RemoteDisconnectUserDeviceResponse: + required: + - pollingUrl + properties: + pollingUrl: + type: string + RemoteConnectUserDeviceResponse: + required: + - pollingUrl + properties: + pollingUrl: + type: string ErrorResponse: required: - message properties: message: type: string - DeviceAddPayload: + AddUserDevicePayload: description: payload object for adding device to user required: - serial @@ -295,11 +337,11 @@ definitions: description: Device Serial type: string timeout: - description: Device timeout in ms. If device is kept idle for this period, it will be automatically disconnected. Default is provider group timeout. + description: Device timeout in ms. If device is kept idle for this period, it will be automatically disconnected. Default is provider group timeout type: integer securityDefinitions: accessTokenAuth: type: apiKey - name: accessTokenAuth + name: authorization in: header From 723094347cb50a0e6a690ded2e1b67f8f211df08 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Tue, 8 Dec 2015 17:37:50 +0900 Subject: [PATCH 19/37] add generated swagger json file for swagger ui --- lib/units/api/swagger/api_v1_generated.json | 531 ++++++++++++++++++++ 1 file changed, 531 insertions(+) create mode 100644 lib/units/api/swagger/api_v1_generated.json diff --git a/lib/units/api/swagger/api_v1_generated.json b/lib/units/api/swagger/api_v1_generated.json new file mode 100644 index 0000000000..0924050192 --- /dev/null +++ b/lib/units/api/swagger/api_v1_generated.json @@ -0,0 +1,531 @@ +{ + "swagger": "2.0", + "info": { + "version": "2.0.0", + "title": "Smartphone Test Farm", + "description": "Control and manages real Smartphone devices from browser and restful apis", + "license": { + "name": "Apache-2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0" + }, + "contact": { + "name": "STF Team", + "email": "contact@openstf.io", + "url": "http://openstf.io/" + } + }, + "basePath": "/api/v1", + "schemes": [ + "http", + "https" + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + { + "name": "user", + "description": "User Operations" + }, + { + "name": "devices", + "description": "Device Operations" + } + ], + "paths": { + "/user": { + "get": { + "summary": "User Profile", + "description": "The User Profile endpoint returns information about current authorized user", + "operationId": "getUser", + "tags": [ + "user" + ], + "responses": { + "200": { + "description": "Current User Profile information", + "schema": { + "$ref": "#/definitions/UserResponse" + } + }, + "default": { + "description": "Unexpected Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "security": [ + { + "accessTokenAuth": [] + } + ] + } + }, + "/user/devices": { + "get": { + "summary": "List devices owned by current user", + "description": "The User Devices endpoint returns device list owner by current authorized user", + "operationId": "getUserDevices", + "tags": [ + "user" + ], + "parameters": [ + { + "name": "fields", + "in": "query", + "description": "Fields query parameter takes a comma seperated list of fields. Only listed field will be return in response", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Current User Devices List", + "schema": { + "$ref": "#/definitions/DeviceListResponse" + } + }, + "default": { + "description": "Unexpected Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "security": [ + { + "accessTokenAuth": [] + } + ] + }, + "post": { + "summary": "Add a device to a user", + "description": "The User Devices endpoint will request stf server for a new device. It will return request accepted if device is usable", + "operationId": "addUserDevice", + "tags": [ + "user" + ], + "parameters": [ + { + "name": "devices", + "in": "body", + "description": "Device to add", + "required": true, + "schema": { + "$ref": "#/definitions/AddUserDevicePayload" + } + } + ], + "responses": { + "202": { + "description": "Add User Device Request Status and polling Url", + "schema": { + "$ref": "#/definitions/AddUserDeviceResponse" + } + }, + "default": { + "description": "Unexpected Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "security": [ + { + "accessTokenAuth": [] + } + ] + } + }, + "/user/devices/{serial}": { + "get": { + "summary": "Device Information", + "description": "The device enpoint return information about device owned by user", + "operationId": "getUserDeviceBySerial", + "tags": [ + "user" + ], + "parameters": [ + { + "name": "serial", + "in": "path", + "description": "Device Serial", + "required": true, + "type": "string" + }, + { + "name": "fields", + "in": "query", + "description": "Fields query parameter takes a comma seperated list of fields. Only listed field will be return in response", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Delete User Device Request Status and polling Url", + "schema": { + "$ref": "#/definitions/DeleteUserDeviceBySerialResponse" + } + }, + "default": { + "description": "Unexpected Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "security": [ + { + "accessTokenAuth": [] + } + ] + }, + "delete": { + "summary": "Release device from user", + "description": "The User Devices endpoint will request for device release from stf server. It will return request accepted if device is being used by current user", + "operationId": "deleteUserDeviceBySerial", + "tags": [ + "user" + ], + "parameters": [ + { + "name": "serial", + "in": "path", + "description": "Device Serial", + "required": true, + "type": "string" + } + ], + "responses": { + "202": { + "description": "Device Release Request Status" + }, + "default": { + "description": "Unexpected Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "security": [ + { + "accessTokenAuth": [] + } + ] + } + }, + "/user/devices/{serial}/remoteConnect": { + "post": { + "summary": "Remote Connect", + "description": "The device connect endpoint will request stf server to connect remotely", + "operationId": "remoteConnectUserDeviceBySerial", + "tags": [ + "user" + ], + "parameters": [ + { + "name": "serial", + "in": "path", + "description": "Device Serial", + "required": true, + "type": "string" + } + ], + "responses": { + "202": { + "description": "Remote Connect User Device Request Status", + "schema": { + "$ref": "#/definitions/RemoteConnectUserDeviceResponse" + } + }, + "default": { + "description": "Unexpected Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "security": [ + { + "accessTokenAuth": [] + } + ] + }, + "delete": { + "summary": "Remote Disconnect", + "description": "The device connect endpoint will request stf server to disconnect remotely", + "operationId": "remoteDisconnectUserDeviceBySerial", + "tags": [ + "user" + ], + "parameters": [ + { + "name": "serial", + "in": "path", + "description": "Device Serial", + "required": true, + "type": "string" + } + ], + "responses": { + "202": { + "description": "Remote Disonnect User Device Request Status", + "schema": { + "$ref": "#/definitions/RemoteDisconnectUserDeviceResponse" + } + }, + "default": { + "description": "Unexpected Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "security": [ + { + "accessTokenAuth": [] + } + ] + } + }, + "/user/accessTokens": { + "get": { + "summary": "Access Tokens", + "description": "The Access Tokens endpoints returns titles of all the valid access tokens", + "operationId": "getUserAccessTokens", + "tags": [ + "user" + ], + "responses": { + "200": { + "description": "Access Tokens titles", + "schema": { + "$ref": "#/definitions/AccessTokensResponse" + } + }, + "default": { + "description": "Unexpected Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "security": [ + { + "accessTokenAuth": [] + } + ] + } + }, + "/devices": { + "get": { + "summary": "Device List", + "description": "The devices endpoint return list of all the STF devices including Disconnected and Offline", + "operationId": "getDevices", + "tags": [ + "devices" + ], + "parameters": [ + { + "name": "fields", + "in": "query", + "description": "Fields query parameter takes a comma seperated list of fields. Only listed field will be return in response", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "List of Devices", + "schema": { + "$ref": "#/definitions/DeviceListResponse" + } + }, + "default": { + "description": "Unexpected Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "security": [ + { + "accessTokenAuth": [] + } + ] + } + }, + "/devices/{serial}": { + "get": { + "summary": "Device Information", + "description": "The device enpoint return information about a single device", + "operationId": "getDeviceBySerial", + "tags": [ + "devices" + ], + "parameters": [ + { + "name": "serial", + "in": "path", + "description": "Device Serial", + "required": true, + "type": "string" + }, + { + "name": "fields", + "in": "query", + "description": "Fields query parameter takes a comma seperated list of fields. Only listed field will be return in response", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "Device Information", + "schema": { + "$ref": "#/definitions/DeviceResponse" + } + }, + "default": { + "description": "Unexpected Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + }, + "security": [ + { + "accessTokenAuth": [] + } + ] + } + }, + "/swagger.json": {} + }, + "definitions": { + "UserResponse": { + "required": [ + "user" + ], + "properties": { + "user": { + "type": "object" + } + } + }, + "AccessTokensResponse": { + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "DeviceListResponse": { + "required": [ + "devices" + ], + "properties": { + "devices": { + "type": "array", + "items": { + "type": "object" + } + } + } + }, + "DeviceResponse": { + "required": [ + "device" + ], + "properties": { + "device": { + "type": "object" + } + } + }, + "AddUserDeviceResponse": { + "required": [ + "pollingUrl" + ], + "properties": { + "pollingUrl": { + "type": "string" + } + } + }, + "DeleteUserDeviceBySerialResponse": { + "required": [ + "pollingUrl" + ], + "properties": { + "pollingUrl": { + "type": "string" + } + } + }, + "RemoteDisconnectUserDeviceResponse": { + "required": [ + "pollingUrl" + ], + "properties": { + "pollingUrl": { + "type": "string" + } + } + }, + "RemoteConnectUserDeviceResponse": { + "required": [ + "pollingUrl" + ], + "properties": { + "pollingUrl": { + "type": "string" + } + } + }, + "ErrorResponse": { + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + }, + "AddUserDevicePayload": { + "description": "payload object for adding device to user", + "required": [ + "serial" + ], + "properties": { + "serial": { + "description": "Device Serial", + "type": "string" + }, + "timeout": { + "description": "Device timeout in ms. If device is kept idle for this period, it will be automatically disconnected. Default is provider group timeout", + "type": "integer" + } + } + } + }, + "securityDefinitions": { + "accessTokenAuth": { + "type": "apiKey", + "name": "authorization", + "in": "header" + } + } +} From b004dcb51b1ab82dee3728ae97c888f3c2c889ec Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Thu, 10 Dec 2015 15:28:47 +0900 Subject: [PATCH 20/37] remove unnecessary requirements from app unit --- lib/units/app/index.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/units/app/index.js b/lib/units/app/index.js index 89f4731e88..9f19dd4b46 100644 --- a/lib/units/app/index.js +++ b/lib/units/app/index.js @@ -9,13 +9,10 @@ var bodyParser = require('body-parser') var serveFavicon = require('serve-favicon') var serveStatic = require('serve-static') var csrf = require('csurf') -var Promise = require('bluebird') var compression = require('compression') var logger = require('../../util/logger') var pathutil = require('../../util/pathutil') -var dbapi = require('../../db/api') -var datautil = require('../../util/datautil') var auth = require('./middleware/auth') var deviceIconMiddleware = require('./middleware/device-icons') From d67d06a19f9ad8bba80ca16c6d7c6bbda7a5e229 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Thu, 10 Dec 2015 15:30:14 +0900 Subject: [PATCH 21/37] typo --- doc/DEPLOYMENT.md | 2 +- lib/cli.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/DEPLOYMENT.md b/doc/DEPLOYMENT.md index d9c1b65961..7c97b1e643 100644 --- a/doc/DEPLOYMENT.md +++ b/doc/DEPLOYMENT.md @@ -681,7 +681,7 @@ ExecStart=/usr/bin/docker run --rm \ -e "SECRET=YOUR_SESSION_SECRET_HERE" \ -p %i:3000 \ openstf/stf:latest \ - stf app --port 3000 + stf api --port 3000 ExecStop=-/usr/bin/docker stop -t 10 %p-%i ``` diff --git a/lib/cli.js b/lib/cli.js index 25e323e275..93daea446b 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -758,7 +758,7 @@ program .option('-u, --app-url ' , 'URL to app' , String) - .option('-u, --api-url ' + .option('-i, --api-url ' , 'URL to api' , String) .option('-a, --auth-url ' From a6266931ad315123b38a9c2fe74afa34eb62b9f5 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Mon, 14 Dec 2015 14:02:20 +0900 Subject: [PATCH 22/37] Modify APIs from Aysnc -> Sync using timeout --- lib/cli.js | 8 + lib/db/api.js | 6 +- lib/units/api/controllers/devices.js | 26 +- lib/units/api/controllers/user.js | 402 ++++++++++++++++----------- lib/units/api/index.js | 49 +++- lib/units/api/swagger/api_v1.yaml | 70 ++--- lib/units/device/plugins/connect.js | 9 +- lib/units/processor/index.js | 2 + lib/util/datautil.js | 2 +- lib/util/deviceutil.js | 13 + 10 files changed, 351 insertions(+), 236 deletions(-) create mode 100644 lib/util/deviceutil.js diff --git a/lib/cli.js b/lib/cli.js index 93daea446b..0af3aff201 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -874,6 +874,9 @@ program .option('-c, --connect-push ' , 'push endpoint' , cliutil.list) + .option('-u, --connect-sub ' + , 'sub endpoint' + , cliutil.list) .action(function(options) { if (!options.secret) { this.missingArgument('--secret') @@ -881,6 +884,9 @@ program if (!options.connectPush) { this.missingArgument('--connect-push') } + if (!options.connectSub) { + this.missingArgument('--connect-sub') + } require('./units/api')({ port: options.port @@ -888,6 +894,7 @@ program , secret: options.secret , endpoints: { push: options.connectPush + , sub: options.connectSub } }) }) @@ -1338,6 +1345,7 @@ program , '--port', options.apiPort , '--secret', options.authSecret , '--connect-push', options.bindAppPull + , '--connect-sub', options.bindAppPub ]) // websocket , procutil.fork(__filename, [ diff --git a/lib/db/api.js b/lib/db/api.js index c1aa36b8fd..c5d7adfc18 100644 --- a/lib/db/api.js +++ b/lib/db/api.js @@ -152,7 +152,7 @@ dbapi.saveDeviceInitialState = function(serial, device) { , ready: false , reverseForwards: [] , remoteConnect: false - , remoteDebugUrl: null + , remoteConnectUrl: null } return db.run(r.table('devices').get(serial).update(data)) .then(function(stats) { @@ -167,14 +167,14 @@ dbapi.saveDeviceInitialState = function(serial, device) { dbapi.setDeviceConnectUrl = function(serial, url) { return db.run(r.table('devices').get(serial).update({ - remoteDebugUrl: url + remoteConnectUrl: url , remoteConnect: true })) } dbapi.unsetDeviceConnectUrl = function(serial, url) { return db.run(r.table('devices').get(serial).update({ - remoteDebugUrl: null + remoteConnectUrl: null , remoteConnect: false })) } diff --git a/lib/units/api/controllers/devices.js b/lib/units/api/controllers/devices.js index 4afc3b9f49..39a68fbf1c 100644 --- a/lib/units/api/controllers/devices.js +++ b/lib/units/api/controllers/devices.js @@ -49,24 +49,22 @@ function getDeviceBySerial(req, res) { dbapi.loadDevice(serial) .then(function(device) { - if (device) { - datautil.normalize(device, req.user) - - if(fields) { - device = _.pick(device, fields.split(',')) - } - - res.json({ - success: true - , device: device - }) - } - else { - res.status(404).json({ + if (!device) { + return res.status(404).json({ success: false , description: 'Device not found' }) } + + datautil.normalize(device, req.user) + if(fields) { + device = _.pick(device, fields.split(',')) + } + + res.json({ + success: true + , device: device + }) }) .catch(function(err) { log.error('Failed to load device "%s": ', req.params.serial, err.stack) diff --git a/lib/units/api/controllers/user.js b/lib/units/api/controllers/user.js index fcab815589..581b9c6490 100644 --- a/lib/units/api/controllers/user.js +++ b/lib/units/api/controllers/user.js @@ -1,13 +1,16 @@ var util = require('util') -var Promise = require('bluebird') var _ = require('lodash') +var Promise = require('bluebird') +var uuid = require('node-uuid') var dbapi = require('../../../db/api') var logger = require('../../../util/logger') var datautil = require('../../../util/datautil') +var deviceutil = require('../../../util/deviceutil') var wire = require('../../../wire') var wireutil = require('../../../wire/util') +var wirerouter = require('../../../wire/router') var log = logger.createLogger('api:controllers:user') @@ -66,32 +69,29 @@ function getUserDeviceBySerial(req, res) { dbapi.loadDevice(serial) .then(function(device) { - if (device) { - datautil.normalize(device, req.user) - - if (device.owner && device.owner.email === req.user.email) { - if(fields) { - device = _.pick(device, fields.split(',')) - } - - res.json({ - success: true - , device: device - }) - } - else { - res.status(401).json({ - success: false - , description: 'Device is not owned by you' - }) - } - } - else { - res.status(404).json({ + if (!device) { + return res.status(404).json({ success: false , description: 'Device not found' }) } + + datautil.normalize(device, req.user) + if (!deviceutil.isOwnedByUser(device, req.user)) { + return res.status(403).json({ + success: false + , description: 'Device is not owned by you' + }) + } + + if(fields) { + device = _.pick(device, fields.split(',')) + } + + res.json({ + success: true + , device: device + }) }) .catch(function(err) { log.error('Failed to load device "%s": ', req.params.serial, err.stack) @@ -107,55 +107,71 @@ function addUserDevice(req, res) { dbapi.loadDevice(serial) .then(function(device) { - if (device) { - datautil.normalize(device, req.user) - if(device.present && device.ready && !device.using && !device.owner) { - - var requirements = { - 'serial': { - 'value': serial, - 'match': 'exact' - } - } - - req.options.push.send([ - device.channel - , wireutil.envelope( - new wire.GroupMessage( - new wire.OwnerMessage( - req.user.email - , req.user.name - , req.user.group - ) - , timeout - , wireutil.toDeviceRequirements(requirements) - ) - ) - ]) - - res.status(202).json({ - success: true - , description: 'Device Add request is accepted. Check if device is successfully added using pollingUrl' - , pollingUrl: util.format('%s://%s%s/user/devices/%s' - , req.protocol - , req.get('host') - , req.swagger.operation.api.basePath - , serial - ) - }) + if (!device) { + return res.status(404).json({ + success: false + , description: 'Device not found' + }) + } - } else { - res.status(401).json({ - success: false - , description: 'Device is being used or not available' - }) - } - } else { - res.status(404).json({ + datautil.normalize(device, req.user) + if (!deviceutil.isAddable(device, req.user)) { + return res.status(403).json({ success: false - , description: 'Bad device serial' + , description: 'Device is being used or not available' }) } + + // Timer will be called if no JoinGroupMessage is received till 5 seconds + var responseTimer = setTimeout(function() { + req.options.channelRouter.removeListener(wireutil.global, messageListener) + return res.status(504).json({ + success: false + , description: 'Device is not responding' + }) + }, 5000) + + var messageListener = wirerouter() + .on(wire.JoinGroupMessage, function(channel, message) { + if (message.serial === serial && message.owner.email === req.user.email) { + clearTimeout(responseTimer) + req.options.channelRouter.removeListener(wireutil.global, messageListener) + + return res.json({ + success: true + , description: 'Device successfully added' + }) + } + }) + .handler() + + req.options.channelRouter.on(wireutil.global, messageListener) + + req.options.push.send([ + device.channel + , wireutil.envelope( + new wire.GroupMessage( + new wire.OwnerMessage( + req.user.email + , req.user.name + , req.user.group + ) + , timeout + , wireutil.toDeviceRequirements({ + 'serial': { + 'value': serial + , 'match': 'exact' + } + }) + ) + ) + ]) + }) + .catch(function(err) { + log.error('Failed to load device "%s": ', req.params.serial, err.stack) + res.status(500).json({ + success: false + }) }) } @@ -164,49 +180,66 @@ function deleteUserDeviceBySerial(req, res) { dbapi.loadDevice(serial) .then(function(device) { - if (device) { - datautil.normalize(device, req.user) - if(device.using && device.owner.email == req.user.email) { - - var requirements = { - 'serial': { - 'value': serial, - 'match': 'exact' - } - } + if (!device) { + return res.status(404).json({ + success: false + , description: 'Device not found' + }) + } - req.options.push.send([ - device.channel - , wireutil.envelope( - new wire.UngroupMessage( - wireutil.toDeviceRequirements(requirements) - ) - ) - ]) + datautil.normalize(device, req.user) + if (!deviceutil.isOwnedByUser(device, req.user)) { + return res.status(403).json({ + success: false + , description: 'You cannot release this device. Not owned by you' + }) - res.status(202).json({ - success: true - , description: 'Device Release request is accepted. Check if device is successfully removed using pollingUrl' - , pollingUrl: util.format('%s://%s%s/user/devices/%s' - , req.protocol - , req.get('host') - , req.swagger.operation.api.basePath - , serial - ) - }) + } - } else { - res.status(401).json({ + // Timer will be called if no JoinGroupMessage is received till 5 seconds + var responseTimer = setTimeout(function() { + req.options.channelRouter.removeListener(wireutil.global, messageListener) + return res.status(504).json({ success: false - , description: 'You cannot kick this device' - }) - } - } else { - res.status(404).json({ - success: false - , description: 'Bad device serial' + , description: 'Device is not responding' }) - } + }, 5000) + + var messageListener = wirerouter() + .on(wire.LeaveGroupMessage, function(channel, message) { + if (message.serial === serial && message.owner.email === req.user.email) { + clearTimeout(responseTimer) + req.options.channelRouter.removeListener(wireutil.global, messageListener) + + return res.json({ + success: true + , description: 'Device successfully removed' + }) + } + }) + .handler() + + req.options.channelRouter.on(wireutil.global, messageListener) + + req.options.push.send([ + device.channel + , wireutil.envelope( + new wire.UngroupMessage( + wireutil.toDeviceRequirements({ + 'serial': { + 'value': serial + , 'match': 'exact' + } + }) + ) + ) + ]) + }) + .catch(function(err) { + log.error('Failed to load device "%s": ', req.params.serial, err.stack) + res.status(500).json({ + success: false + }) }) } @@ -215,41 +248,58 @@ function remoteConnectUserDeviceBySerial(req, res) { dbapi.loadDevice(serial) .then(function(device) { - if (device) { - datautil.normalize(device, req.user) - - if (device.present && device.ready && device.using && device.owner.email === req.user.email) { - req.options.push.send([ - device.channel - , wireutil.envelope( - new wire.ConnectStartMessage() - ) - ]) - - res.status(202).json({ - success: true - , description: 'Device Connect request is accepted. Check if device is successfully connected using pollingUrl' - , pollingUrl: util.format('%s://%s%s/user/devices/%s' - , req.protocol - , req.get('host') - , req.swagger.operation.api.basePath - , serial - ) - }) - } - else { - res.status(401).json({ - success: false - , description: 'Device is not owned by you or is not available' - }) - } - } - else { - res.status(404).json({ + if (!device) { + return res.status(404).json({ success: false , description: 'Device not found' }) } + + datautil.normalize(device, req.user) + if (!deviceutil.isOwnedByUser(device, req.user)) { + return res.status(403).json({ + success: false + , description: 'Device is not owned by you or is not available' + }) + } + + var responseChannel = 'txn_' + uuid.v4() + req.options.sub.subscribe(responseChannel) + + // Timer will be called if no JoinGroupMessage is received till 5 seconds + var timer = setTimeout(function() { + req.options.channelRouter.removeListener(responseChannel, messageListener) + req.options.sub.unsubscribe(responseChannel) + return res.status(504).json({ + success: false + , description: 'Device is not responding' + }) + }, 5000) + + var messageListener = wirerouter() + .on(wire.ConnectStartedMessage, function(channel, message) { + if (message.serial === serial) { + clearTimeout(timer) + req.options.sub.unsubscribe(responseChannel) + req.options.channelRouter.removeListener(responseChannel, messageListener) + + return res.json({ + success: true + , remoteConnectUrl: message.url + }) + } + }) + .handler() + + req.options.channelRouter.on(responseChannel, messageListener) + + req.options.push.send([ + device.channel + , wireutil.transaction( + responseChannel + , new wire.ConnectStartMessage() + ) + ]) }) .catch(function(err) { log.error('Failed to load device "%s": ', req.params.serial, err.stack) @@ -264,41 +314,59 @@ function remoteDisconnectUserDeviceBySerial(req, res) { dbapi.loadDevice(serial) .then(function(device) { - if (device) { - datautil.normalize(device, req.user) - - if (device.present && device.ready && device.using && device.owner.email == req.user.email) { - req.options.push.send([ - device.channel - , wireutil.envelope( - new wire.ConnectStopMessage() - ) - ]) - res.status(202).json({ - success: true - , description: 'Device Disonnect request is accepted. Check if device is successfully disconnected using pollingUrl' - , pollingUrl: util.format('%s://%s%s/user/devices/%s' - , req.protocol - , req.get('host') - , req.swagger.operation.api.basePath - , serial - ) - }) - } - else { - res.status(401).json({ - success: false - , description: 'Device is not owned by you or is not available' - }) - } - } - else { - res.status(404).json({ + if (!device) { + return res.status(404).json({ success: false , description: 'Device not found' }) } + + datautil.normalize(device, req.user) + if (!deviceutil.isOwnedByUser(device, req.user)) { + return res.status(403).json({ + success: false + , description: 'Device is not owned by you or is not available' + }) + } + + var responseChannel = 'txn_' + uuid.v4() + req.options.sub.subscribe(responseChannel) + + // Timer will be called if no JoinGroupMessage is received till 5 seconds + var timer = setTimeout(function() { + req.options.channelRouter.removeListener(responseChannel, messageListener) + req.options.sub.unsubscribe(responseChannel) + return res.status(504).json({ + success: false + , description: 'Device is not responding' + }) + }, 5000) + + var messageListener = wirerouter() + .on(wire.ConnectStoppedMessage, function(channel, message) { + if (message.serial === serial) { + clearTimeout(timer) + req.options.sub.unsubscribe(responseChannel) + req.options.channelRouter.removeListener(responseChannel, messageListener) + + return res.json({ + success: true + , description: 'Device remote disconnected successfully' + }) + } + }) + .handler() + + req.options.channelRouter.on(responseChannel, messageListener) + + req.options.push.send([ + device.channel + , wireutil.transaction( + responseChannel + , new wire.ConnectStopMessage() + ) + ]) }) .catch(function(err) { log.error('Failed to load device "%s": ', req.params.serial, err.stack) diff --git a/lib/units/api/index.js b/lib/units/api/index.js index 16fc171105..8bc3dd4605 100644 --- a/lib/units/api/index.js +++ b/lib/units/api/index.js @@ -1,5 +1,6 @@ var http = require('http') var path = require('path') +var events = require('events') var express = require('express') var SwaggerExpress = require('swagger-express-mw') @@ -11,11 +12,13 @@ var logger = require('../../util/logger') var zmqutil = require('../../util/zmqutil') var srv = require('../../util/srv') var lifecycle = require('../../util/lifecycle') +var wireutil = require('../../wire/util') module.exports = function(options) { var log = logger.createLogger('api') , app = express() , server = http.createServer(app) + , channelRouter = new events.EventEmitter() var push = zmqutil.socket('push') Promise.map(options.endpoints.push, function(endpoint) { @@ -27,18 +30,47 @@ module.exports = function(options) { }) }) }) - .catch(function(err) { + .catch(function(err) {w log.fatal('Unable to connect to push endpoint', err) lifecycle.fatal() }) + // Input + var sub = zmqutil.socket('sub') + Promise.map(options.endpoints.sub, function(endpoint) { + return srv.resolve(endpoint).then(function(records) { + return srv.attempt(records, function(record) { + log.info('Receiving input from "%s"', record.url) + sub.connect(record.url) + return Promise.resolve(true) + }) + }) + }) + .catch(function(err) { + log.fatal('Unable to connect to sub endpoint', err) + lifecycle.fatal() + }) + + // Establish always-on channels + ;[wireutil.global].forEach(function(channel) { + log.info('Subscribing to permanent channel "%s"', channel) + sub.subscribe(channel) + }) + + sub.on('message', function(channel, data) { + channelRouter.emit(channel.toString(), channel, data) + }) + + // Swagger Express Config var config = { appRoot: __dirname , swaggerFile: path.resolve(__dirname, 'swagger', 'api_v1.yaml') }; SwaggerExpress.create(config, function(err, swaggerExpress) { - if (err) { throw err; } + if (err) { + throw err + } swaggerExpress.register(app); }) @@ -47,6 +79,8 @@ module.exports = function(options) { app.use(function(req, res, next) { var reqOptions = _.merge(options, { 'push': push + , 'sub': sub + , 'channelRouter': channelRouter }) req.options = reqOptions @@ -59,6 +93,17 @@ module.exports = function(options) { , keys: [options.secret] })) + lifecycle.observe(function() { + [push, sub].forEach(function(sock) { + try { + sock.close() + } + catch (err) { + // No-op + } + }) + }) + server.listen(options.port) log.info('Listening on port %d', options.port) } diff --git a/lib/units/api/swagger/api_v1.yaml b/lib/units/api/swagger/api_v1.yaml index aa455bdd60..10a9c97fe2 100644 --- a/lib/units/api/swagger/api_v1.yaml +++ b/lib/units/api/swagger/api_v1.yaml @@ -46,7 +46,7 @@ paths: /user/devices: x-swagger-router-controller: user get: - summary: List devices owned by current user + summary: User Devices description: The User Devices endpoint returns device list owner by current authorized user operationId: getUserDevices tags: @@ -70,22 +70,20 @@ paths: - accessTokenAuth: [] post: summary: Add a device to a user - description: The User Devices endpoint will request stf server for a new device. It will return request accepted if device is usable + description: The User Devices endpoint will request stf server for a new device. operationId: addUserDevice tags: - user parameters: - - name: devices + - name: device in: body description: Device to add required: true schema: $ref: "#/definitions/AddUserDevicePayload" responses: - "202": - description: Add User Device Request Status and polling Url - schema: - $ref: "#/definitions/AddUserDeviceResponse" + "200": + description: Add User Device Status default: description: Unexpected Error schema: @@ -95,8 +93,8 @@ paths: /user/devices/{serial}: x-swagger-router-controller: user get: - summary: Device Information - description: The device enpoint return information about device owned by user + summary: User Device + description: The devices enpoint return information about device owned by user operationId: getUserDeviceBySerial tags: - user @@ -113,9 +111,9 @@ paths: type: string responses: "200": - description: Delete User Device Request Status and polling Url + description: Device Information owned by user schema: - $ref: "#/definitions/DeleteUserDeviceBySerialResponse" + $ref: "#/definitions/DeviceResponse" default: description: Unexpected Error schema: @@ -123,7 +121,7 @@ paths: security: - accessTokenAuth: [] delete: - summary: Release device from user + summary: Delete User Device description: The User Devices endpoint will request for device release from stf server. It will return request accepted if device is being used by current user operationId: deleteUserDeviceBySerial tags: @@ -135,8 +133,8 @@ paths: required: true type: string responses: - "202": - description: Device Release Request Status + "200": + description: Delete User Device Status default: description: Unexpected Error schema: @@ -160,7 +158,7 @@ paths: required: true type: string responses: - "202": + "200": description: Remote Connect User Device Request Status schema: $ref: "#/definitions/RemoteConnectUserDeviceResponse" @@ -183,10 +181,8 @@ paths: required: true type: string responses: - "202": + "200": description: Remote Disonnect User Device Request Status - schema: - $ref: "#/definitions/RemoteDisconnectUserDeviceResponse" default: description: Unexpected Error schema: @@ -194,7 +190,7 @@ paths: security: - accessTokenAuth: [] /user/accessTokens: - x-swagger-router-controller: token + x-swagger-router-controller: user get: summary: Access Tokens description: The Access Tokens endpoints returns titles of all the valid access tokens @@ -298,35 +294,14 @@ definitions: properties: device: type: object - AddUserDeviceResponse: - required: - - pollingUrl - properties: - pollingUrl: - type: string - DeleteUserDeviceBySerialResponse: - required: - - pollingUrl - properties: - pollingUrl: - type: string - RemoteDisconnectUserDeviceResponse: - required: - - pollingUrl - properties: - pollingUrl: - type: string RemoteConnectUserDeviceResponse: required: - - pollingUrl + - remoteConnectUrl + - serial properties: - pollingUrl: + remoteConnectUrl: type: string - ErrorResponse: - required: - - message - properties: - message: + serial: type: string AddUserDevicePayload: description: payload object for adding device to user @@ -339,7 +314,12 @@ definitions: timeout: description: Device timeout in ms. If device is kept idle for this period, it will be automatically disconnected. Default is provider group timeout type: integer - + ErrorResponse: + required: + - message + properties: + message: + type: string securityDefinitions: accessTokenAuth: type: apiKey diff --git a/lib/units/device/plugins/connect.js b/lib/units/device/plugins/connect.js index 55d93ef6f0..90bf3f7901 100644 --- a/lib/units/device/plugins/connect.js +++ b/lib/units/device/plugins/connect.js @@ -142,15 +142,16 @@ module.exports = syrup.serial() channel , reply.okay(url) ]) + // Update DB push.send([ - wireutil.global + channel , wireutil.envelope(new wire.ConnectStartedMessage( options.serial , url )) ]) - log.info('Remote Connect Started for device "%s" at "%s"', options.serial, url) + log.important('Remote Connect Started for device "%s" at "%s"', options.serial, url) }) .catch(function(err) { log.error('Unable to start remote connect service', err.stack) @@ -170,12 +171,12 @@ module.exports = syrup.serial() ]) // Update DB push.send([ - wireutil.global + channel , wireutil.envelope(new wire.ConnectStoppedMessage( options.serial )) ]) - log.info('Remote Connect Stopped for device "%s"', options.serial) + log.important('Remote Connect Stopped for device "%s"', options.serial) }) .catch(function(err) { log.error('Failed to stop connect service', err.stack) diff --git a/lib/units/processor/index.js b/lib/units/processor/index.js index 733e68524b..886fcdbb19 100644 --- a/lib/units/processor/index.js +++ b/lib/units/processor/index.js @@ -167,9 +167,11 @@ module.exports = function(options) { }) .on(wire.ConnectStartedMessage, function(channel, message, data) { dbapi.setDeviceConnectUrl(message.serial, message.url) + appDealer.send([channel, data]) }) .on(wire.ConnectStoppedMessage, function(channel, message, data) { dbapi.unsetDeviceConnectUrl(message.serial) + appDealer.send([channel, data]) }) .on(wire.JoinGroupMessage, function(channel, message, data) { dbapi.setDeviceOwner(message.serial, message.owner) diff --git a/lib/util/datautil.js b/lib/util/datautil.js index 8b04b3687f..12dd9e4223 100644 --- a/lib/util/datautil.js +++ b/lib/util/datautil.js @@ -59,7 +59,7 @@ datautil.applyOwnerOnlyInfo = function(device, user) { if (device.owner && device.owner.email === user.email) { } else { device.remoteConnect = false - device.remoteDebugUrl = null + device.remoteConnectUrl = null } } diff --git a/lib/util/deviceutil.js b/lib/util/deviceutil.js new file mode 100644 index 0000000000..d5264830d9 --- /dev/null +++ b/lib/util/deviceutil.js @@ -0,0 +1,13 @@ +var logger = require('./logger') + +var log = logger.createLogger('util:deviceutil') + +var deviceutil = module.exports = Object.create(null) + +deviceutil.isOwnedByUser = function(device, user) { + return device.present && device.ready && device.owner && device.owner.email === user.email && device.using +} + +deviceutil.isAddable = function(device, user) { + return device.present && device.ready && !device.using && !device.owner +} From fa9ee605f34cbc77b56072aa50843cf44b614d43 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Mon, 14 Dec 2015 14:47:15 +0900 Subject: [PATCH 23/37] update generated json --- lib/units/api/swagger/api_v1_generated.json | 92 +++++++-------------- 1 file changed, 30 insertions(+), 62 deletions(-) diff --git a/lib/units/api/swagger/api_v1_generated.json b/lib/units/api/swagger/api_v1_generated.json index 0924050192..5d4b6c70bb 100644 --- a/lib/units/api/swagger/api_v1_generated.json +++ b/lib/units/api/swagger/api_v1_generated.json @@ -67,7 +67,7 @@ }, "/user/devices": { "get": { - "summary": "List devices owned by current user", + "summary": "User Devices", "description": "The User Devices endpoint returns device list owner by current authorized user", "operationId": "getUserDevices", "tags": [ @@ -104,14 +104,14 @@ }, "post": { "summary": "Add a device to a user", - "description": "The User Devices endpoint will request stf server for a new device. It will return request accepted if device is usable", + "description": "The User Devices endpoint will request stf server for a new device.", "operationId": "addUserDevice", "tags": [ "user" ], "parameters": [ { - "name": "devices", + "name": "device", "in": "body", "description": "Device to add", "required": true, @@ -121,11 +121,8 @@ } ], "responses": { - "202": { - "description": "Add User Device Request Status and polling Url", - "schema": { - "$ref": "#/definitions/AddUserDeviceResponse" - } + "200": { + "description": "Add User Device Status" }, "default": { "description": "Unexpected Error", @@ -143,8 +140,8 @@ }, "/user/devices/{serial}": { "get": { - "summary": "Device Information", - "description": "The device enpoint return information about device owned by user", + "summary": "User Device", + "description": "The devices enpoint return information about device owned by user", "operationId": "getUserDeviceBySerial", "tags": [ "user" @@ -167,9 +164,9 @@ ], "responses": { "200": { - "description": "Delete User Device Request Status and polling Url", + "description": "Device Information owned by user", "schema": { - "$ref": "#/definitions/DeleteUserDeviceBySerialResponse" + "$ref": "#/definitions/DeviceResponse" } }, "default": { @@ -186,7 +183,7 @@ ] }, "delete": { - "summary": "Release device from user", + "summary": "Delete User Device", "description": "The User Devices endpoint will request for device release from stf server. It will return request accepted if device is being used by current user", "operationId": "deleteUserDeviceBySerial", "tags": [ @@ -202,8 +199,8 @@ } ], "responses": { - "202": { - "description": "Device Release Request Status" + "200": { + "description": "Delete User Device Status" }, "default": { "description": "Unexpected Error", @@ -237,7 +234,7 @@ } ], "responses": { - "202": { + "200": { "description": "Remote Connect User Device Request Status", "schema": { "$ref": "#/definitions/RemoteConnectUserDeviceResponse" @@ -273,11 +270,8 @@ } ], "responses": { - "202": { - "description": "Remote Disonnect User Device Request Status", - "schema": { - "$ref": "#/definitions/RemoteDisconnectUserDeviceResponse" - } + "200": { + "description": "Remote Disonnect User Device Request Status" }, "default": { "description": "Unexpected Error", @@ -454,52 +448,16 @@ } } }, - "AddUserDeviceResponse": { - "required": [ - "pollingUrl" - ], - "properties": { - "pollingUrl": { - "type": "string" - } - } - }, - "DeleteUserDeviceBySerialResponse": { - "required": [ - "pollingUrl" - ], - "properties": { - "pollingUrl": { - "type": "string" - } - } - }, - "RemoteDisconnectUserDeviceResponse": { - "required": [ - "pollingUrl" - ], - "properties": { - "pollingUrl": { - "type": "string" - } - } - }, "RemoteConnectUserDeviceResponse": { "required": [ - "pollingUrl" + "remoteConnectUrl", + "serial" ], "properties": { - "pollingUrl": { + "remoteConnectUrl": { "type": "string" - } - } - }, - "ErrorResponse": { - "required": [ - "message" - ], - "properties": { - "message": { + }, + "serial": { "type": "string" } } @@ -519,6 +477,16 @@ "type": "integer" } } + }, + "ErrorResponse": { + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } } }, "securityDefinitions": { From 87fcf194068a7af36d6cd0067c132e71ac00efec Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Mon, 14 Dec 2015 15:06:46 +0900 Subject: [PATCH 24/37] refactor security handler --- lib/units/api/helpers/securityHandlers.js | 71 ++++++++++++++--------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/lib/units/api/helpers/securityHandlers.js b/lib/units/api/helpers/securityHandlers.js index 9c5fa3ee9e..f0b8ef881e 100644 --- a/lib/units/api/helpers/securityHandlers.js +++ b/lib/units/api/helpers/securityHandlers.js @@ -16,46 +16,59 @@ function accessTokenAuth(req, res, next) { , tokenId = authHeader[1] if (format !== 'bearer') { - res.status(401).json({ + return res.status(401).json({ success: false , description: 'Authorization header should be in "bearer $AUTH_TOKEN" format' }) } - if (tokenId) { - dbapi.loadAccessToken(tokenId) - .then(function(token) { - var jwt = token.jwt - , data = jwtutil.decode(jwt, req.options.secret) + if (!tokenId) { + log.error('Bad Access Token Header') + return res.status(401).json({ + success: false + , description: 'Bad Credentials' + }) + } - if (data) { - dbapi.loadUser(data.email) - .then(function(user) { - if (user) { - req.user = user - next() - } - }) + dbapi.loadAccessToken(tokenId) + .then(function(token) { + if (!token) { + return res.status(401).json({ + success: false + , description: 'Bad Credentials' + }) + } + + var jwt = token.jwt + , data = jwtutil.decode(jwt, req.options.secret) + + if (!data) { + return res.status(500).json({ + success: false + }) + } + dbapi.loadUser(data.email) + .then(function(user) { + if (user) { + req.user = user + next() } else { - res.status(500).json({ + return res.status(500).json({ success: false }) } }) - .catch(function(err) { - log.error('Failed to load token: ', err.stack) - res.status(401).json({ - success: false, - description: 'Bad Credentials' + .catch(function(err) { + log.error('Failed to load user: ', err.stack) }) + }) + .catch(function(err) { + log.error('Failed to load token: ', err.stack) + return res.status(401).json({ + success: false + , description: 'Bad Credentials' }) - } else { - log.error('Bad Access Token Header') - res.status(401).json({ - success: false, - description: 'Bad Credentials' }) - } } // Request is coming from browser app // TODO: Remove this once frontend become stateless @@ -68,7 +81,7 @@ function accessTokenAuth(req, res, next) { next() } else { - res.json(500, { + return res.status(500).json({ success: false }) } @@ -77,8 +90,8 @@ function accessTokenAuth(req, res, next) { } else { res.status(401).json({ - success: false, - description: 'Requires Authentication' + success: false + , description: 'Requires Authentication' }) } } From d5aade25b6f35dd2f88a0b2f627d12a547833b7f Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Thu, 17 Dec 2015 03:43:14 +0900 Subject: [PATCH 25/37] Add API doc --- doc/API.md | 298 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 doc/API.md diff --git a/doc/API.md b/doc/API.md new file mode 100644 index 0000000000..17e4ab2c75 --- /dev/null +++ b/doc/API.md @@ -0,0 +1,298 @@ +## Overview + +This document describes important RESTful APIs of STF. These APIs may open the door to new creative uses of STF. You can use these APIs for running UI tests on real devices. Integrating STF with CI tools such as Jenkins etc. Building device farm for data extraction for smartphone app data mining. You can even use these apis for creating bitcoin mining farm. Possibilities are infinite! + +*PS: Please, don't forget to give us our share, if you successfully mine some bitcoins ;)*. + +Let's talk about APIs now. Internally STF uses [Swagger](http://swagger.io/) interface for its API implementation. For those who don't know about Swagger, Swagger provides specifications for RESTful apis. By using it you can generate documentations and client SDKs in various language automatically for Swagger-enabled apps. This gives you power to use STF APIs in any language of your favorite. You can read more about Swagger at [here](http://swagger.io/getting-started/). + +### Swagger Documentations + +You can check swagger documentations for STF APIs from [here](https://vbanthia.github.io/angular-swagger-ui). From this document you can check latest APIs, their definitions, usage etc. + +## APIs +- [Authentication](#authentication) +- [Devices](#devices) +- [User](#user) + +### Authentication +STF uses oauth2 for RESTful APIs authentication. In order to use APIs, you will first need to generate an access token. Access tokens can be easily generated from STF UI. Just go to the **Settings** tab and generate new access token from keys section. Don't forget to save this token somewhere, you will not be able to see it again. + +Put access token in the header of every request + +Curl Sample +```bash +curl -H "Authorization: bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user +``` + +NodeJS Sample +```js + +var Swagger = require('swagger-client'); + +var SWAGGER_URL = 'https://stf.example.org/api/v1/swagger.json'; +var AUTH_TOKEN = 'xx-xxxx-xx'; + +// Without Promise +var client = new Swagger({ + url: SWAGGER_URL +, authorizations: { + accessTokenAuth: new Swagger.ApiKeyAuthorization('Authorization', 'bearer ' + AUTH_TOKEN, 'header') + } +, success: function() { + client.user.getUser(function(user) { + console.log(user.obj) + }) + } +}); + +// Using Promise +var clientWithPromise = new Swagger({ + url: SWAGGER_URL +, usePromise: true +, authorizations: { + accessTokenAuth: new Swagger.ApiKeyAuthorization('Authorization', 'bearer ' + AUTH_TOKEN, 'header') + } +}) + +clientWithPromise.then(function(api) { + api.user.getUser() + .then(function(res) { + console.log(res.obj) + }) +}) +``` + +### Devices +#### /devices + +**List all STF devices including disconnected or offline ones** + +```bash +GET /api/v1/devices +``` + +Curl Sample + +```bash +curl -H "Authorization: bearer OAUTH-TOKEN" https://stf.example.org/api/v1/devices +``` + +NodeJS Sample + +```js +clientWithPromise.then(function(api) { + api.devices.getDevices() + .then(function(res) { + console.log(res.obj.devices.length) + }) +}) + +// OR +clientWithPromise.then(function(api) { + api.devices.getDevices({fields: 'serial,using,ready'}) + .then(function(res) { + console.log(res.obj.devices) + }) +}) +``` + +#### /devices/{serial} + +**Provide information to specific device** + +```bash +GET /api/v1/devices/{serial} +``` + +Curl Sample + +```bash +curl -H "Authorization: bearer OAUTH-TOKEN" https://stf.example.org/api/v1/devices/xxxxxxxxx +``` + +NodeJS Sample + +```js +clientWithPromise.then(function(api) { + api.devices.getDeviceBySerial({serial: 'EP7351U3WQ'}) + .then(function(res) { + console.log(res.obj.device) + }) +}) + +// OR +clientWithPromise.then(function(api) { + api.devices.getDeviceBySerial({serial: 'EP7351U3WQ', fields: 'serial,using,ready'}) + .then(function(res) { + console.log(res.obj.device) + }) +}) +``` + +### User +#### /user + +**Provides current authenticated user information** + +```bash +GET /api/v1/user +``` + +Curl Sample + +```bash +curl -H "Authorization: bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user +``` + +NodeJS Sample + +```js +clientWithPromise.then(function(api) { + api.user.getUser() + .then(function(res) { + console.log(res.obj) + }) +}) +``` + +#### /user/devices + +**Provide devices owned by user** + +```bash +GET /api/v1/user/devices +``` + +Curl Sample + +```bash +curl -H "Authorization: bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices +``` + +NodeJS Sample + +```js +clientWithPromise.then(function(api) { + api.user.getUserDevices() + .then(function(res) { + console.log(res.obj.devices.length) + }) +}) + +// OR +clientWithPromise.then(function(api) { + api.user.getUserDevices({fields: 'serial,using,ready'}) + .then(function(res) { + console.log(res.obj.devices) + }) +}) +``` + +**Add new device for user** + +```bash +POST /api/v1/user/devices +``` + +Curl Sample + +```bash +curl -X POST --header "Content-Type:application/json" --data '{"serial":"EP7351U3WQ"}' -H "Authorization: bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices +``` + +NodeJS Sample + +```js +var device = {serial: 'CB5125LBYM', timeout: 900000 } + +clientWithPromise.then(function(api) { + return api.user.addUserDevice({device: device}) + .then(function(res) { + console.log(res.obj) + }) +}) +.catch(function(err) { + console.log(err) +}) +``` + +**Delete a device from user** + +```bash +DELETE /api/v1/user/devices/{serial} +``` + +Curl Sample + +```bash +curl -X DELETE -H "Authorization: bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices/{serial} +``` + +NodeJS Sample + +```js +clientWithPromise.then(function(api) { + return api.user.deleteUserDeviceBySerial({serial: 'CB5125LBYM'}) + .then(function(res) { + console.log(res.obj) + }) +}) +.catch(function(err) { + console.log(err) +}) +``` + +#### /user/devices/{serial}/remoteConnect + +**Remote Connect** + +```bash +POST /api/v1/user/devices/{serial}/remoteConnect +``` + +Curl Sample + +```bash +curl -X POST --header "Content-Type:application/json" -H "Authorization: bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices/{serial}/remoteConnect +``` + +NodeJS Sample + +```js +clientWithPromise.then(function(api) { + return api.user.remoteConnectUserDeviceBySerial({serial: 'CB5125LBYM'}) + .then(function(res) { + console.log(res.obj.remoteConnectUrl) + }) +}) +.catch(function(err) { + console.log(err) +}) +``` + +**Remote Disconnect** + +```bash +DELETE /api/v1/user/devices/{serial}/remoteConnect +``` + +Curl Sample + +```bash +curl -X DELETE -H "Authorization: bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices/{serial}/remoteConnect +``` + +NodeJS Sample + +```js +clientWithPromise.then(function(api) { + return api.user.remoteDisconnectUserDeviceBySerial({serial: 'CB5125LBYM'}) + .then(function(res) { + console.log(res.obj) + }) +}) +.catch(function(err) { + console.log(err) +}) +``` From b1f7f67eb7675a9be4d66a0846f3ec53b92d0758 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Fri, 18 Dec 2015 15:57:28 +0900 Subject: [PATCH 26/37] Deployment Doc: Update rethinkdb version and fix api unit file. --- doc/DEPLOYMENT.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/DEPLOYMENT.md b/doc/DEPLOYMENT.md index 7c97b1e643..6f227115d2 100644 --- a/doc/DEPLOYMENT.md +++ b/doc/DEPLOYMENT.md @@ -128,7 +128,7 @@ Requires=docker.service EnvironmentFile=/etc/environment TimeoutStartSec=0 Restart=always -ExecStartPre=/usr/bin/docker pull rethinkdb:2.1.1 +ExecStartPre=/usr/bin/docker pull rethinkdb:2.1.5 ExecStartPre=-/usr/bin/docker kill %p ExecStartPre=-/usr/bin/docker rm %p ExecStartPre=/usr/bin/mkdir -p /srv/rethinkdb @@ -138,7 +138,7 @@ ExecStart=/usr/bin/docker run --rm \ -v /srv/rethinkdb:/data \ -e "AUTHKEY=YOUR_RETHINKDB_AUTH_KEY_HERE_IF_ANY" \ --net host \ - rethinkdb:2.1.1 \ + rethinkdb:2.1.5 \ rethinkdb --bind all \ --cache-size 8192 ExecStop=-/usr/bin/docker stop -t 10 %p @@ -681,7 +681,9 @@ ExecStart=/usr/bin/docker run --rm \ -e "SECRET=YOUR_SESSION_SECRET_HERE" \ -p %i:3000 \ openstf/stf:latest \ - stf api --port 3000 + stf api --port 3000 \ + --connect-sub tcp://appside.stf.example.org:7150 \ + --connect-push tcp://appside.stf.example.org:7170 ExecStop=-/usr/bin/docker stop -t 10 %p-%i ``` From 484e56be129a88c52c6300d0b9c1ffac56d3bea6 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Fri, 18 Dec 2015 16:00:24 +0900 Subject: [PATCH 27/37] Use capital letter in Bearer as per rfc spec --- doc/API.md | 22 +++++++++++----------- lib/units/api/helpers/securityHandlers.js | 6 ++++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/doc/API.md b/doc/API.md index 17e4ab2c75..d37624725b 100644 --- a/doc/API.md +++ b/doc/API.md @@ -22,7 +22,7 @@ Put access token in the header of every request Curl Sample ```bash -curl -H "Authorization: bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user +curl -H "Authorization: Bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user ``` NodeJS Sample @@ -37,7 +37,7 @@ var AUTH_TOKEN = 'xx-xxxx-xx'; var client = new Swagger({ url: SWAGGER_URL , authorizations: { - accessTokenAuth: new Swagger.ApiKeyAuthorization('Authorization', 'bearer ' + AUTH_TOKEN, 'header') + accessTokenAuth: new Swagger.ApiKeyAuthorization('Authorization', 'Bearer ' + AUTH_TOKEN, 'header') } , success: function() { client.user.getUser(function(user) { @@ -51,7 +51,7 @@ var clientWithPromise = new Swagger({ url: SWAGGER_URL , usePromise: true , authorizations: { - accessTokenAuth: new Swagger.ApiKeyAuthorization('Authorization', 'bearer ' + AUTH_TOKEN, 'header') + accessTokenAuth: new Swagger.ApiKeyAuthorization('Authorization', 'Bearer ' + AUTH_TOKEN, 'header') } }) @@ -75,7 +75,7 @@ GET /api/v1/devices Curl Sample ```bash -curl -H "Authorization: bearer OAUTH-TOKEN" https://stf.example.org/api/v1/devices +curl -H "Authorization: Bearer OAUTH-TOKEN" https://stf.example.org/api/v1/devices ``` NodeJS Sample @@ -108,7 +108,7 @@ GET /api/v1/devices/{serial} Curl Sample ```bash -curl -H "Authorization: bearer OAUTH-TOKEN" https://stf.example.org/api/v1/devices/xxxxxxxxx +curl -H "Authorization: Bearer OAUTH-TOKEN" https://stf.example.org/api/v1/devices/xxxxxxxxx ``` NodeJS Sample @@ -142,7 +142,7 @@ GET /api/v1/user Curl Sample ```bash -curl -H "Authorization: bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user +curl -H "Authorization: Bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user ``` NodeJS Sample @@ -167,7 +167,7 @@ GET /api/v1/user/devices Curl Sample ```bash -curl -H "Authorization: bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices +curl -H "Authorization: Bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices ``` NodeJS Sample @@ -198,7 +198,7 @@ POST /api/v1/user/devices Curl Sample ```bash -curl -X POST --header "Content-Type:application/json" --data '{"serial":"EP7351U3WQ"}' -H "Authorization: bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices +curl -X POST --header "Content-Type:application/json" --data '{"serial":"EP7351U3WQ"}' -H "Authorization: Bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices ``` NodeJS Sample @@ -226,7 +226,7 @@ DELETE /api/v1/user/devices/{serial} Curl Sample ```bash -curl -X DELETE -H "Authorization: bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices/{serial} +curl -X DELETE -H "Authorization: Bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices/{serial} ``` NodeJS Sample @@ -254,7 +254,7 @@ POST /api/v1/user/devices/{serial}/remoteConnect Curl Sample ```bash -curl -X POST --header "Content-Type:application/json" -H "Authorization: bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices/{serial}/remoteConnect +curl -X POST --header "Content-Type:application/json" -H "Authorization: Bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices/{serial}/remoteConnect ``` NodeJS Sample @@ -280,7 +280,7 @@ DELETE /api/v1/user/devices/{serial}/remoteConnect Curl Sample ```bash -curl -X DELETE -H "Authorization: bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices/{serial}/remoteConnect +curl -X DELETE -H "Authorization: Bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices/{serial}/remoteConnect ``` NodeJS Sample diff --git a/lib/units/api/helpers/securityHandlers.js b/lib/units/api/helpers/securityHandlers.js index f0b8ef881e..a856eb0f58 100644 --- a/lib/units/api/helpers/securityHandlers.js +++ b/lib/units/api/helpers/securityHandlers.js @@ -9,16 +9,18 @@ module.exports = { accessTokenAuth: accessTokenAuth } +// Specifications: https://tools.ietf.org/html/rfc6750#section-2.1 + function accessTokenAuth(req, res, next) { if (req.headers.authorization) { var authHeader = req.headers.authorization.split(' ') , format = authHeader[0] , tokenId = authHeader[1] - if (format !== 'bearer') { + if (format !== 'Bearer') { return res.status(401).json({ success: false - , description: 'Authorization header should be in "bearer $AUTH_TOKEN" format' + , description: 'Authorization header should be in "Bearer $AUTH_TOKEN" format' }) } From c686d1a5bb3da41cd8f3ce5b0b9d5520b354f82d Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Mon, 4 Jan 2016 13:56:24 +0900 Subject: [PATCH 28/37] Documentation: Add connect-device, disconnect-device sample script --- doc/API.md | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 141 insertions(+), 7 deletions(-) diff --git a/doc/API.md b/doc/API.md index d37624725b..0fc7c41383 100644 --- a/doc/API.md +++ b/doc/API.md @@ -15,6 +15,10 @@ You can check swagger documentations for STF APIs from [here](https://vbanthia.g - [Devices](#devices) - [User](#user) +## Sample Usages +- [ConnectDevice](#connect-device) +- [DisconnectDevice](#disconnect-device) + ### Authentication STF uses oauth2 for RESTful APIs authentication. In order to use APIs, you will first need to generate an access token. Access tokens can be easily generated from STF UI. Just go to the **Settings** tab and generate new access token from keys section. Don't forget to save this token somewhere, you will not be able to see it again. @@ -58,7 +62,8 @@ var clientWithPromise = new Swagger({ clientWithPromise.then(function(api) { api.user.getUser() .then(function(res) { - console.log(res.obj) + console.log(res.obj.user.email) + // vishal.banthia.vb@gmail.com }) }) ``` @@ -85,6 +90,7 @@ clientWithPromise.then(function(api) { api.devices.getDevices() .then(function(res) { console.log(res.obj.devices.length) + // 50 }) }) @@ -93,6 +99,8 @@ clientWithPromise.then(function(api) { api.devices.getDevices({fields: 'serial,using,ready'}) .then(function(res) { console.log(res.obj.devices) + // [ { serial: 'xxxx', using: false, ready: true }, + // { serial: 'yyyy', using: false, ready: true }] }) }) ``` @@ -115,17 +123,19 @@ NodeJS Sample ```js clientWithPromise.then(function(api) { - api.devices.getDeviceBySerial({serial: 'EP7351U3WQ'}) + api.devices.getDeviceBySerial({serial: 'xxxx'}) .then(function(res) { - console.log(res.obj.device) + console.log(res.obj.device.serial) + // xxxx }) }) // OR clientWithPromise.then(function(api) { - api.devices.getDeviceBySerial({serial: 'EP7351U3WQ', fields: 'serial,using,ready'}) + api.devices.getDeviceBySerial({serial: 'xxxx', fields: 'serial,using,ready'}) .then(function(res) { console.log(res.obj.device) + // { serial: 'xxxx', using: false, ready: true } }) }) ``` @@ -151,7 +161,8 @@ NodeJS Sample clientWithPromise.then(function(api) { api.user.getUser() .then(function(res) { - console.log(res.obj) + console.log(res.obj.user.email) + // vishal.banthia.vb@gmail.com }) }) ``` @@ -177,6 +188,7 @@ clientWithPromise.then(function(api) { api.user.getUserDevices() .then(function(res) { console.log(res.obj.devices.length) + // 1 }) }) @@ -185,6 +197,7 @@ clientWithPromise.then(function(api) { api.user.getUserDevices({fields: 'serial,using,ready'}) .then(function(res) { console.log(res.obj.devices) + // [ { serial: 'xxxx', using: true, ready: true } ] }) }) ``` @@ -204,12 +217,13 @@ curl -X POST --header "Content-Type:application/json" --data '{"serial":"EP7351U NodeJS Sample ```js -var device = {serial: 'CB5125LBYM', timeout: 900000 } +var device = {serial: 'yyyy', timeout: 900000 } clientWithPromise.then(function(api) { return api.user.addUserDevice({device: device}) .then(function(res) { console.log(res.obj) + // { success: true, description: 'Device successfully added' } }) }) .catch(function(err) { @@ -233,9 +247,10 @@ NodeJS Sample ```js clientWithPromise.then(function(api) { - return api.user.deleteUserDeviceBySerial({serial: 'CB5125LBYM'}) + return api.user.deleteUserDeviceBySerial({serial: 'yyyy'}) .then(function(res) { console.log(res.obj) + // { success: true, description: 'Device successfully removed' } }) }) .catch(function(err) { @@ -264,10 +279,13 @@ clientWithPromise.then(function(api) { return api.user.remoteConnectUserDeviceBySerial({serial: 'CB5125LBYM'}) .then(function(res) { console.log(res.obj.remoteConnectUrl) + // $PROVIDER_IP:16829 }) }) .catch(function(err) { console.log(err) + // {"success":false, + // "description":"Device is not owned by you or is not available"}' } }) ``` @@ -290,9 +308,125 @@ clientWithPromise.then(function(api) { return api.user.remoteDisconnectUserDeviceBySerial({serial: 'CB5125LBYM'}) .then(function(res) { console.log(res.obj) + // { success: true, + // description: 'Device remote disconnected successfully' } }) }) .catch(function(err) { console.log(err) + // {"success":false,"description":"Device is not owned by you or is not available"} +}) +``` + +## Sample Usages +### Connect Device + +```js +// stf-connect.js + +var Swagger = require('swagger-client'); + +var SWAGGER_URL = 'https://stf.example.org/api/v1/swagger.json'; +var AUTH_TOKEN = 'xx-xxxx-xx'; + +// Using Promise +var clientWithPromise = new Swagger({ + url: SWAGGER_URL +, usePromise: true +, authorizations: { + accessTokenAuth: new Swagger.ApiKeyAuthorization('Authorization', 'bearer ' + AUTH_TOKEN, 'header') + } +}) + +var serial = process.argv.slice(2)[0] + +clientWithPromise.then(function(api) { + return api.devices.getDeviceBySerial({ + serial: serial + , fields: 'serial,present,ready,using,owner' + }).then(function(res) { + // check if device can be added or not + var device = res.obj.device + if (!device.present || !device.ready || device.using || device.owner) { + throw new Error('Device is not available') + } + + return api.user.addUserDevice({ + device: { + serial: device.serial + , timeout: 900000 + } + }).then(function(res) { + if (!res.obj.success) { + throw new Error('Could not connect to device') + } + + return api.user.remoteConnectUserDeviceBySerial({ + serial: device.serial + }).then(function(res) { + console.log(res.obj.remoteConnectUrl) + }) + }) + }) }) ``` +```bash +node stf-connect.js xxxx +# $PROVIDR_IP:16829 +``` + +### Disconnect Device + +```js +var Swagger = require('swagger-client'); + +var SWAGGER_URL = 'https://stf.example.org/api/v1/swagger.json'; +var AUTH_TOKEN = 'xx-xxxx-xx'; + +// Using Promise +var clientWithPromise = new Swagger({ + url: SWAGGER_URL +, usePromise: true +, authorizations: { + accessTokenAuth: new Swagger.ApiKeyAuthorization('Authorization', 'bearer ' + AUTH_TOKEN, 'header') + } +}) + +var serial = process.argv.slice(2)[0] + +clientWithPromise.then(function(api) { + return api.user.getUserDevices({ + serial: serial + , fields: 'serial,present,ready,using,owner' + }).then(function(res) { + // check if device can be added or not + var devices = res.obj.devices + + var hasDevice = false + devices.forEach(function(device) { + if(device.serial === serial) { + hasDevice = true; + } + }) + + if (!hasDevice) { + throw new Error('You are not owner') + } + + return api.user.deleteUserDeviceBySerial({ + serial: serial + }).then(function(res) { + if (!res.obj.success) { + throw new Error('Could not disconnect to device') + } + + console.log('Device disconnected successfully!') + }) + }) +}) +``` + +```bash +node stf-disconnect.js xxxx +# Device disconnected successfully! +``` From 981fb0d449829f9b173033a7f23eb41332f64264 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Mon, 4 Jan 2016 14:02:18 +0900 Subject: [PATCH 29/37] Update CHANGELOG and bump version to 2.0.0 --- CHANGELOG.md | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23251cb4c4..1b9a9e596c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 2.0.0 () + +Major release addressing the following: + +### Enhancements + +- STF REST API, refer [API Doc](./doc/API.md) + + ## 1.2.0 (2016-07-22) Minor release addressing the following: diff --git a/package.json b/package.json index 996134e2b7..8dda937f19 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stf", - "version": "1.2.0", + "version": "2.0.0", "description": "Smartphone Test Farm", "keywords": [ "adb", From a875d85c4633fcbbcdc23905322bb47e8784c67a Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Mon, 4 Jan 2016 21:53:52 +0900 Subject: [PATCH 30/37] Documentation: Remove personal info from sample --- doc/API.md | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/doc/API.md b/doc/API.md index 0fc7c41383..34829f270f 100644 --- a/doc/API.md +++ b/doc/API.md @@ -63,7 +63,7 @@ clientWithPromise.then(function(api) { api.user.getUser() .then(function(res) { console.log(res.obj.user.email) - // vishal.banthia.vb@gmail.com + // vishal@example.com }) }) ``` @@ -162,7 +162,7 @@ clientWithPromise.then(function(api) { api.user.getUser() .then(function(res) { console.log(res.obj.user.email) - // vishal.banthia.vb@gmail.com + // vishal@example.com }) }) ``` @@ -330,17 +330,17 @@ var SWAGGER_URL = 'https://stf.example.org/api/v1/swagger.json'; var AUTH_TOKEN = 'xx-xxxx-xx'; // Using Promise -var clientWithPromise = new Swagger({ +var client = new Swagger({ url: SWAGGER_URL , usePromise: true , authorizations: { - accessTokenAuth: new Swagger.ApiKeyAuthorization('Authorization', 'bearer ' + AUTH_TOKEN, 'header') + accessTokenAuth: new Swagger.ApiKeyAuthorization('Authorization', 'Bearer ' + AUTH_TOKEN, 'header') } }) var serial = process.argv.slice(2)[0] -clientWithPromise.then(function(api) { +client.then(function(api) { return api.devices.getDeviceBySerial({ serial: serial , fields: 'serial,present,ready,using,owner' @@ -348,9 +348,11 @@ clientWithPromise.then(function(api) { // check if device can be added or not var device = res.obj.device if (!device.present || !device.ready || device.using || device.owner) { - throw new Error('Device is not available') + console.log('Device is not available') + return } + // add device to user return api.user.addUserDevice({ device: { serial: device.serial @@ -358,9 +360,11 @@ clientWithPromise.then(function(api) { } }).then(function(res) { if (!res.obj.success) { - throw new Error('Could not connect to device') + console.log('Could not add device') + return } + // get remote connect url return api.user.remoteConnectUserDeviceBySerial({ serial: device.serial }).then(function(res) { @@ -383,26 +387,25 @@ var Swagger = require('swagger-client'); var SWAGGER_URL = 'https://stf.example.org/api/v1/swagger.json'; var AUTH_TOKEN = 'xx-xxxx-xx'; -// Using Promise -var clientWithPromise = new Swagger({ +var client = new Swagger({ url: SWAGGER_URL , usePromise: true , authorizations: { - accessTokenAuth: new Swagger.ApiKeyAuthorization('Authorization', 'bearer ' + AUTH_TOKEN, 'header') + accessTokenAuth: new Swagger.ApiKeyAuthorization('Authorization', 'Bearer ' + AUTH_TOKEN, 'header') } }) var serial = process.argv.slice(2)[0] -clientWithPromise.then(function(api) { +client.then(function(api) { return api.user.getUserDevices({ serial: serial , fields: 'serial,present,ready,using,owner' }).then(function(res) { - // check if device can be added or not + // check if user has that device or not var devices = res.obj.devices - var hasDevice = false + devices.forEach(function(device) { if(device.serial === serial) { hasDevice = true; @@ -410,16 +413,17 @@ clientWithPromise.then(function(api) { }) if (!hasDevice) { - throw new Error('You are not owner') + console.log('You do not own that device') + return } return api.user.deleteUserDeviceBySerial({ serial: serial }).then(function(res) { if (!res.obj.success) { - throw new Error('Could not disconnect to device') + console.log('Could not disconnect') + return } - console.log('Device disconnected successfully!') }) }) From b60cb6ef6a597487067137bd0f719772e248d588 Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Wed, 3 Feb 2016 17:22:05 +0900 Subject: [PATCH 31/37] Fix eslint errors --- lib/units/api/controllers/devices.js | 14 ++++++---- lib/units/api/controllers/user.js | 26 +++++++++---------- lib/units/api/helpers/securityHandlers.js | 9 ++++--- lib/units/api/index.js | 18 ++++++------- lib/util/datautil.js | 4 ++- lib/util/deviceutil.js | 11 ++++++-- .../components/stf/device/device-service.js | 2 +- 7 files changed, 49 insertions(+), 35 deletions(-) diff --git a/lib/units/api/controllers/devices.js b/lib/units/api/controllers/devices.js index 39a68fbf1c..ba236b4fda 100644 --- a/lib/units/api/controllers/devices.js +++ b/lib/units/api/controllers/devices.js @@ -23,10 +23,12 @@ function getDevices(req, res) { list.forEach(function(device) { datautil.normalize(device, req.user) + var responseDevice = device + if (fields) { - device = _.pick(device, fields.split(',')) + responseDevice = _.pick(device, fields.split(',')) } - deviceList.push(device) + deviceList.push(responseDevice) }) res.json({ @@ -57,13 +59,15 @@ function getDeviceBySerial(req, res) { } datautil.normalize(device, req.user) - if(fields) { - device = _.pick(device, fields.split(',')) + var responseDevice = device + + if (fields) { + responseDevice = _.pick(device, fields.split(',')) } res.json({ success: true - , device: device + , device: responseDevice }) }) .catch(function(err) { diff --git a/lib/units/api/controllers/user.js b/lib/units/api/controllers/user.js index 581b9c6490..6cfb5fa277 100644 --- a/lib/units/api/controllers/user.js +++ b/lib/units/api/controllers/user.js @@ -43,10 +43,11 @@ function getUserDevices(req, res) { list.forEach(function(device) { datautil.normalize(device, req.user) + var responseDevice = device if (fields) { - device = _.pick(device, fields.split(',')) + responseDevice = _.pick(device, fields.split(',')) } - deviceList.push(device) + deviceList.push(responseDevice) }) res.json({ @@ -84,13 +85,14 @@ function getUserDeviceBySerial(req, res) { }) } - if(fields) { - device = _.pick(device, fields.split(',')) + var responseDevice = device + if (fields) { + responseDevice = _.pick(device, fields.split(',')) } res.json({ success: true - , device: device + , device: responseDevice }) }) .catch(function(err) { @@ -158,9 +160,9 @@ function addUserDevice(req, res) { ) , timeout , wireutil.toDeviceRequirements({ - 'serial': { - 'value': serial - , 'match': 'exact' + serial: { + value: serial + , match: 'exact' } }) ) @@ -193,7 +195,6 @@ function deleteUserDeviceBySerial(req, res) { success: false , description: 'You cannot release this device. Not owned by you' }) - } // Timer will be called if no JoinGroupMessage is received till 5 seconds @@ -226,9 +227,9 @@ function deleteUserDeviceBySerial(req, res) { , wireutil.envelope( new wire.UngroupMessage( wireutil.toDeviceRequirements({ - 'serial': { - 'value': serial - , 'match': 'exact' + serial: { + value: serial + , match: 'exact' } }) ) @@ -314,7 +315,6 @@ function remoteDisconnectUserDeviceBySerial(req, res) { dbapi.loadDevice(serial) .then(function(device) { - if (!device) { return res.status(404).json({ success: false diff --git a/lib/units/api/helpers/securityHandlers.js b/lib/units/api/helpers/securityHandlers.js index a856eb0f58..99eedd1c85 100644 --- a/lib/units/api/helpers/securityHandlers.js +++ b/lib/units/api/helpers/securityHandlers.js @@ -14,8 +14,8 @@ module.exports = { function accessTokenAuth(req, res, next) { if (req.headers.authorization) { var authHeader = req.headers.authorization.split(' ') - , format = authHeader[0] - , tokenId = authHeader[1] + var format = authHeader[0] + var tokenId = authHeader[1] if (format !== 'Bearer') { return res.status(401).json({ @@ -42,7 +42,7 @@ function accessTokenAuth(req, res, next) { } var jwt = token.jwt - , data = jwtutil.decode(jwt, req.options.secret) + var data = jwtutil.decode(jwt, req.options.secret) if (!data) { return res.status(500).json({ @@ -54,7 +54,8 @@ function accessTokenAuth(req, res, next) { if (user) { req.user = user next() - } else { + } + else { return res.status(500).json({ success: false }) diff --git a/lib/units/api/index.js b/lib/units/api/index.js index 8bc3dd4605..f89e46204b 100644 --- a/lib/units/api/index.js +++ b/lib/units/api/index.js @@ -16,9 +16,9 @@ var wireutil = require('../../wire/util') module.exports = function(options) { var log = logger.createLogger('api') - , app = express() - , server = http.createServer(app) - , channelRouter = new events.EventEmitter() + var app = express() + var server = http.createServer(app) + var channelRouter = new events.EventEmitter() var push = zmqutil.socket('push') Promise.map(options.endpoints.push, function(endpoint) { @@ -30,7 +30,7 @@ module.exports = function(options) { }) }) }) - .catch(function(err) {w + .catch(function(err) { log.fatal('Unable to connect to push endpoint', err) lifecycle.fatal() }) @@ -65,22 +65,22 @@ module.exports = function(options) { var config = { appRoot: __dirname , swaggerFile: path.resolve(__dirname, 'swagger', 'api_v1.yaml') - }; + } SwaggerExpress.create(config, function(err, swaggerExpress) { if (err) { throw err } - swaggerExpress.register(app); + swaggerExpress.register(app) }) // Adding options in request, so that swagger controller // can use it. app.use(function(req, res, next) { var reqOptions = _.merge(options, { - 'push': push - , 'sub': sub - , 'channelRouter': channelRouter + push: push + , sub: sub + , channelRouter: channelRouter }) req.options = reqOptions diff --git a/lib/util/datautil.js b/lib/util/datautil.js index 12dd9e4223..1220a0b50d 100644 --- a/lib/util/datautil.js +++ b/lib/util/datautil.js @@ -57,7 +57,9 @@ datautil.applyOwner = function(device, user) { // Only owner can see this information datautil.applyOwnerOnlyInfo = function(device, user) { if (device.owner && device.owner.email === user.email) { - } else { + // No-op + } + else { device.remoteConnect = false device.remoteConnectUrl = null } diff --git a/lib/util/deviceutil.js b/lib/util/deviceutil.js index d5264830d9..7b125d9a6f 100644 --- a/lib/util/deviceutil.js +++ b/lib/util/deviceutil.js @@ -5,9 +5,16 @@ var log = logger.createLogger('util:deviceutil') var deviceutil = module.exports = Object.create(null) deviceutil.isOwnedByUser = function(device, user) { - return device.present && device.ready && device.owner && device.owner.email === user.email && device.using + return device.present && + device.ready && + device.owner && + device.owner.email === user.email && + device.using } deviceutil.isAddable = function(device, user) { - return device.present && device.ready && !device.using && !device.owner + return device.present && + device.ready && + !device.using && + !device.owner } diff --git a/res/app/components/stf/device/device-service.js b/res/app/components/stf/device/device-service.js index 833962ede3..a5672a46f8 100644 --- a/res/app/components/stf/device/device-service.js +++ b/res/app/components/stf/device/device-service.js @@ -182,7 +182,7 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi }) oboe('/api/v1/user/devices') - .node('devices[*]', function (device) { + .node('devices[*]', function(device) { tracker.add(device) }) From 9d8325d7656180baec9b6c7cc721e2f68362d39d Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Thu, 21 Jul 2016 14:32:23 +0530 Subject: [PATCH 32/37] Update API documentation --- doc/API.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/doc/API.md b/doc/API.md index 34829f270f..4def6ca859 100644 --- a/doc/API.md +++ b/doc/API.md @@ -1,14 +1,10 @@ ## Overview -This document describes important RESTful APIs of STF. These APIs may open the door to new creative uses of STF. You can use these APIs for running UI tests on real devices. Integrating STF with CI tools such as Jenkins etc. Building device farm for data extraction for smartphone app data mining. You can even use these apis for creating bitcoin mining farm. Possibilities are infinite! - -*PS: Please, don't forget to give us our share, if you successfully mine some bitcoins ;)*. - -Let's talk about APIs now. Internally STF uses [Swagger](http://swagger.io/) interface for its API implementation. For those who don't know about Swagger, Swagger provides specifications for RESTful apis. By using it you can generate documentations and client SDKs in various language automatically for Swagger-enabled apps. This gives you power to use STF APIs in any language of your favorite. You can read more about Swagger at [here](http://swagger.io/getting-started/). +STF API is a RESTful API which allows you to reserve and release any STF device. Internally STF uses [Swagger](http://swagger.io/) interface for its API implementation. For those who don't know about Swagger, Swagger provides specifications for RESTful apis. By using it you can generate documentations and client SDKs in various language automatically for Swagger-enabled apps. This gives you power to use STF APIs in any language of your favorite. You can read more about Swagger at [here](http://swagger.io/getting-started/). ### Swagger Documentations -You can check swagger documentations for STF APIs from [here](https://vbanthia.github.io/angular-swagger-ui). From this document you can check latest APIs, their definitions, usage etc. +You can check swagger documentations for STF API from [here](https://vbanthia.github.io/angular-swagger-ui). ## APIs - [Authentication](#authentication) From fd099a057aa56fda0183d76b4ac66e688a3a476e Mon Sep 17 00:00:00 2001 From: Simo Kinnunen Date: Thu, 28 Jul 2016 23:55:49 +0900 Subject: [PATCH 33/37] Update RethinkDB version in deployment guide. --- doc/DEPLOYMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/DEPLOYMENT.md b/doc/DEPLOYMENT.md index 6f227115d2..66b17e8875 100644 --- a/doc/DEPLOYMENT.md +++ b/doc/DEPLOYMENT.md @@ -128,7 +128,7 @@ Requires=docker.service EnvironmentFile=/etc/environment TimeoutStartSec=0 Restart=always -ExecStartPre=/usr/bin/docker pull rethinkdb:2.1.5 +ExecStartPre=/usr/bin/docker pull rethinkdb:2.3 ExecStartPre=-/usr/bin/docker kill %p ExecStartPre=-/usr/bin/docker rm %p ExecStartPre=/usr/bin/mkdir -p /srv/rethinkdb From b9c3ec4f28e47dbef463c6b0c372db04fe024e08 Mon Sep 17 00:00:00 2001 From: Simo Kinnunen Date: Fri, 29 Jul 2016 01:04:11 +0900 Subject: [PATCH 34/37] Change wording in API docs a bit and add them to the README. --- CHANGELOG.md | 7 ++- README.md | 8 ++-- doc/API.md | 126 ++++++++++++++++++++++++++++++--------------------- 3 files changed, 85 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b9a9e596c..237f5a19ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,16 @@ # Changelog -## 2.0.0 () +## 2.0.0 (next) Major release addressing the following: ### Enhancements -- STF REST API, refer [API Doc](./doc/API.md) +- Added a simple [REST API](doc/API.md). Huge thanks to @vbanthia! +### Breaking changes + +- The API server is a new app unit that must be added to your deployment. Please see the [deployment guide](doc/DEPLOYMENT.md) for up to date instructions. ## 1.2.0 (2016-07-22) diff --git a/README.md b/README.md index 5c8ef3ffd5..650166efa0 100644 --- a/README.md +++ b/README.md @@ -75,21 +75,23 @@ Please [contact us][contact-link] for sponsor arrangements. Both recurring and o - Rudimentary Play Store account management * List, remove and add new accounts (adding may not work on all devices) - Display hardware specs +* Simple REST [API](doc/API.md) ## Status STF is in continued, active development, but development is still largely funded by individual team members and their unpaid free time, leading to slow progress. While normal for many open source projects, STF is quite heavy on the hardware side, and is therefore somewhat of a money sink. See [how to become a sponsor](#how-to-become-a-sponsor) if you or your company would like to support future development. -We're also actively working to expand the team. Welcome **@vbanthia** as our newest full contributor! +We're also actively working to expand the team, don't be afraid to ask if you're interested. ### Short term goals Here are some things we are planning to address ASAP. -1. Properly expose the new VNC functionality in the UI -2. Implement a basic REST API for programmatically using devices +1. Performance +2. Properly expose the new VNC functionality in the UI 3. Properly reset user data between uses (Android 4.0+) 4. Automated scheduled restarts for devices +5. More! ### Consulting services diff --git a/doc/API.md b/doc/API.md index 4def6ca859..49a21fe57b 100644 --- a/doc/API.md +++ b/doc/API.md @@ -1,31 +1,35 @@ +# STF API + ## Overview -STF API is a RESTful API which allows you to reserve and release any STF device. Internally STF uses [Swagger](http://swagger.io/) interface for its API implementation. For those who don't know about Swagger, Swagger provides specifications for RESTful apis. By using it you can generate documentations and client SDKs in various language automatically for Swagger-enabled apps. This gives you power to use STF APIs in any language of your favorite. You can read more about Swagger at [here](http://swagger.io/getting-started/). +STF API is a RESTful API which allows you to reserve and release any STF device. Internally STF uses [Swagger](http://swagger.io/) interface for its API implementation. For those who don't know about Swagger, Swagger provides specifications for RESTful apis. By using it you can generate documentation and client SDKs in various language automatically for Swagger-enabled apps. This gives you power to use STF APIs in any language of your favorite. You can read more about Swagger [here](http://swagger.io/getting-started/). -### Swagger Documentations +## Swagger documentation -You can check swagger documentations for STF API from [here](https://vbanthia.github.io/angular-swagger-ui). +Swagger documentation for the API is available [here](https://vbanthia.github.io/angular-swagger-ui). ## APIs + - [Authentication](#authentication) - [Devices](#devices) - [User](#user) -## Sample Usages -- [ConnectDevice](#connect-device) -- [DisconnectDevice](#disconnect-device) +A few [examples](#examples) are also provided. ### Authentication -STF uses oauth2 for RESTful APIs authentication. In order to use APIs, you will first need to generate an access token. Access tokens can be easily generated from STF UI. Just go to the **Settings** tab and generate new access token from keys section. Don't forget to save this token somewhere, you will not be able to see it again. -Put access token in the header of every request +STF uses OAuth 2.0 for authentication. In order to use the API, you will first need to generate an access token. Access tokens can be easily generated from the STF UI. Just go to the **Settings** tab and generate a new access token in **Keys** section. Don't forget to save this token somewhere, you will not be able to see it again. + +The access token must be included in every request. + +Using cURL: -Curl Sample ```bash -curl -H "Authorization: Bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user +curl -H "Authorization: Bearer YOUR-TOKEN-HERE" https://stf.example.org/api/v1/user ``` -NodeJS Sample +Using Node.js: + ```js var Swagger = require('swagger-client'); @@ -64,22 +68,33 @@ clientWithPromise.then(function(api) { }) ``` +### Pretty printing output + +Please use [jq](https://stedolan.github.io/jq/manual/) for pretty printing. It's very easy to use: + +```sh +curl -H "Authorization: Bearer YOUR-TOKEN-HERE" https://stf.example.org/api/v1/devices | jq . +``` + +It also provides much more complex patterns for retrieving and/or filtering data. + ### Devices -#### /devices -**List all STF devices including disconnected or offline ones** +#### GET /devices + +List **all** STF devices (including disconnected or otherwise inaccessible devices). ```bash GET /api/v1/devices ``` -Curl Sample +Using cURL: ```bash -curl -H "Authorization: Bearer OAUTH-TOKEN" https://stf.example.org/api/v1/devices +curl -H "Authorization: Bearer YOUR-TOKEN-HERE" https://stf.example.org/api/v1/devices ``` -NodeJS Sample +Using Node.js: ```js clientWithPromise.then(function(api) { @@ -101,21 +116,21 @@ clientWithPromise.then(function(api) { }) ``` -#### /devices/{serial} +#### GET /devices/{serial} -**Provide information to specific device** +Returns information about a specific device. ```bash GET /api/v1/devices/{serial} ``` -Curl Sample +Using cURL: ```bash -curl -H "Authorization: Bearer OAUTH-TOKEN" https://stf.example.org/api/v1/devices/xxxxxxxxx +curl -H "Authorization: Bearer YOUR-TOKEN-HERE" https://stf.example.org/api/v1/devices/xxxxxxxxx ``` -NodeJS Sample +Using Node.js: ```js clientWithPromise.then(function(api) { @@ -137,21 +152,22 @@ clientWithPromise.then(function(api) { ``` ### User -#### /user -**Provides current authenticated user information** +#### GET /user + +Returns information about yourself (the authenticated user). ```bash GET /api/v1/user ``` -Curl Sample +Using cURL: ```bash -curl -H "Authorization: Bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user +curl -H "Authorization: Bearer YOUR-TOKEN-HERE" https://stf.example.org/api/v1/user ``` -NodeJS Sample +Using Node.js: ```js clientWithPromise.then(function(api) { @@ -163,21 +179,21 @@ clientWithPromise.then(function(api) { }) ``` -#### /user/devices +#### GET /user/devices -**Provide devices owned by user** +Returns a list of devices currently being used by the authenticated user. ```bash GET /api/v1/user/devices ``` -Curl Sample +Using cURL: ```bash -curl -H "Authorization: Bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices +curl -H "Authorization: Bearer YOUR-TOKEN-HERE" https://stf.example.org/api/v1/user/devices ``` -NodeJS Sample +Using Node.js: ```js clientWithPromise.then(function(api) { @@ -198,19 +214,21 @@ clientWithPromise.then(function(api) { }) ``` -**Add new device for user** +#### POST /user/devices + +Attempts to add a device under the authenticated user's control. This is analogous to pressing "Use" in the UI. ```bash POST /api/v1/user/devices ``` -Curl Sample +Using cURL: ```bash -curl -X POST --header "Content-Type:application/json" --data '{"serial":"EP7351U3WQ"}' -H "Authorization: Bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices +curl -X POST --header "Content-Type: application/json" --data '{"serial":"EP7351U3WQ"}' -H "Authorization: Bearer YOUR-TOKEN-HERE" https://stf.example.org/api/v1/user/devices ``` -NodeJS Sample +Using Node.js: ```js var device = {serial: 'yyyy', timeout: 900000 } @@ -227,19 +245,21 @@ clientWithPromise.then(function(api) { }) ``` -**Delete a device from user** +#### DELETE /user/devices/{serial} + +Removes a device from the authenticated user's device list. This is analogous to pressing "Stop using" in the UI. ```bash DELETE /api/v1/user/devices/{serial} ``` -Curl Sample +Using cURL: ```bash -curl -X DELETE -H "Authorization: Bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices/{serial} +curl -X DELETE -H "Authorization: Bearer YOUR-TOKEN-HERE" https://stf.example.org/api/v1/user/devices/{serial} ``` -NodeJS Sample +Using Node.js: ```js clientWithPromise.then(function(api) { @@ -254,21 +274,21 @@ clientWithPromise.then(function(api) { }) ``` -#### /user/devices/{serial}/remoteConnect +#### POST /user/devices/{serial}/remoteConnect -**Remote Connect** +Allows you to retrieve the remote debug URL (i.e. an `adb connect`able address) for a device the authenticated user controls. ```bash POST /api/v1/user/devices/{serial}/remoteConnect ``` -Curl Sample +Using cURL: ```bash -curl -X POST --header "Content-Type:application/json" -H "Authorization: Bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices/{serial}/remoteConnect +curl -X POST --header "Content-Type: application/json" -H "Authorization: Bearer YOUR-TOKEN-HERE" https://stf.example.org/api/v1/user/devices/{serial}/remoteConnect ``` -NodeJS Sample +Using Node.js: ```js clientWithPromise.then(function(api) { @@ -285,19 +305,21 @@ clientWithPromise.then(function(api) { }) ``` -**Remote Disconnect** +#### DELETE /api/v1/user/devices/{serial}/remoteConnect + +Disconnect a remote debugging session. ```bash DELETE /api/v1/user/devices/{serial}/remoteConnect ``` -Curl Sample +Using cURL: ```bash -curl -X DELETE -H "Authorization: Bearer OAUTH-TOKEN" https://stf.example.org/api/v1/user/devices/{serial}/remoteConnect +curl -X DELETE -H "Authorization: Bearer YOUR-TOKEN-HERE" https://stf.example.org/api/v1/user/devices/{serial}/remoteConnect ``` -NodeJS Sample +Using Node.js: ```js clientWithPromise.then(function(api) { @@ -314,8 +336,9 @@ clientWithPromise.then(function(api) { }) ``` -## Sample Usages -### Connect Device +## Examples + +### Connect to a device and retrieve its remote debug URL ```js // stf-connect.js @@ -370,12 +393,13 @@ client.then(function(api) { }) }) ``` + ```bash node stf-connect.js xxxx # $PROVIDR_IP:16829 ``` -### Disconnect Device +### Disconnect a device once you no longer need it ```js var Swagger = require('swagger-client'); From af6a363e437d20cdb318cd165d336c005cb2e785 Mon Sep 17 00:00:00 2001 From: Simo Kinnunen Date: Fri, 29 Jul 2016 01:15:53 +0900 Subject: [PATCH 35/37] Mention that you should set up your ADB key before using the remoteConnect API. --- doc/API.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/API.md b/doc/API.md index 49a21fe57b..0f752aa2cf 100644 --- a/doc/API.md +++ b/doc/API.md @@ -278,6 +278,8 @@ clientWithPromise.then(function(api) { Allows you to retrieve the remote debug URL (i.e. an `adb connect`able address) for a device the authenticated user controls. +_Note that if you haven't added your ADB key to STF yet, the device may be in unauthorized state after connecting to it for the first time. We recommend you make sure your ADB key has already been set up properly before you start using this API. You can add your ADB key from the settings page, or by connecting to a device you're actively using in the UI and responding to the dialog that appears._ + ```bash POST /api/v1/user/devices/{serial}/remoteConnect ``` From c550b65084359f49c211bcda68bec83dbbf12199 Mon Sep 17 00:00:00 2001 From: Simo Kinnunen Date: Fri, 29 Jul 2016 01:32:13 +0900 Subject: [PATCH 36/37] Mention the Appium example in the docs. --- CHANGELOG.md | 1 + doc/API.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 237f5a19ca..68192ced83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Major release addressing the following: ### Enhancements - Added a simple [REST API](doc/API.md). Huge thanks to @vbanthia! + * Also, we have an example showing [how to use the API with Appium](https://github.com/openstf/stf-appium-example). ### Breaking changes diff --git a/doc/API.md b/doc/API.md index 0f752aa2cf..3826cab59b 100644 --- a/doc/API.md +++ b/doc/API.md @@ -14,7 +14,7 @@ Swagger documentation for the API is available [here](https://vbanthia.github.io - [Devices](#devices) - [User](#user) -A few [examples](#examples) are also provided. +A few [examples](#examples) are also provided. We also have a more advanced example showing [how to use the API with Appium](https://github.com/openstf/stf-appium-example). ### Authentication From bc37c89094baca8a56a2b24024adaa502689e0ae Mon Sep 17 00:00:00 2001 From: Vishal Banthia Date: Thu, 28 Jul 2016 22:07:53 +0530 Subject: [PATCH 37/37] Add 2.0.0 release date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68192ced83..a1074c3049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 2.0.0 (next) +## 2.0.0 (2016-07-29) Major release addressing the following: