mirror of
https://github.com/DeviceFarmer/stf.git
synced 2026-04-21 16:25:13 +02:00
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## 2.0.0 (2016-07-29)
|
||||
|
||||
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
|
||||
|
||||
- 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)
|
||||
|
||||
Minor release addressing the following:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
458
doc/API.md
Normal file
458
doc/API.md
Normal file
@@ -0,0 +1,458 @@
|
||||
# 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 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 documentation
|
||||
|
||||
Swagger documentation for the API is available [here](https://vbanthia.github.io/angular-swagger-ui).
|
||||
|
||||
## APIs
|
||||
|
||||
- [Authentication](#authentication)
|
||||
- [Devices](#devices)
|
||||
- [User](#user)
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer YOUR-TOKEN-HERE" https://stf.example.org/api/v1/user
|
||||
```
|
||||
|
||||
Using Node.js:
|
||||
|
||||
```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.user.email)
|
||||
// vishal@example.com
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
#### GET /devices
|
||||
|
||||
List **all** STF devices (including disconnected or otherwise inaccessible devices).
|
||||
|
||||
```bash
|
||||
GET /api/v1/devices
|
||||
```
|
||||
|
||||
Using cURL:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer YOUR-TOKEN-HERE" https://stf.example.org/api/v1/devices
|
||||
```
|
||||
|
||||
Using Node.js:
|
||||
|
||||
```js
|
||||
clientWithPromise.then(function(api) {
|
||||
api.devices.getDevices()
|
||||
.then(function(res) {
|
||||
console.log(res.obj.devices.length)
|
||||
// 50
|
||||
})
|
||||
})
|
||||
|
||||
// OR
|
||||
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 }]
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### GET /devices/{serial}
|
||||
|
||||
Returns information about a specific device.
|
||||
|
||||
```bash
|
||||
GET /api/v1/devices/{serial}
|
||||
```
|
||||
|
||||
Using cURL:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer YOUR-TOKEN-HERE" https://stf.example.org/api/v1/devices/xxxxxxxxx
|
||||
```
|
||||
|
||||
Using Node.js:
|
||||
|
||||
```js
|
||||
clientWithPromise.then(function(api) {
|
||||
api.devices.getDeviceBySerial({serial: 'xxxx'})
|
||||
.then(function(res) {
|
||||
console.log(res.obj.device.serial)
|
||||
// xxxx
|
||||
})
|
||||
})
|
||||
|
||||
// OR
|
||||
clientWithPromise.then(function(api) {
|
||||
api.devices.getDeviceBySerial({serial: 'xxxx', fields: 'serial,using,ready'})
|
||||
.then(function(res) {
|
||||
console.log(res.obj.device)
|
||||
// { serial: 'xxxx', using: false, ready: true }
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### User
|
||||
|
||||
#### GET /user
|
||||
|
||||
Returns information about yourself (the authenticated user).
|
||||
|
||||
```bash
|
||||
GET /api/v1/user
|
||||
```
|
||||
|
||||
Using cURL:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer YOUR-TOKEN-HERE" https://stf.example.org/api/v1/user
|
||||
```
|
||||
|
||||
Using Node.js:
|
||||
|
||||
```js
|
||||
clientWithPromise.then(function(api) {
|
||||
api.user.getUser()
|
||||
.then(function(res) {
|
||||
console.log(res.obj.user.email)
|
||||
// vishal@example.com
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### GET /user/devices
|
||||
|
||||
Returns a list of devices currently being used by the authenticated user.
|
||||
|
||||
```bash
|
||||
GET /api/v1/user/devices
|
||||
```
|
||||
|
||||
Using cURL:
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer YOUR-TOKEN-HERE" https://stf.example.org/api/v1/user/devices
|
||||
```
|
||||
|
||||
Using Node.js:
|
||||
|
||||
```js
|
||||
clientWithPromise.then(function(api) {
|
||||
api.user.getUserDevices()
|
||||
.then(function(res) {
|
||||
console.log(res.obj.devices.length)
|
||||
// 1
|
||||
})
|
||||
})
|
||||
|
||||
// OR
|
||||
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 } ]
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### 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
|
||||
```
|
||||
|
||||
Using cURL:
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
Using Node.js:
|
||||
|
||||
```js
|
||||
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) {
|
||||
console.log(err)
|
||||
})
|
||||
```
|
||||
|
||||
#### 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}
|
||||
```
|
||||
|
||||
Using cURL:
|
||||
|
||||
```bash
|
||||
curl -X DELETE -H "Authorization: Bearer YOUR-TOKEN-HERE" https://stf.example.org/api/v1/user/devices/{serial}
|
||||
```
|
||||
|
||||
Using Node.js:
|
||||
|
||||
```js
|
||||
clientWithPromise.then(function(api) {
|
||||
return api.user.deleteUserDeviceBySerial({serial: 'yyyy'})
|
||||
.then(function(res) {
|
||||
console.log(res.obj)
|
||||
// { success: true, description: 'Device successfully removed' }
|
||||
})
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.log(err)
|
||||
})
|
||||
```
|
||||
|
||||
#### POST /user/devices/{serial}/remoteConnect
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
Using cURL:
|
||||
|
||||
```bash
|
||||
curl -X POST --header "Content-Type: application/json" -H "Authorization: Bearer YOUR-TOKEN-HERE" https://stf.example.org/api/v1/user/devices/{serial}/remoteConnect
|
||||
```
|
||||
|
||||
Using Node.js:
|
||||
|
||||
```js
|
||||
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"}' }
|
||||
})
|
||||
```
|
||||
|
||||
#### DELETE /api/v1/user/devices/{serial}/remoteConnect
|
||||
|
||||
Disconnect a remote debugging session.
|
||||
|
||||
```bash
|
||||
DELETE /api/v1/user/devices/{serial}/remoteConnect
|
||||
```
|
||||
|
||||
Using cURL:
|
||||
|
||||
```bash
|
||||
curl -X DELETE -H "Authorization: Bearer YOUR-TOKEN-HERE" https://stf.example.org/api/v1/user/devices/{serial}/remoteConnect
|
||||
```
|
||||
|
||||
Using Node.js:
|
||||
|
||||
```js
|
||||
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"}
|
||||
})
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Connect to a device and retrieve its remote debug URL
|
||||
|
||||
```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 client = new Swagger({
|
||||
url: SWAGGER_URL
|
||||
, usePromise: true
|
||||
, authorizations: {
|
||||
accessTokenAuth: new Swagger.ApiKeyAuthorization('Authorization', 'Bearer ' + AUTH_TOKEN, 'header')
|
||||
}
|
||||
})
|
||||
|
||||
var serial = process.argv.slice(2)[0]
|
||||
|
||||
client.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) {
|
||||
console.log('Device is not available')
|
||||
return
|
||||
}
|
||||
|
||||
// add device to user
|
||||
return api.user.addUserDevice({
|
||||
device: {
|
||||
serial: device.serial
|
||||
, timeout: 900000
|
||||
}
|
||||
}).then(function(res) {
|
||||
if (!res.obj.success) {
|
||||
console.log('Could not add device')
|
||||
return
|
||||
}
|
||||
|
||||
// get remote connect url
|
||||
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 a device once you no longer need it
|
||||
|
||||
```js
|
||||
var Swagger = require('swagger-client');
|
||||
|
||||
var SWAGGER_URL = 'https://stf.example.org/api/v1/swagger.json';
|
||||
var AUTH_TOKEN = 'xx-xxxx-xx';
|
||||
|
||||
var client = new Swagger({
|
||||
url: SWAGGER_URL
|
||||
, usePromise: true
|
||||
, authorizations: {
|
||||
accessTokenAuth: new Swagger.ApiKeyAuthorization('Authorization', 'Bearer ' + AUTH_TOKEN, 'header')
|
||||
}
|
||||
})
|
||||
|
||||
var serial = process.argv.slice(2)[0]
|
||||
|
||||
client.then(function(api) {
|
||||
return api.user.getUserDevices({
|
||||
serial: serial
|
||||
, fields: 'serial,present,ready,using,owner'
|
||||
}).then(function(res) {
|
||||
// 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;
|
||||
}
|
||||
})
|
||||
|
||||
if (!hasDevice) {
|
||||
console.log('You do not own that device')
|
||||
return
|
||||
}
|
||||
|
||||
return api.user.deleteUserDeviceBySerial({
|
||||
serial: serial
|
||||
}).then(function(res) {
|
||||
if (!res.obj.success) {
|
||||
console.log('Could not disconnect')
|
||||
return
|
||||
}
|
||||
console.log('Device disconnected successfully!')
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
```bash
|
||||
node stf-disconnect.js xxxx
|
||||
# Device disconnected successfully!
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -127,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.3
|
||||
ExecStartPre=-/usr/bin/docker kill %p
|
||||
ExecStartPre=-/usr/bin/docker rm %p
|
||||
ExecStartPre=/usr/bin/mkdir -p /srv/rethinkdb
|
||||
@@ -137,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
|
||||
@@ -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,39 @@ 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 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
|
||||
```
|
||||
|
||||
## Optional units
|
||||
|
||||
These units are optional and don't affect the way STF works in any way.
|
||||
@@ -836,6 +870,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 +942,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;
|
||||
}
|
||||
|
||||
64
lib/cli.js
64
lib/cli.js
@@ -758,6 +758,9 @@ program
|
||||
.option('-u, --app-url <url>'
|
||||
, 'URL to app'
|
||||
, String)
|
||||
.option('-i, --api-url <url>'
|
||||
, 'URL to api'
|
||||
, String)
|
||||
.option('-a, --auth-url <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,49 @@ program
|
||||
})
|
||||
})
|
||||
|
||||
program
|
||||
.command('api')
|
||||
.description('start api')
|
||||
.option('-p, --port <port>'
|
||||
, 'port (or $PORT)'
|
||||
, Number
|
||||
, process.env.PORT || 7106)
|
||||
.option('-i, --ssid <ssid>'
|
||||
, 'session SSID (or $SSID)'
|
||||
, String
|
||||
, process.env.SSID || 'ssid')
|
||||
.option('-s, --secret <secret>'
|
||||
, 'secret (or $SECRET)'
|
||||
, String
|
||||
, process.env.SECRET)
|
||||
.option('-c, --connect-push <endpoint>'
|
||||
, 'push endpoint'
|
||||
, cliutil.list)
|
||||
.option('-u, --connect-sub <endpoint>'
|
||||
, 'sub endpoint'
|
||||
, cliutil.list)
|
||||
.action(function(options) {
|
||||
if (!options.secret) {
|
||||
this.missingArgument('--secret')
|
||||
}
|
||||
if (!options.connectPush) {
|
||||
this.missingArgument('--connect-push')
|
||||
}
|
||||
if (!options.connectSub) {
|
||||
this.missingArgument('--connect-sub')
|
||||
}
|
||||
|
||||
require('./units/api')({
|
||||
port: options.port
|
||||
, ssid: options.ssid
|
||||
, secret: options.secret
|
||||
, endpoints: {
|
||||
push: options.connectPush
|
||||
, sub: options.connectSub
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
program
|
||||
.command('websocket')
|
||||
.description('start websocket')
|
||||
@@ -1110,6 +1160,10 @@ program
|
||||
, 'app port'
|
||||
, Number
|
||||
, 7105)
|
||||
.option('--api-port <port>'
|
||||
, 'api port'
|
||||
, Number
|
||||
, 7106)
|
||||
.option('--websocket-port <port>'
|
||||
, 'websocket port'
|
||||
, Number
|
||||
@@ -1285,6 +1339,14 @@ program
|
||||
return extra
|
||||
})()))
|
||||
|
||||
// api
|
||||
, procutil.fork(__filename, [
|
||||
'api'
|
||||
, '--port', options.apiPort
|
||||
, '--secret', options.authSecret
|
||||
, '--connect-push', options.bindAppPull
|
||||
, '--connect-sub', options.bindAppPub
|
||||
])
|
||||
// websocket
|
||||
, procutil.fork(__filename, [
|
||||
'websocket'
|
||||
@@ -1326,6 +1388,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'
|
||||
|
||||
@@ -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'
|
||||
}))
|
||||
@@ -151,6 +151,8 @@ dbapi.saveDeviceInitialState = function(serial, device) {
|
||||
, statusChangedAt: r.now()
|
||||
, ready: false
|
||||
, reverseForwards: []
|
||||
, remoteConnect: false
|
||||
, remoteConnectUrl: 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({
|
||||
remoteConnectUrl: url
|
||||
, remoteConnect: true
|
||||
}))
|
||||
}
|
||||
|
||||
dbapi.unsetDeviceConnectUrl = function(serial, url) {
|
||||
return db.run(r.table('devices').get(serial).update({
|
||||
remoteConnectUrl: null
|
||||
, remoteConnect: false
|
||||
}))
|
||||
}
|
||||
|
||||
dbapi.saveDeviceStatus = function(serial, status) {
|
||||
return db.run(r.table('devices').get(serial).update({
|
||||
status: status
|
||||
@@ -329,4 +345,8 @@ dbapi.loadAccessTokens = function(email) {
|
||||
}))
|
||||
}
|
||||
|
||||
dbapi.loadAccessToken = function(id) {
|
||||
return db.run(r.table('accessTokens').get(id))
|
||||
}
|
||||
|
||||
module.exports = dbapi
|
||||
|
||||
41
lib/units/api/config/default.yaml
Normal file
41
lib/units/api/config/default.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
_swagger_security:
|
||||
name: swagger_security
|
||||
securityHandlersModule: helpers/securityHandlers
|
||||
|
||||
# pipe for all swagger-node controllers
|
||||
swagger_controllers:
|
||||
- onError: json_error_handler
|
||||
- swagger_params_parser
|
||||
- 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...
|
||||
79
lib/units/api/controllers/devices.js
Normal file
79
lib/units/api/controllers/devices.js
Normal file
@@ -0,0 +1,79 @@
|
||||
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:devices')
|
||||
|
||||
module.exports = {
|
||||
getDevices: getDevices
|
||||
, getDeviceBySerial: getDeviceBySerial
|
||||
}
|
||||
|
||||
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)
|
||||
var responseDevice = device
|
||||
|
||||
if (fields) {
|
||||
responseDevice = _.pick(device, fields.split(','))
|
||||
}
|
||||
deviceList.push(responseDevice)
|
||||
})
|
||||
|
||||
res.json({
|
||||
success: true
|
||||
, devices: deviceList
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error('Failed to load device list: ', err.stack)
|
||||
res.status(500).json({
|
||||
success: false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
return res.status(404).json({
|
||||
success: false
|
||||
, description: 'Device not found'
|
||||
})
|
||||
}
|
||||
|
||||
datautil.normalize(device, req.user)
|
||||
var responseDevice = device
|
||||
|
||||
if (fields) {
|
||||
responseDevice = _.pick(device, fields.split(','))
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true
|
||||
, device: responseDevice
|
||||
})
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error('Failed to load device "%s": ', req.params.serial, err.stack)
|
||||
res.status(500).json({
|
||||
success: false
|
||||
})
|
||||
})
|
||||
}
|
||||
400
lib/units/api/controllers/user.js
Normal file
400
lib/units/api/controllers/user.js
Normal file
@@ -0,0 +1,400 @@
|
||||
var util = require('util')
|
||||
|
||||
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')
|
||||
|
||||
module.exports = {
|
||||
getUser: getUser
|
||||
, getUserDevices: getUserDevices
|
||||
, addUserDevice: addUserDevice
|
||||
, getUserDeviceBySerial: getUserDeviceBySerial
|
||||
, deleteUserDeviceBySerial: deleteUserDeviceBySerial
|
||||
, remoteConnectUserDeviceBySerial: remoteConnectUserDeviceBySerial
|
||||
, remoteDisconnectUserDeviceBySerial: remoteDisconnectUserDeviceBySerial
|
||||
, getUserAccessTokens: getUserAccessTokens
|
||||
}
|
||||
|
||||
function getUser(req, res) {
|
||||
res.json({
|
||||
success: true
|
||||
, user: req.user
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
var responseDevice = device
|
||||
if (fields) {
|
||||
responseDevice = _.pick(device, fields.split(','))
|
||||
}
|
||||
deviceList.push(responseDevice)
|
||||
})
|
||||
|
||||
res.json({
|
||||
success: true
|
||||
, devices: deviceList
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error('Failed to load device list: ', err.stack)
|
||||
res.status(500).json({
|
||||
success: false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
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'
|
||||
})
|
||||
}
|
||||
|
||||
var responseDevice = device
|
||||
if (fields) {
|
||||
responseDevice = _.pick(device, fields.split(','))
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true
|
||||
, device: responseDevice
|
||||
})
|
||||
})
|
||||
.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
|
||||
|
||||
dbapi.loadDevice(serial)
|
||||
.then(function(device) {
|
||||
if (!device) {
|
||||
return res.status(404).json({
|
||||
success: false
|
||||
, description: 'Device not found'
|
||||
})
|
||||
}
|
||||
|
||||
datautil.normalize(device, req.user)
|
||||
if (!deviceutil.isAddable(device, req.user)) {
|
||||
return res.status(403).json({
|
||||
success: false
|
||||
, 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
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function deleteUserDeviceBySerial(req, res) {
|
||||
var serial = req.swagger.params.serial.value
|
||||
|
||||
dbapi.loadDevice(serial)
|
||||
.then(function(device) {
|
||||
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: 'You cannot release this device. Not owned by you'
|
||||
})
|
||||
}
|
||||
|
||||
// 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.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
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function remoteConnectUserDeviceBySerial(req, res) {
|
||||
var serial = req.swagger.params.serial.value
|
||||
|
||||
dbapi.loadDevice(serial)
|
||||
.then(function(device) {
|
||||
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)
|
||||
res.status(500).json({
|
||||
success: false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function remoteDisconnectUserDeviceBySerial(req, res) {
|
||||
var serial = req.swagger.params.serial.value
|
||||
|
||||
dbapi.loadDevice(serial)
|
||||
.then(function(device) {
|
||||
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)
|
||||
res.status(500).json({
|
||||
success: false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
res.json({
|
||||
success: true
|
||||
, titles: titles
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error('Failed to load tokens: ', err.stack)
|
||||
res.status(500).json({
|
||||
success: false
|
||||
})
|
||||
})
|
||||
}
|
||||
100
lib/units/api/helpers/securityHandlers.js
Normal file
100
lib/units/api/helpers/securityHandlers.js
Normal file
@@ -0,0 +1,100 @@
|
||||
var dbapi = require('../../../db/api')
|
||||
var jwtutil = require('../../../util/jwtutil')
|
||||
var urlutil = require('../../../util/urlutil')
|
||||
var logger = require('../../../util/logger')
|
||||
|
||||
var log = logger.createLogger('api:helpers:securityHandlers')
|
||||
|
||||
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(' ')
|
||||
var format = authHeader[0]
|
||||
var tokenId = authHeader[1]
|
||||
|
||||
if (format !== 'Bearer') {
|
||||
return res.status(401).json({
|
||||
success: false
|
||||
, description: 'Authorization header should be in "Bearer $AUTH_TOKEN" format'
|
||||
})
|
||||
}
|
||||
|
||||
if (!tokenId) {
|
||||
log.error('Bad Access Token Header')
|
||||
return res.status(401).json({
|
||||
success: false
|
||||
, description: 'Bad Credentials'
|
||||
})
|
||||
}
|
||||
|
||||
dbapi.loadAccessToken(tokenId)
|
||||
.then(function(token) {
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
success: false
|
||||
, description: 'Bad Credentials'
|
||||
})
|
||||
}
|
||||
|
||||
var jwt = token.jwt
|
||||
var 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 {
|
||||
return res.status(500).json({
|
||||
success: false
|
||||
})
|
||||
}
|
||||
})
|
||||
.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'
|
||||
})
|
||||
})
|
||||
}
|
||||
// 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) {
|
||||
if (user) {
|
||||
req.user = user
|
||||
next()
|
||||
}
|
||||
else {
|
||||
return res.status(500).json({
|
||||
success: false
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(next)
|
||||
}
|
||||
else {
|
||||
res.status(401).json({
|
||||
success: false
|
||||
, description: 'Requires Authentication'
|
||||
})
|
||||
}
|
||||
}
|
||||
109
lib/units/api/index.js
Normal file
109
lib/units/api/index.js
Normal file
@@ -0,0 +1,109 @@
|
||||
var http = require('http')
|
||||
var path = require('path')
|
||||
var events = require('events')
|
||||
|
||||
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')
|
||||
var wireutil = require('../../wire/util')
|
||||
|
||||
module.exports = function(options) {
|
||||
var log = logger.createLogger('api')
|
||||
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) {
|
||||
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()
|
||||
})
|
||||
|
||||
// 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
|
||||
}
|
||||
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
|
||||
})
|
||||
|
||||
req.options = reqOptions
|
||||
next()
|
||||
})
|
||||
|
||||
// TODO: Remove this once frontend is stateless
|
||||
app.use(cookieSession({
|
||||
name: options.ssid
|
||||
, 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)
|
||||
}
|
||||
327
lib/units/api/swagger/api_v1.yaml
Normal file
327
lib/units/api/swagger/api_v1.yaml
Normal file
@@ -0,0 +1,327 @@
|
||||
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:
|
||||
x-swagger-router-controller: 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:
|
||||
x-swagger-router-controller: user
|
||||
get:
|
||||
summary: User Devices
|
||||
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.
|
||||
operationId: addUserDevice
|
||||
tags:
|
||||
- user
|
||||
parameters:
|
||||
- name: device
|
||||
in: body
|
||||
description: Device to add
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/AddUserDevicePayload"
|
||||
responses:
|
||||
"200":
|
||||
description: Add User Device Status
|
||||
default:
|
||||
description: Unexpected Error
|
||||
schema:
|
||||
$ref: "#/definitions/ErrorResponse"
|
||||
security:
|
||||
- accessTokenAuth: []
|
||||
/user/devices/{serial}:
|
||||
x-swagger-router-controller: user
|
||||
get:
|
||||
summary: User Device
|
||||
description: The devices 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: Device Information owned by user
|
||||
schema:
|
||||
$ref: "#/definitions/DeviceResponse"
|
||||
default:
|
||||
description: Unexpected Error
|
||||
schema:
|
||||
$ref: "#/definitions/ErrorResponse"
|
||||
security:
|
||||
- accessTokenAuth: []
|
||||
delete:
|
||||
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:
|
||||
- user
|
||||
parameters:
|
||||
- name: serial
|
||||
in: path
|
||||
description: Device Serial
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Delete User Device Status
|
||||
default:
|
||||
description: Unexpected Error
|
||||
schema:
|
||||
$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:
|
||||
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:
|
||||
"200":
|
||||
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:
|
||||
"200":
|
||||
description: Remote Disonnect User Device Request Status
|
||||
default:
|
||||
description: Unexpected Error
|
||||
schema:
|
||||
$ref: "#/definitions/ErrorResponse"
|
||||
security:
|
||||
- accessTokenAuth: []
|
||||
/user/accessTokens:
|
||||
x-swagger-router-controller: user
|
||||
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:
|
||||
x-swagger-router-controller: 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}:
|
||||
x-swagger-router-controller: devices
|
||||
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:
|
||||
x-swagger-pipe: swagger_raw
|
||||
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
|
||||
RemoteConnectUserDeviceResponse:
|
||||
required:
|
||||
- remoteConnectUrl
|
||||
- serial
|
||||
properties:
|
||||
remoteConnectUrl:
|
||||
type: string
|
||||
serial:
|
||||
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
|
||||
ErrorResponse:
|
||||
required:
|
||||
- message
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
securityDefinitions:
|
||||
accessTokenAuth:
|
||||
type: apiKey
|
||||
name: authorization
|
||||
in: header
|
||||
499
lib/units/api/swagger/api_v1_generated.json
Normal file
499
lib/units/api/swagger/api_v1_generated.json
Normal file
@@ -0,0 +1,499 @@
|
||||
{
|
||||
"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": "User Devices",
|
||||
"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.",
|
||||
"operationId": "addUserDevice",
|
||||
"tags": [
|
||||
"user"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "device",
|
||||
"in": "body",
|
||||
"description": "Device to add",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/AddUserDevicePayload"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Add User Device Status"
|
||||
},
|
||||
"default": {
|
||||
"description": "Unexpected Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"accessTokenAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/user/devices/{serial}": {
|
||||
"get": {
|
||||
"summary": "User Device",
|
||||
"description": "The devices 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": "Device Information owned by user",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/DeviceResponse"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "Unexpected Error",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"accessTokenAuth": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"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": [
|
||||
"user"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "serial",
|
||||
"in": "path",
|
||||
"description": "Device Serial",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Delete User Device 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": {
|
||||
"200": {
|
||||
"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": {
|
||||
"200": {
|
||||
"description": "Remote Disonnect User Device Request Status"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RemoteConnectUserDeviceResponse": {
|
||||
"required": [
|
||||
"remoteConnectUrl",
|
||||
"serial"
|
||||
],
|
||||
"properties": {
|
||||
"remoteConnectUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"serial": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ErrorResponse": {
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securityDefinitions": {
|
||||
"accessTokenAuth": {
|
||||
"type": "apiKey",
|
||||
"name": "authorization",
|
||||
"in": "header"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
@@ -124,105 +121,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) {
|
||||
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/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) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -142,6 +142,16 @@ module.exports = syrup.serial()
|
||||
channel
|
||||
, reply.okay(url)
|
||||
])
|
||||
|
||||
// Update DB
|
||||
push.send([
|
||||
channel
|
||||
, wireutil.envelope(new wire.ConnectStartedMessage(
|
||||
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)
|
||||
@@ -159,6 +169,14 @@ module.exports = syrup.serial()
|
||||
channel
|
||||
, reply.okay()
|
||||
])
|
||||
// Update DB
|
||||
push.send([
|
||||
channel
|
||||
, wireutil.envelope(new wire.ConnectStoppedMessage(
|
||||
options.serial
|
||||
))
|
||||
])
|
||||
log.important('Remote Connect Stopped for device "%s"', options.serial)
|
||||
})
|
||||
.catch(function(err) {
|
||||
log.error('Failed to stop connect service', err.stack)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -165,6 +165,14 @@ 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)
|
||||
appDealer.send([channel, data])
|
||||
|
||||
@@ -54,10 +54,22 @@ 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) {
|
||||
// No-op
|
||||
}
|
||||
else {
|
||||
device.remoteConnect = false
|
||||
device.remoteConnectUrl = 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
|
||||
}
|
||||
|
||||
20
lib/util/deviceutil.js
Normal file
20
lib/util/deviceutil.js
Normal file
@@ -0,0 +1,20 @@
|
||||
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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "stf",
|
||||
"version": "1.2.0",
|
||||
"version": "2.0.0",
|
||||
"description": "Smartphone Test Farm",
|
||||
"keywords": [
|
||||
"adb",
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
@@ -181,7 +181,7 @@ module.exports = function DeviceServiceFactory($http, socket, EnhanceDeviceServi
|
||||
, digest: true
|
||||
})
|
||||
|
||||
oboe('/app/api/v1/group')
|
||||
oboe('/api/v1/user/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
|
||||
})
|
||||
|
||||
@@ -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/user/accessTokens')
|
||||
}
|
||||
|
||||
AccessTokenService.generateAccessToken = function(title) {
|
||||
|
||||
Reference in New Issue
Block a user