1
0
Fork 0
mirror of https://github.com/openstf/stf synced 2025-10-04 10:19:30 +02:00

Merge branch 'master' of https://github.com/openstf/stf into s3insteadoftmp

This commit is contained in:
tchibana 2015-10-20 01:34:05 +09:00
commit bbbd8f688c
180 changed files with 5434 additions and 1532 deletions

14
.eslintrc Normal file
View file

@ -0,0 +1,14 @@
{
"env": {
"node": true
},
"rules": {
"comma-style": [2, "first"],
"no-extra-semi": 2,
"no-underscore-dangle": 0,
"quotes": [2, "single"],
"semi": [2, "never"],
"space-before-blocks": [2, "always"],
"strict": [0, "function"]
}
}

View file

@ -2,6 +2,7 @@
.DS_Store .DS_Store
/*.tgz /*.tgz
/.bowerrc /.bowerrc
/.dockerignore
/.editorconfig /.editorconfig
/.env /.env
/.gitignore /.gitignore
@ -9,11 +10,12 @@
/.jscsrc /.jscsrc
/.npmignore /.npmignore
/.npmrc /.npmrc
/.travis.yml
/docker
/Dockerfile /Dockerfile
/bower.json /bower.json
/component.json /component.json
/gulpfile.js /gulpfile.js
/node_modules/
/npm-debug.log /npm-debug.log
/res/bower_components/ /res/bower_components/
/res/test/ /res/test/

View file

@ -1,24 +1,61 @@
language: node_js language: cpp
os:
- linux
- osx
sudo: false sudo: false
node_js:
- "0.12"
- "0.10"
- "iojs"
addons: addons:
apt: apt:
sources:
- ubuntu-toolchain-r-test
packages: packages:
- libzmq3-dev - libzmq3-dev
- libprotobuf-dev - libprotobuf-dev
- graphicsmagick - graphicsmagick
- rethinkdb - rethinkdb
script: - g++-4.9
- gulp build - yasm
env:
matrix:
- NODE_VERSION=0.12
- NODE_VERSION=4
matrix:
allow_failures:
- os: osx
fast_finish: true
before_install:
- rm -rf ~/.nvm && git clone --depth 1 https://github.com/creationix/nvm.git ~/.nvm
- source ~/.nvm/nvm.sh
- nvm install $NODE_VERSION
- node --version
- npm --version
- if [ "${TRAVIS_OS_NAME}" == "linux" ]; then export CXX=g++-4.9; fi
- if [ "${TRAVIS_OS_NAME}" == "osx" ]; then brew install rethinkdb graphicsmagick zeromq protobuf yasm pkg-config; fi
install:
- npm install
- export PATH=$PWD/node_modules/.bin:$PATH
before_script: before_script:
- npm install -g bower # - rethinkdb --daemon
- bower install
script:
- npm test
# - ./bin/stf local
- gulp build
after_script:
# - killall rethinkdb
cache: cache:
directories: directories:
- node_modules - node_modules
- res/bower_components - res/bower_components
notifications: notifications:
slack: openstf:qu01BtEgttJOrGGsRxKBJwki slack: openstf:qu01BtEgttJOrGGsRxKBJwki

View file

@ -1,11 +1,4 @@
FROM openstf/base:v1.0.1 FROM openstf/base:v1.0.6
# Add a user for the app.
RUN useradd --system \
--no-create-home \
--shell /usr/sbin/nologin \
--home-dir /app \
stf
# Sneak the stf executable into $PATH. # Sneak the stf executable into $PATH.
ENV PATH /app/bin:$PATH ENV PATH /app/bin:$PATH
@ -18,15 +11,31 @@ WORKDIR /app
EXPOSE 3000 EXPOSE 3000
# Copy app source. # Copy app source.
COPY . /app/ COPY . /tmp/build/
# Get the rest of the dependencies and build. # Give permissions to our build user.
RUN export PATH=/app/node_modules/.bin:$PATH && \ RUN mkdir -p /app && \
npm install && \ chown -R stf-build:stf-build /tmp/build /app
bower install --allow-root && \
gulp build
# Switch to weak user. # Switch over to the build user.
USER stf-build
# Run the build.
RUN set -x && \
cd /tmp/build && \
export PATH=$PWD/node_modules/.bin:$PATH && \
npm install --loglevel http && \
npm pack && \
tar xzf stf-*.tgz --strip-components 1 -C /app && \
bower cache clean && \
npm prune --production && \
mv node_modules /app && \
npm cache clean && \
rm -rf ~/.node-gyp && \
cd /app && \
rm -rf /tmp/*
# Switch to the app user.
USER stf USER stf
# Show help by default. # Show help by default.

View file

@ -17,7 +17,7 @@ It is currently being used at [CyberAgent](https://www.cyberagent.co.jp/en/) to
* OS support * OS support
- Android - Android
* Supports versions 2.3.3 (SDK level 10) to 5.1 (SDK level 22), plus Android M Developer Preview * Supports versions 2.3.3 (SDK level 10) to 5.1 (SDK level 22), plus Android M Developer Preview 3
* Supports Wear 5.1 (but not 5.0 due to missing permissions) * Supports Wear 5.1 (but not 5.0 due to missing permissions)
* Supports Fire OS, CyanogenMod, and other heavily Android based distributions * Supports Fire OS, CyanogenMod, and other heavily Android based distributions
* `root` is **not** required for any current functionality * `root` is **not** required for any current functionality
@ -43,6 +43,7 @@ It is currently being used at [CyberAgent](https://www.cyberagent.co.jp/en/) to
* Run any `adb` command locally, including shell access * Run any `adb` command locally, including shell access
* [Android Studio](http://developer.android.com/tools/studio/index.html) and other IDE support, debug your app while watching the device screen on your browser * [Android Studio](http://developer.android.com/tools/studio/index.html) and other IDE support, debug your app while watching the device screen on your browser
* Supports [Chrome remote debug tools](https://developer.chrome.com/devtools/docs/remote-debugging) * Supports [Chrome remote debug tools](https://developer.chrome.com/devtools/docs/remote-debugging)
- Experimental VNC support (work in progress)
* Manage your device inventory * Manage your device inventory
- See which devices are connected, offline/unavailable (indicating a weak USB connection), unauthorized or unplugged - See which devices are connected, offline/unavailable (indicating a weak USB connection), unauthorized or unplugged
- See who's using a device - See who's using a device
@ -65,14 +66,15 @@ As the product has evolved from an internal tool running in our internal network
* [GraphicsMagick](http://www.graphicsmagick.org/) (for resizing screenshots) * [GraphicsMagick](http://www.graphicsmagick.org/) (for resizing screenshots)
* [ZeroMQ](http://zeromq.org/) libraries installed * [ZeroMQ](http://zeromq.org/) libraries installed
* [Protocol Buffers](https://github.com/google/protobuf) libraries installed * [Protocol Buffers](https://github.com/google/protobuf) libraries installed
* [yasm](http://yasm.tortall.net/) installed (for compiling embedded [libjpeg-turbo](https://github.com/sorccu/node-jpeg-turbo))
* [pkg-config](http://www.freedesktop.org/wiki/Software/pkg-config/) so that Node.js can find the libraries * [pkg-config](http://www.freedesktop.org/wiki/Software/pkg-config/) so that Node.js can find the libraries
Note that you need these dependencies even if you've installed STF directly from [NPM](https://www.npmjs.com/), because they can't be included. Note that you need these dependencies even if you've installed STF directly from [NPM](https://www.npmjs.com/), because they can't be included in the package.
On OS X, you can use [homebrew](http://brew.sh/) to install most of the dependencies: On OS X, you can use [homebrew](http://brew.sh/) to install most of the dependencies:
```bash ```bash
brew install rethinkdb graphicsmagick zeromq protobuf pkg-config brew install rethinkdb graphicsmagick zeromq protobuf yasm pkg-config
``` ```
On Windows you're on your own. In theory you might be able to get STF installed via [Cygwin](https://www.cygwin.com/) or similar, but we've never tried. In principle we will not provide any Windows installation support, but please do send a documentation pull request if you figure out what to do. On Windows you're on your own. In theory you might be able to get STF installed via [Cygwin](https://www.cygwin.com/) or similar, but we've never tried. In principle we will not provide any Windows installation support, but please do send a documentation pull request if you figure out what to do.

View file

@ -2,47 +2,46 @@
"name": "stf", "name": "stf",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"angular": "1.4.3", "angular": "~1.4.7",
"angular-cookies": "1.4.3", "angular-cookies": "~1.4.7",
"angular-route": "1.4.3", "angular-route": "~1.4.7",
"angular-sanitize": "1.4.3", "angular-sanitize": "~1.4.7",
"angular-animate": "1.4.3", "angular-animate": "~1.4.7",
"angular-touch": "1.4.3", "angular-touch": "~1.4.7",
"lodash": "~3.10.1", "lodash": "~3.10.1",
"oboe": "~2.1.2", "oboe": "~2.1.2",
"ng-table": "~0.8.1", "ng-table": "~1.0.0-beta.7",
"angular-gettext": "~2.1.0", "angular-gettext": "~2.1.0",
"angular-ui-ace": "~0.2.3", "angular-ui-ace": "~0.2.3",
"angular-bootstrap": "~0.13.2",
"angular-dialog-service": "~5.2.6", "angular-dialog-service": "~5.2.6",
"ng-file-upload": "~2.0.5", "ng-file-upload": "~2.0.5",
"angular-growl-v2": "JanStevens/angular-growl-2#~0.7.3", "angular-growl-v2": "JanStevens/angular-growl-2#~0.7.3",
"underscore.string": "~3.1.1", "underscore.string": "~3.2.2",
"bootstrap": "~3.3.5", "bootstrap": "~3.3.5",
"font-lato-2-subset": "~0.4.0", "font-lato-2-subset": "~0.4.0",
"packery": "~1.4.2", "packery": "~1.4.3",
"draggabilly": "~1.2.4", "draggabilly": "~1.2.4",
"angular-elastic": "~2.5.0", "angular-elastic": "~2.5.1",
"angular-hotkeys": "chieffancypants/angular-hotkeys#~1.4.5", "angular-hotkeys": "chieffancypants/angular-hotkeys#~1.6.0",
"angular-borderlayout": "git://github.com/filearts/angular-borderlayout.git#7c9716aebd9260763f798561ca49d6fbfd4a5c67", "angular-borderlayout": "git://github.com/filearts/angular-borderlayout.git#7c9716aebd9260763f798561ca49d6fbfd4a5c67",
"angular-ui-bootstrap": "~0.13.2", "angular-ui-bootstrap": "~0.14.2",
"ng-context-menu": "~1.0.1", "ng-context-menu": "AdiDahan/ng-context-menu#~1.0.5",
"components-font-awesome": "~4.4.0", "components-font-awesome": "~4.4.0",
"epoch": "~0.6.0", "epoch": "~0.6.0",
"ng-epoch": "~1.0.7", "ng-epoch": "~1.0.7",
"eventEmitter": "~4.2.11", "eventEmitter": "~4.3.0",
"angular-ladda": "~0.3.1", "angular-ladda": "~0.3.1",
"d3": "~3.5.6", "d3": "~3.5.6",
"spin.js": "~2.3.2" "spin.js": "~2.3.2"
}, },
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"angular-mocks": "1.4.3" "angular-mocks": "~1.4.7"
}, },
"resolutions": { "resolutions": {
"angular": "1.4.3", "angular": "~1.4.7",
"d3": "~3.5.5", "d3": "~3.5.5",
"spin.js": "~2.3.2", "spin.js": "~2.3.2",
"angular-bootstrap": "~0.13.2" "eventEmitter": "~4.3.0"
} }
} }

View file

@ -56,6 +56,12 @@ The app role can contain any of the following units. You may distribute them as
* [stf-triproxy-dev.service](#stf-triproxy-devservice) * [stf-triproxy-dev.service](#stf-triproxy-devservice)
* [stf-websocket@.service](#stf-websocketservice) * [stf-websocket@.service](#stf-websocketservice)
### Database role
The database role requires the following units, UNLESS you already have a working RethinkDB server/cluster running somewhere. In that case you simply will not have this role, and should point your [rethinkdb-proxy-28015.service](#rethinkdb-proxy-28015service) to that server instead.
* [rethinkdb.service](#rethinkdbservice)
### Proxy role ### Proxy role
The proxy role ties all HTTP-based units together behind a common reverse proxy. See [nginx configuration](#nginx-configuration) for more information. The proxy role ties all HTTP-based units together behind a common reverse proxy. See [nginx configuration](#nginx-configuration) for more information.
@ -91,13 +97,29 @@ ExecStart=/usr/bin/docker run --rm \
ExecStop=-/usr/bin/docker stop -t 2 %p ExecStop=-/usr/bin/docker stop -t 2 %p
``` ```
### `rethinkdb-proxy-28015.service` ### `rethinkdb.service`
You need a single instance of the `rethinkdb-proxy-28015.service` unit on each host where you have another unit that needs to access the database. Having a local proxy simplifies configuration for other units and allows the `AUTHKEY` to be specified only once. As mentioned before, you only need this unit if you do not have an existing RethinkDB cluster. This configuration is provided as an example, and will get you going, but is not very robust or secure.
If you need to expand your RethinkDB cluster beyond one server you may encounter problems that you'll have to solve by yourself, we're not going to help with that. There are many ways to configure the unit, this is just one possibility! Note that if you end up not using `--net host`, you will then have to give `rethinkdb` the `--canonical-address` option with the server's real IP, and expose the necessary ports somehow.
You will also have to:
1. Modify the `--cache-size` as you please. It limits the amount of memory RethinkDB uses and is given in megabytes, but is not an absolute limit! Real usage can be slightly higher.
2. Update the version number in `rethinkdb:2.1.1` for the latest release. We don't use `rethinkdb:latest` here because then you might occasionally have to manually rebuild your indexes after an update and not even realize it, bringing the whole system effectively down.
3. The `AUTHKEY` environment variable is only for convenience when linking. So, the first time you set things up, you will have to access http://DB_SERVER_IP:8080 after starting the unit and run the following command:
```javascript
r.db('rethinkdb').table('cluster_config').get('auth').update({auth_key: 'newkey'})
```
More information can be found [here](https://rethinkdb.com/docs/security/). You will then need to replace `YOUR_RETHINKDB_AUTH_KEY_HERE_IF_ANY` in the the rest of the units with the real authentication key.
Here's the unit configuration itself.
```ini ```ini
[Unit] [Unit]
Description=RethinkDB proxy/28015 Description=RethinkDB
After=docker.service After=docker.service
Requires=docker.service Requires=docker.service
@ -105,7 +127,39 @@ Requires=docker.service
EnvironmentFile=/etc/environment EnvironmentFile=/etc/environment
TimeoutStartSec=0 TimeoutStartSec=0
Restart=always Restart=always
ExecStartPre=/usr/bin/docker pull ctlc/ambassador:latest ExecStartPre=/usr/bin/docker pull rethinkdb:2.1.1
ExecStartPre=-/usr/bin/docker kill %p
ExecStartPre=-/usr/bin/docker rm %p
ExecStartPre=/usr/bin/mkdir -p /srv/rethinkdb
ExecStartPre=/usr/bin/chattr -R +C /srv/rethinkdb
ExecStart=/usr/bin/docker run --rm \
--name %p \
-v /srv/rethinkdb:/data \
-e "AUTHKEY=YOUR_RETHINKDB_AUTH_KEY_HERE_IF_ANY" \
--net host \
rethinkdb:2.1.1 \
rethinkdb --bind all \
--cache-size 8192
ExecStop=-/usr/bin/docker stop -t 10 %p
```
### `rethinkdb-proxy-28015.service`
You need a single instance of the `rethinkdb-proxy-28015.service` unit on each host where you have another unit that needs to access the database. Having a local proxy simplifies configuration for other units and allows the `AUTHKEY` to be specified only once.
Note that the `After` condition also specifies the [rethinkdb.service](#rethinkdbservice) unit just in case you're on a low budget and want to run the RethinkDB unit on the same server as the rest of the units, which by the way is NOT recommended at all.
```ini
[Unit]
Description=RethinkDB proxy/28015
After=docker.service rethinkdb.service
Requires=docker.service
[Service]
EnvironmentFile=/etc/environment
TimeoutStartSec=0
Restart=always
ExecStartPre=/usr/bin/docker pull openstf/ambassador:latest
ExecStartPre=-/usr/bin/docker kill %p ExecStartPre=-/usr/bin/docker kill %p
ExecStartPre=-/usr/bin/docker rm %p ExecStartPre=-/usr/bin/docker rm %p
ExecStart=/usr/bin/docker run --rm \ ExecStart=/usr/bin/docker run --rm \
@ -113,7 +167,7 @@ ExecStart=/usr/bin/docker run --rm \
-e "AUTHKEY=YOUR_RETHINKDB_AUTH_KEY_HERE_IF_ANY" \ -e "AUTHKEY=YOUR_RETHINKDB_AUTH_KEY_HERE_IF_ANY" \
-p 28015 \ -p 28015 \
-e RETHINKDB_PORT_28015_TCP=tcp://rethinkdb.stf.example.org:28015 \ -e RETHINKDB_PORT_28015_TCP=tcp://rethinkdb.stf.example.org:28015 \
ctlc/ambassador:latest openstf/ambassador:latest
ExecStop=-/usr/bin/docker stop -t 10 %p ExecStop=-/usr/bin/docker stop -t 10 %p
``` ```
@ -146,7 +200,7 @@ ExecStart=/usr/bin/docker run --rm \
--name %p-%i \ --name %p-%i \
--link rethinkdb-proxy-28015:rethinkdb \ --link rethinkdb-proxy-28015:rethinkdb \
-e "SECRET=YOUR_SESSION_SECRET_HERE" \ -e "SECRET=YOUR_SESSION_SECRET_HERE" \
-p 127.0.0.1:%i:3000 \ -p %i:3000 \
openstf/stf:latest \ openstf/stf:latest \
stf app --port 3000 \ stf app --port 3000 \
--auth-url https://stf.example.org/auth/mock/ \ --auth-url https://stf.example.org/auth/mock/ \
@ -160,7 +214,7 @@ You may have to change the `--auth-url` depending on which authentication method
You have multiple options here. STF currently provides authentication units for [OAuth 2.0](http://oauth.net/2/) and [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol), plus a mock implementation that simply asks for a name and an email address. You have multiple options here. STF currently provides authentication units for [OAuth 2.0](http://oauth.net/2/) and [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol), plus a mock implementation that simply asks for a name and an email address.
Since the other providers require quite a bit of configuration, we'll simply set up a mock auth unit here. If you'd rather use the real providers, see `stf auth-oauth2 --help` and `stf auth-ldap --help` for the required variables. Since the other providers require quite a bit of configuration, we'll simply set up a mock auth unit here. If you'd rather use the real providers, see `stf auth-oauth2 --help` and `stf auth-ldap --help` for the required variables. Note that if your OAuth 2 provider uses a self-signed cert, you may have to add `-e "NODE_TLS_REJECT_UNAUTHORIZED=0"` to the `docker run` command. Don't forget to end the line with `\`.
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-auth@3200.service` runs on port 3200). You can have multiple instances running on the same host by using different ports. 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-auth@3200.service` runs on port 3200). You can have multiple instances running on the same host by using different ports.
@ -180,7 +234,7 @@ ExecStartPre=-/usr/bin/docker rm %p-%i
ExecStart=/usr/bin/docker run --rm \ ExecStart=/usr/bin/docker run --rm \
--name %p-%i \ --name %p-%i \
-e "SECRET=YOUR_SESSION_SECRET_HERE" \ -e "SECRET=YOUR_SESSION_SECRET_HERE" \
-p 127.0.0.1:%i:3000 \ -p %i:3000 \
openstf/stf:latest \ openstf/stf:latest \
stf auth-mock --port 3000 \ stf auth-mock --port 3000 \
--app-url https://stf.example.org/ --app-url https://stf.example.org/
@ -259,6 +313,8 @@ This is a template unit, meaning that you'll need to start it with an instance i
Note that you cannot have more than one provider unit running on the same host, as they would compete over which one gets to control the devices. In the future we might add a negotiation protocol to allow for relatively seamless upgrades. Note that you cannot have more than one provider unit running on the same host, as they would compete over which one gets to control the devices. In the future we might add a negotiation protocol to allow for relatively seamless upgrades.
Furthermore, if you're using a self-signed cert, you may have to add `-e "NODE_TLS_REJECT_UNAUTHORIZED=0"` to the `docker run` command. Don't forget to end the line with `\`.
```ini ```ini
[Unit] [Unit]
Description=STF provider Description=STF provider
@ -293,7 +349,7 @@ ExecStop=-/usr/bin/docker stop -t 10 %p-%i
**Requires** the `rethinkdb-proxy-28015.service` unit on the same host. **Requires** the `rethinkdb-proxy-28015.service` unit on the same host.
The reaper unit receives heartbeat events from device workers, and marks lost devices as absent until a heartbeat is received again. The purpose of this unit is to ensure the integrity of the present/absent flag in the database, in case a provider shuts down unexpectedly or another unexpected failure occurs. It loads the current state from the database on startup and keeps keeps patching its internal view as events are routed to it. The reaper unit receives heartbeat events from device workers, and marks lost devices as absent until a heartbeat is received again. The purpose of this unit is to ensure the integrity of the present/absent flag in the database, in case a provider shuts down unexpectedly or another unexpected failure occurs. It loads the current state from the database on startup and keeps patching its internal view as events are routed to it.
Note that it doesn't make sense to have more than one reaper running at once, as they would just duplicate the events. Note that it doesn't make sense to have more than one reaper running at once, as they would just duplicate the events.
@ -327,6 +383,8 @@ The APK storage plugin loads raw blobs from the main storage unit and allows add
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-storage-plugin-apk@3300.service` runs on port 3300). You can have multiple instances running on the same host by using different ports. 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-storage-plugin-apk@3300.service` runs on port 3300). You can have multiple instances running on the same host by using different ports.
Furthermore, if you're using a self-signed cert, you may have to add `-e "NODE_TLS_REJECT_UNAUTHORIZED=0"` to the `docker run` command. Don't forget to end the line with `\`.
```ini ```ini
[Unit] [Unit]
Description=STF APK storage plugin Description=STF APK storage plugin
@ -342,7 +400,7 @@ ExecStartPre=-/usr/bin/docker kill %p-%i
ExecStartPre=-/usr/bin/docker rm %p-%i ExecStartPre=-/usr/bin/docker rm %p-%i
ExecStart=/usr/bin/docker run --rm \ ExecStart=/usr/bin/docker run --rm \
--name %p-%i \ --name %p-%i \
-p 127.0.0.1:%i:3000 \ -p %i:3000 \
openstf/stf:latest \ openstf/stf:latest \
stf storage-plugin-apk --port 3000 \ stf storage-plugin-apk --port 3000 \
--storage-url https://stf.example.org/ --storage-url https://stf.example.org/
@ -355,6 +413,8 @@ The image storage plugin loads raw blobs from the main storage unit and and allo
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-storage-plugin-image@3400.service` runs on port 3400). You can have multiple instances running on the same host by using different ports. 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-storage-plugin-image@3400.service` runs on port 3400). You can have multiple instances running on the same host by using different ports.
Furthermore, if you're using a self-signed cert, you may have to add `-e "NODE_TLS_REJECT_UNAUTHORIZED=0"` to the `docker run` command. Don't forget to end the line with `\`.
```ini ```ini
[Unit] [Unit]
Description=STF image storage plugin Description=STF image storage plugin
@ -370,7 +430,7 @@ ExecStartPre=-/usr/bin/docker kill %p-%i
ExecStartPre=-/usr/bin/docker rm %p-%i ExecStartPre=-/usr/bin/docker rm %p-%i
ExecStart=/usr/bin/docker run --rm \ ExecStart=/usr/bin/docker run --rm \
--name %p-%i \ --name %p-%i \
-p 127.0.0.1:%i:3000 \ -p %i:3000 \
openstf/stf:latest \ openstf/stf:latest \
stf storage-plugin-image --port 3000 \ stf storage-plugin-image --port 3000 \
--storage-url https://stf.example.org/ --storage-url https://stf.example.org/
@ -397,7 +457,7 @@ ExecStartPre=-/usr/bin/docker rm %p-%i
ExecStart=/usr/bin/docker run --rm \ ExecStart=/usr/bin/docker run --rm \
--name %p-%i \ --name %p-%i \
-v /mnt/storage:/data \ -v /mnt/storage:/data \
-p 127.0.0.1:%i:3000 \ -p %i:3000 \
openstf/stf:latest \ openstf/stf:latest \
stf storage-temp --port 3000 \ stf storage-temp --port 3000 \
--save-dir /data --save-dir /data
@ -476,6 +536,8 @@ The websocket unit provides the communication layer between client-side JavaScri
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-websocket@3600.service` runs on port 3600). You can have multiple instances running on the same host by using different ports. 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-websocket@3600.service` runs on port 3600). You can have multiple instances running on the same host by using different ports.
Furthermore, if you're using a self-signed cert, you may have to add `-e "NODE_TLS_REJECT_UNAUTHORIZED=0"` to the `docker run` command. Don't forget to end the line with `\`.
```ini ```ini
[Unit] [Unit]
Description=STF websocket Description=STF websocket
@ -493,7 +555,7 @@ ExecStart=/usr/bin/docker run --rm \
--name %p-%i \ --name %p-%i \
--link rethinkdb-proxy-28015:rethinkdb \ --link rethinkdb-proxy-28015:rethinkdb \
-e "SECRET=YOUR_SESSION_SECRET_HERE" \ -e "SECRET=YOUR_SESSION_SECRET_HERE" \
-p 127.0.0.1:%i:3000 \ -p %i:3000 \
openstf/stf:latest \ openstf/stf:latest \
stf websocket --port 3000 \ stf websocket --port 3000 \
--storage-url https://stf.example.org/ \ --storage-url https://stf.example.org/ \
@ -688,8 +750,8 @@ http {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
} }
location /auth/mock/ { location /auth/ {
proxy_pass http://stf_auth/auth/mock/; proxy_pass http://stf_auth/auth/;
} }
location /s/image/ { location /s/image/ {

17
doc/VNC.md Normal file
View file

@ -0,0 +1,17 @@
# VNC
## Implementation details
### Authentication
#### According to the spec
VNC authentication is very weak by default, and doesn't encrypt traffic in any way. It works by sending a random 16-byte challenge to the user, who then encrypts with his/her password and sends back the 16-byte result. The server then encrypts the challenge as well, and checks whether the result sent by the client matches the server's result. Passwords are required to be 8 characters long. Shorter passwords are padded with zeroes and longer passwords simply truncated. Both the server and the client have to know the password. There are no usernames.
#### The way we do it
Since the authentication is very weak anyway, we might as well exploit it. The problem with the spec method is that since there's no username, it's difficult to know *who* wants to connect to a device. The only place for any kind of information is the password, but without knowing the password we can't decrypt the challenge response to see the contents. While we could go through our whole user database encrypting the challenge with each user's password, that doesn't really scale in the long run, especially since we're interested in having per-device passwords as well (more on that later).
Instead, we send over a *static* challenge, e.g. 16 zeroes, every time. Then we simply identify the user by the returned challenge response itself, which is both unique and constant for each password. This makes the authentication more susceptible to eavesdropping since responses from previous sessions can be reused, but given the already weak nature of basic VNC authentication this shouldn't be a massive downgrade, and the app should be running inside an internal network anyway. For real security, all connections should be over a secure tunnel.
Furthermore, each password is only valid for a single device. This will enable interesting proxying and/or load balancing opportunities in the future as we should be able to expose every single device in the system via a single port if desired.

View file

@ -24,7 +24,8 @@ COPY . /app/
RUN export PATH=/app/node_modules/.bin:$PATH && \ RUN export PATH=/app/node_modules/.bin:$PATH && \
npm install && \ npm install && \
bower install --allow-root && \ bower install --allow-root && \
gulp build gulp build && \
npm prune --production
# Switch to weak user. # Switch to weak user.
USER stf USER stf

View file

@ -1,16 +1,16 @@
var path = require('path')
var gulp = require('gulp') var gulp = require('gulp')
var gutil = require('gulp-util') var gutil = require('gulp-util')
var jshint = require('gulp-jshint') var jshint = require('gulp-jshint')
var jsonlint = require('gulp-jsonlint') var jsonlint = require('gulp-jsonlint')
var standard = require('gulp-standard') var standard = require('gulp-standard')
var webpack = require('webpack') var webpack = require('webpack')
var ngAnnotatePlugin = require('ng-annotate-webpack-plugin')
var webpackConfig = require('./webpack.config').webpack var webpackConfig = require('./webpack.config').webpack
var webpackStatusConfig = require('./res/common/status/webpack.config') var webpackStatusConfig = require('./res/common/status/webpack.config')
var gettext = require('gulp-angular-gettext') var gettext = require('gulp-angular-gettext')
var jade = require('gulp-jade') var jade = require('gulp-jade')
var del = require('del') var del = require('del')
var runSequence = require('run-sequence').use(gulp)
//var protractor = require('gulp-protractor') //var protractor = require('gulp-protractor')
var protractor = require('./res/test/e2e/helpers/gulp-protractor-adv') var protractor = require('./res/test/e2e/helpers/gulp-protractor-adv')
var protractorConfig = './res/test/protractor.conf' var protractorConfig = './res/test/protractor.conf'
@ -22,30 +22,42 @@ var run = require('gulp-run')
gulp.task('jshint', function () { gulp.task('jshint', function () {
return gulp.src([ return gulp.src([
'lib/**/*.js', 'res/app/**/*.js', 'res/auth-ldap/**/*.js', 'lib/**/*.js'
'res/auth-mock/**/*.js', 'res/common/**/*.js', 'res/test/**/*.js', , 'res/app/**/*.js'
'*.js' , 'res/auth-ldap/**/*.js'
]) , 'res/auth-mock/**/*.js'
, 'res/common/**/*.js'
, 'res/test/**/*.js'
, '*.js'
])
.pipe(jshint()) .pipe(jshint())
.pipe(jshint.reporter('jshint-stylish')) .pipe(jshint.reporter('jshint-stylish'))
}) })
gulp.task('jsonlint', function () { gulp.task('jsonlint', function () {
return gulp.src([ return gulp.src([
'.jshintrc', 'res/.jshintrc', '.bowerrc', '.yo-rc.json', '*.json' '.jshintrc'
]) , 'res/.jshintrc'
, '.bowerrc'
, '.yo-rc.json'
, '*.json'
])
.pipe(jsonlint()) .pipe(jsonlint())
.pipe(jsonlint.reporter()) .pipe(jsonlint.reporter())
}) })
gulp.task('jscs', function () { gulp.task('jscs', function () {
return gulp.src([ return gulp.src([
'lib/**/*.js', 'res/app/**/*.js', 'res/auth-ldap/**/*.js', 'lib/**/*.js'
'res/auth-mock/**/*.js', 'res/common/**/*.js', 'res/test/**/*.js', , 'res/app/**/*.js'
'*.js' , 'res/auth-ldap/**/*.js'
]) , 'res/auth-mock/**/*.js'
, 'res/common/**/*.js'
, 'res/test/**/*.js'
, '*.js'
])
.pipe(jscs()) .pipe(jscs())
}); })
gulp.task('standard', function () { gulp.task('standard', function () {
// Check res/app for now // Check res/app for now
@ -58,27 +70,23 @@ gulp.task('standard', function () {
gulp.task('lint', ['jshint', 'jsonlint']) gulp.task('lint', ['jshint', 'jsonlint'])
gulp.task('test', ['lint', 'run:checkversion']) gulp.task('test', ['lint', 'run:checkversion'])
gulp.task('build', ['clean', 'webpack:build'])
gulp.task('build', function (cb) {
runSequence('clean', 'webpack:build', cb)
})
gulp.task('run:checkversion', function () { gulp.task('run:checkversion', function () {
gutil.log('Checking STF version...') gutil.log('Checking STF version...')
return run('./bin/stf -V').exec() return run('./bin/stf -V').exec()
}) })
gulp.task('karma_ci', function (done) { gulp.task('karma_ci', function (done) {
karma.start({ karma.start({
configFile: __dirname + karmaConfig, configFile: path.join(__dirname, karmaConfig)
singleRun: true , singleRun: true
}, done) }, done)
}) })
gulp.task('karma', function (done) { gulp.task('karma', function (done) {
karma.start({ karma.start({
configFile: __dirname + karmaConfig configFile: path.join(__dirname, karmaConfig)
}, done) }, done)
}) })
@ -88,32 +96,37 @@ if (gutil.env.multi) {
protractorConfig = './res/test/protractor-appium.conf' protractorConfig = './res/test/protractor-appium.conf'
} }
gulp.task('webdriver-update', protractor.webdriver_update) gulp.task('webdriver-update', protractor.webdriverUpdate)
gulp.task('webdriver-standalone', protractor.webdriver_standalone) gulp.task('webdriver-standalone', protractor.webdriverStandalone)
gulp.task('protractor-explorer', function (callback) { gulp.task('protractor-explorer', function (callback) {
protractor.protractor_explorer({ protractor.protractorExplorer({
url: require(protractorConfig).config.baseUrl url: require(protractorConfig).config.baseUrl
}, callback) }, callback)
}) })
gulp.task('protractor', ['webdriver-update'], function (callback) { gulp.task('protractor', ['webdriver-update'], function (callback) {
gulp.src(["./res/test/e2e/**/*.js"]) gulp.src(['./res/test/e2e/**/*.js'])
.pipe(protractor.protractor({ .pipe(protractor.protractor({
configFile: protractorConfig, configFile: protractorConfig
debug: gutil.env.debug, , debug: gutil.env.debug
suite: gutil.env.suite , suite: gutil.env.suite
})) }))
.on('error', function (e) { .on('error', function (e) {
console.log(e) console.log(e)
}).on('end', callback) })
.on('end', callback)
}) })
// For piping strings // For piping strings
function fromString(filename, string) { function fromString(filename, string) {
var src = stream.Readable({objectMode: true}) /* eslint no-underscore-dangle: 0 */
var src = new stream.Readable({objectMode: true})
src._read = function () { src._read = function () {
this.push(new gutil.File({ this.push(new gutil.File({
cwd: '', base: '', path: filename, contents: new Buffer(string) cwd: ''
, base: ''
, path: filename
, contents: new Buffer(string)
})) }))
this.push(null) this.push(null)
} }
@ -122,20 +135,14 @@ function fromString(filename, string) {
// For production // For production
gulp.task("webpack:build", function (callback) { gulp.task('webpack:build', function (callback) {
var myConfig = Object.create(webpackConfig) var myConfig = Object.create(webpackConfig)
myConfig.plugins = myConfig.plugins.concat( myConfig.plugins = myConfig.plugins.concat(
new webpack.DefinePlugin({ new webpack.DefinePlugin({
"process.env": { 'process.env': {
"NODE_ENV": JSON.stringify('production') 'NODE_ENV': JSON.stringify('production')
} }
}) })
//new webpack.optimize.DedupePlugin(),
//new ngAnnotatePlugin({
// add: true,
//})
// TODO: mangle when ngmin works
//new webpack.optimize.UglifyJsPlugin({mangle: false})
) )
myConfig.devtool = false myConfig.devtool = false
@ -144,7 +151,7 @@ gulp.task("webpack:build", function (callback) {
throw new gutil.PluginError('webpack:build', err) throw new gutil.PluginError('webpack:build', err)
} }
gutil.log("[webpack:build]", stats.toString({ gutil.log('[webpack:build]', stats.toString({
colors: true colors: true
})) }))
@ -157,17 +164,14 @@ gulp.task("webpack:build", function (callback) {
}) })
}) })
gulp.task("webpack:others", function (callback) { gulp.task('webpack:others', function (callback) {
var myConfig = Object.create(webpackStatusConfig) var myConfig = Object.create(webpackStatusConfig)
myConfig.plugins = myConfig.plugins.concat( myConfig.plugins = myConfig.plugins.concat(
new webpack.DefinePlugin({ new webpack.DefinePlugin({
"process.env": { 'process.env': {
"NODE_ENV": JSON.stringify('production') 'NODE_ENV': JSON.stringify('production')
} }
}), })
new webpack.optimize.DedupePlugin()
// new ngminPlugin(),
// new webpack.optimize.UglifyJsPlugin({mangle: false})
) )
myConfig.devtool = false myConfig.devtool = false
@ -176,19 +180,25 @@ gulp.task("webpack:others", function (callback) {
throw new gutil.PluginError('webpack:others', err) throw new gutil.PluginError('webpack:others', err)
} }
gutil.log("[webpack:others]", stats.toString({ gutil.log('[webpack:others]', stats.toString({
colors: true colors: true
})) }))
callback() callback()
}) })
}) })
gulp.task('translate', ['translate:compile']) gulp.task('translate', [
'translate:extract'
, 'translate:push'
, 'translate:pull'
, 'translate:compile'
])
gulp.task('jade', function (cb) { gulp.task('jade', function () {
return gulp.src([ return gulp.src([
'./res/**/*.jade', '!./res/bower_components/**' './res/**/*.jade'
]) , '!./res/bower_components/**'
])
.pipe(jade({ .pipe(jade({
locals: { locals: {
// So res/views/docs.jade doesn't complain // So res/views/docs.jade doesn't complain
@ -201,16 +211,18 @@ gulp.task('jade', function (cb) {
.pipe(gulp.dest('./tmp/html/')) .pipe(gulp.dest('./tmp/html/'))
}) })
gulp.task('translate:extract', ['jade'], function (cb) { gulp.task('translate:extract', ['jade'], function () {
return gulp.src([ return gulp.src([
'./tmp/html/**/*.html', './res/**/*.js', '!./res/bower_components/**', './tmp/html/**/*.html'
'!./res/build/**' , './res/**/*.js'
]) , '!./res/bower_components/**'
, '!./res/build/**'
])
.pipe(gettext.extract('stf.pot')) .pipe(gettext.extract('stf.pot'))
.pipe(gulp.dest('./res/common/lang/po/')) .pipe(gulp.dest('./res/common/lang/po/'))
}) })
gulp.task('translate:compile', ['translate:pull'], function (cb) { gulp.task('translate:compile', function () {
return gulp.src('./res/common/lang/po/**/*.po') return gulp.src('./res/common/lang/po/**/*.po')
.pipe(gettext.compile({ .pipe(gettext.compile({
format: 'json' format: 'json'
@ -218,21 +230,16 @@ gulp.task('translate:compile', ['translate:pull'], function (cb) {
.pipe(gulp.dest('./res/common/lang/translations/')) .pipe(gulp.dest('./res/common/lang/translations/'))
}) })
gulp.task('translate:push', ['translate:extract'], function () { gulp.task('translate:push', function () {
gutil.log('Pushing translation source to Transifex...') gutil.log('Pushing translation source to Transifex...')
return run('tx push -s').exec() return run('tx push -s').exec()
}) })
gulp.task('translate:pull', ['translate:push'], function () { gulp.task('translate:pull', function () {
gutil.log('Pulling translations from Transifex...') gutil.log('Pulling translations from Transifex...')
return run('tx pull').exec() return run('tx pull').exec()
}) })
gulp.task('clean', function (cb) { gulp.task('clean', function (cb) {
del(['./tmp', './res/build'], cb) del(['./tmp', './res/build'], cb)
}) })

View file

@ -15,7 +15,7 @@ program
.version(pkg.version) .version(pkg.version)
program program
.command('provider [serial..]') .command('provider [serial...]')
.description('start provider') .description('start provider')
.option('-s, --connect-sub <endpoint>' .option('-s, --connect-sub <endpoint>'
, 'sub endpoint' , 'sub endpoint'
@ -68,12 +68,15 @@ program
, 'adb connect URL pattern' , 'adb connect URL pattern'
, String , String
, '${publicIp}:${publicPort}') , '${publicIp}:${publicPort}')
.option('--vnc-initial-size <size>'
, 'initial VNC size'
, cliutil.size
, [600, 800])
.option('--mute-master' .option('--mute-master'
, 'whether to mute master volume when devices are being used') , 'whether to mute master volume when devices are being used')
.action(function() { .option('--lock-rotation'
var serials = cliutil.allUnknownArgs(arguments) , 'whether to lock rotation when devices are being used')
, options = cliutil.lastArg(arguments) .action(function(serials, options) {
if (!options.connectSub) { if (!options.connectSub) {
this.missingArgument('--connect-sub') this.missingArgument('--connect-sub')
} }
@ -101,6 +104,7 @@ program
, '--connect-push', options.connectPush.join(',') , '--connect-push', options.connectPush.join(',')
, '--screen-port', ports.shift() , '--screen-port', ports.shift()
, '--connect-port', ports.shift() , '--connect-port', ports.shift()
, '--vnc-port', ports.shift()
, '--public-ip', options.publicIp , '--public-ip', options.publicIp
, '--group-timeout', options.groupTimeout , '--group-timeout', options.groupTimeout
, '--storage-url', options.storageUrl , '--storage-url', options.storageUrl
@ -109,8 +113,10 @@ program
, '--screen-ws-url-pattern', options.screenWsUrlPattern , '--screen-ws-url-pattern', options.screenWsUrlPattern
, '--connect-url-pattern', options.connectUrlPattern , '--connect-url-pattern', options.connectUrlPattern
, '--heartbeat-interval', options.heartbeatInterval , '--heartbeat-interval', options.heartbeatInterval
, '--vnc-initial-size', options.vncInitialSize.join('x')
] ]
.concat(options.muteMaster ? ['--mute-master'] : [])) .concat(options.muteMaster ? ['--mute-master'] : [])
.concat(options.lockRotation ? ['--lock-rotation'] : []))
} }
, endpoints: { , endpoints: {
sub: options.connectSub sub: options.connectSub
@ -139,6 +145,13 @@ program
.option('--connect-port <port>' .option('--connect-port <port>'
, 'port allocated to adb connect' , 'port allocated to adb connect'
, Number) , Number)
.option('--vnc-port <port>'
, 'port allocated to vnc'
, Number)
.option('--vnc-initial-size <size>'
, 'initial VNC size'
, cliutil.size
, [600, 800])
.option('--connect-url-pattern <pattern>' .option('--connect-url-pattern <pattern>'
, 'adb connect URL pattern' , 'adb connect URL pattern'
, String , String
@ -172,6 +185,8 @@ program
, 10000) , 10000)
.option('--mute-master' .option('--mute-master'
, 'whether to mute master volume when devices are being used') , 'whether to mute master volume when devices are being used')
.option('--lock-rotation'
, 'whether to lock rotation when devices are being used')
.action(function(serial, options) { .action(function(serial, options) {
if (!options.connectSub) { if (!options.connectSub) {
this.missingArgument('--connect-sub') this.missingArgument('--connect-sub')
@ -188,6 +203,9 @@ program
if (!options.connectPort) { if (!options.connectPort) {
this.missingArgument('--connect-port') this.missingArgument('--connect-port')
} }
if (!options.vncPort) {
this.missingArgument('--vnc-port')
}
if (!options.storageUrl) { if (!options.storageUrl) {
this.missingArgument('--storage-url') this.missingArgument('--storage-url')
} }
@ -208,8 +226,11 @@ program
, screenPort: options.screenPort , screenPort: options.screenPort
, connectUrlPattern: options.connectUrlPattern , connectUrlPattern: options.connectUrlPattern
, connectPort: options.connectPort , connectPort: options.connectPort
, vncPort: options.vncPort
, vncInitialSize: options.vncInitialSize
, heartbeatInterval: options.heartbeatInterval , heartbeatInterval: options.heartbeatInterval
, muteMaster: options.muteMaster , muteMaster: options.muteMaster
, lockRotation: options.lockRotation
}) })
}) })
@ -866,7 +887,7 @@ program
}) })
program program
.command('local [serial..]') .command('local [serial...]')
.description('start everything locally') .description('start everything locally')
.option('--bind-app-pub <endpoint>' .option('--bind-app-pub <endpoint>'
, 'app pub endpoint' , 'app pub endpoint'
@ -976,6 +997,10 @@ program
.option('--user-profile-url <url>' .option('--user-profile-url <url>'
, 'URL to external user profile page' , 'URL to external user profile page'
, String) , String)
.option('--vnc-initial-size <size>'
, 'initial VNC size'
, cliutil.size
, [600, 800])
.option('--mute-master' .option('--mute-master'
, 'whether to mute master volume when devices are being used') , 'whether to mute master volume when devices are being used')
.option('--use-s3' .option('--use-s3'
@ -991,10 +1016,10 @@ program
, 's3 endpoint' , 's3 endpoint'
, String , String
, 's3-ap-northeast-1.amazonaws.com') , 's3-ap-northeast-1.amazonaws.com')
.action(function() { .option('--lock-rotation'
, 'whether to lock rotation when devices are being used')
.action(function(serials, options) {
var log = logger.createLogger('cli:local') var log = logger.createLogger('cli:local')
, args = arguments
, options = cliutil.lastArg(args)
, procutil = require('./util/procutil') , procutil = require('./util/procutil')
// Each forked process waits for signals to stop, and so we run over the // Each forked process waits for signals to stop, and so we run over the
@ -1054,10 +1079,12 @@ program
, util.format('http://localhost:%d/', options.poorxyPort) , util.format('http://localhost:%d/', options.poorxyPort)
, '--adb-host', options.adbHost , '--adb-host', options.adbHost
, '--adb-port', options.adbPort , '--adb-port', options.adbPort
, '--vnc-initial-size', options.vncInitialSize.join('x')
] ]
.concat(options.allowRemote ? ['--allow-remote'] : []) .concat(options.allowRemote ? ['--allow-remote'] : [])
.concat(options.muteMaster ? ['--mute-master'] : []) .concat(options.muteMaster ? ['--mute-master'] : [])
.concat(cliutil.allUnknownArgs(args))) .concat(options.lockRotation ? ['--lock-rotation'] : [])
.concat(serials))
// auth // auth
, procutil.fork(__filename, [ , procutil.fork(__filename, [

View file

@ -100,6 +100,27 @@ dbapi.lookupUserByAdbFingerprint = function(fingerprint) {
}) })
} }
dbapi.lookupUserByVncAuthResponse = function(response, serial) {
return db.run(r.table('vncauth').getAll([response, serial], {
index: 'responsePerDevice'
})
.eqJoin('userId', r.table('users'))('right')
.pluck('email', 'name', 'group'))
.then(function(cursor) {
return cursor.toArray()
})
.then(function(groups) {
switch (groups.length) {
case 1:
return groups[0]
case 0:
return null
default:
throw new Error('Found multiple users with the same VNC response')
}
})
}
dbapi.loadGroup = function(email) { dbapi.loadGroup = function(email) {
return db.run(r.table('devices').getAll(email, { return db.run(r.table('devices').getAll(email, {
index: 'owner' index: 'owner'

View file

@ -14,6 +14,17 @@ module.exports = {
} }
} }
} }
, vncauth: {
primaryKey: 'password'
, indexes: {
response: null
, responsePerDevice: {
indexFunction: function(row) {
return [row('response'), row('deviceId')]
}
}
}
}
, devices: { , devices: {
primaryKey: 'serial' primaryKey: 'serial'
, indexes: { , indexes: {

View file

@ -5,7 +5,7 @@ var webpack = require('webpack')
var mime = require('mime') var mime = require('mime')
var Promise = require('bluebird') var Promise = require('bluebird')
var _ = require('lodash') var _ = require('lodash')
var MemoryFileSystem = require('webpack/node_modules/memory-fs') var MemoryFileSystem = require('memory-fs')
var logger = require('../../../util/logger') var logger = require('../../../util/logger')
var lifecycle = require('../../../util/lifecycle') var lifecycle = require('../../../util/lifecycle')

View file

@ -20,6 +20,7 @@ module.exports = function(options) {
.dependency(require('./plugins/solo')) .dependency(require('./plugins/solo'))
.dependency(require('./plugins/screen/stream')) .dependency(require('./plugins/screen/stream'))
.dependency(require('./plugins/screen/capture')) .dependency(require('./plugins/screen/capture'))
.dependency(require('./plugins/vnc'))
.dependency(require('./plugins/service')) .dependency(require('./plugins/service'))
.dependency(require('./plugins/browser')) .dependency(require('./plugins/browser'))
.dependency(require('./plugins/store')) .dependency(require('./plugins/store'))
@ -38,6 +39,7 @@ module.exports = function(options) {
.dependency(require('./plugins/ringer')) .dependency(require('./plugins/ringer'))
.dependency(require('./plugins/wifi')) .dependency(require('./plugins/wifi'))
.dependency(require('./plugins/sd')) .dependency(require('./plugins/sd'))
.dependency(require('./plugins/filesystem'))
.define(function(options, heartbeat, solo) { .define(function(options, heartbeat, solo) {
if (process.send) { if (process.send) {
// Only if we have a parent process // Only if we have a parent process

View file

@ -0,0 +1,74 @@
var syrup = require('stf-syrup')
var path = require('path')
var logger = require('../../../util/logger')
var wire = require('../../../wire')
var wireutil = require('../../../wire/util')
module.exports = syrup.serial()
.dependency(require('../support/adb'))
.dependency(require('../support/router'))
.dependency(require('../support/push'))
.dependency(require('../support/storage'))
.define(function(options, adb, router, push, storage) {
var log = logger.createLogger('device:plugins:filesystem')
var plugin = Object.create(null)
plugin.retrieve = function(file) {
log.info('Retrieving file "%s"', file)
return adb.stat(options.serial, file)
.then(function(stats) {
return adb.pull(options.serial, file)
.then(function(transfer) {
// We may have add new storage plugins for various file types
// in the future, and add proper detection for the mimetype.
// But for now, let's just use application/octet-stream for
// everything like it's 2001.
return storage.store('blob', transfer, {
filename: path.basename(file)
, contentType: 'application/octet-stream'
, knownLength: stats.size
})
})
})
}
router.on(wire.FileSystemGetMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
plugin.retrieve(message.file)
.then(function(file) {
push.send([
channel
, reply.okay('success', file)
])
})
.catch(function(err) {
log.warn('Unable to retrieve "%s"', message.file, err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
router.on(wire.FileSystemListMessage, function(channel, message) {
var reply = wireutil.reply(options.serial)
adb.readdir(options.serial, message.dir)
.then(function(files) {
push.send([
channel
, reply.okay('success', files)
])
})
.catch(function(err) {
log.warn('Unable to list directory "%s"', message.dir, err.stack)
push.send([
channel
, reply.fail(err.message)
])
})
})
return plugin
})

View file

@ -104,6 +104,7 @@ module.exports = syrup.serial()
plugin.on('leave', function() { plugin.on('leave', function() {
service.pressKey('home') service.pressKey('home')
service.thawRotation()
service.releaseWakeLock() service.releaseWakeLock()
}) })

View file

@ -25,7 +25,6 @@ module.exports = syrup.serial()
.dependency(require('./options')) .dependency(require('./options'))
.define(function(options, adb, minicap, display, screenOptions) { .define(function(options, adb, minicap, display, screenOptions) {
var log = logger.createLogger('device:plugins:screen:stream') var log = logger.createLogger('device:plugins:screen:stream')
var plugin = Object.create(null)
function FrameProducer(config) { function FrameProducer(config) {
EventEmitter.call(this) EventEmitter.call(this)
@ -443,9 +442,9 @@ module.exports = syrup.serial()
return createServer() return createServer()
.then(function(wss) { .then(function(wss) {
var broadcastSet = new BroadcastSet()
var frameProducer = new FrameProducer( var frameProducer = new FrameProducer(
new FrameConfig(display.properties, display.properties)) new FrameConfig(display.properties, display.properties))
var broadcastSet = frameProducer.broadcastSet = new BroadcastSet()
broadcastSet.on('nonempty', function() { broadcastSet.on('nonempty', function() {
frameProducer.start() frameProducer.start()
@ -455,37 +454,26 @@ module.exports = syrup.serial()
frameProducer.stop() frameProducer.stop()
}) })
broadcastSet.on('insert', function(id) {
// If two clients join a session in the middle, one of them
// may not release the initial size because the projection
// doesn't necessarily change, and the producer doesn't Getting
// restarted. Therefore we have to call onStart() manually
// if the producer is already up and running.
switch (frameProducer.runningState) {
case FrameProducer.STATE_STARTED:
broadcastSet.get(id).onStart(frameProducer)
break
}
})
display.on('rotationChange', function(newRotation) { display.on('rotationChange', function(newRotation) {
frameProducer.updateRotation(newRotation) frameProducer.updateRotation(newRotation)
}) })
frameProducer.on('start', function() { frameProducer.on('start', function() {
var message = util.format( broadcastSet.keys().map(function(id) {
'start %s' return broadcastSet.get(id).onStart(frameProducer)
, JSON.stringify(frameProducer.banner)
)
broadcastSet.keys().forEach(function(id) {
var ws = broadcastSet.get(id)
switch (ws.readyState) {
case WebSocket.OPENING:
// This should never happen.
log.warn('Unable to send banner to OPENING client "%s"', id)
break
case WebSocket.OPEN:
// This is what SHOULD happen.
ws.send(message)
break
case WebSocket.CLOSING:
// Ok, a 'close' event should remove the client from the set
// soon.
break
case WebSocket.CLOSED:
// This should never happen.
log.warn('Unable to send banner to CLOSED client "%s"', id)
broadcastSet.remove(id)
break
}
}) })
}) })
@ -493,32 +481,7 @@ module.exports = syrup.serial()
var frame var frame
if ((frame = frameProducer.nextFrame())) { if ((frame = frameProducer.nextFrame())) {
Promise.settle([broadcastSet.keys().map(function(id) { Promise.settle([broadcastSet.keys().map(function(id) {
return new Promise(function(resolve, reject) { return broadcastSet.get(id).onFrame(frame)
var ws = broadcastSet.get(id)
switch (ws.readyState) {
case WebSocket.OPENING:
// This should never happen.
return reject(new Error(util.format(
'Unable to send frame to OPENING client "%s"', id)))
case WebSocket.OPEN:
// This is what SHOULD happen.
ws.send(frame, {
binary: true
}, function(err) {
return err ? reject(err) : resolve()
})
return
case WebSocket.CLOSING:
// Ok, a 'close' event should remove the client from the set
// soon.
return
case WebSocket.CLOSED:
// This should never happen.
broadcastSet.remove(id)
return reject(new Error(util.format(
'Unable to send frame to CLOSED client "%s"', id)))
}
})
})]).then(next) })]).then(next)
} }
else { else {
@ -534,12 +497,74 @@ module.exports = syrup.serial()
wss.on('connection', function(ws) { wss.on('connection', function(ws) {
var id = uuid.v4() var id = uuid.v4()
function wsStartNotifier() {
return new Promise(function(resolve, reject) {
var message = util.format(
'start %s'
, JSON.stringify(frameProducer.banner)
)
switch (ws.readyState) {
case WebSocket.OPENING:
// This should never happen.
log.warn('Unable to send banner to OPENING client "%s"', id)
break
case WebSocket.OPEN:
// This is what SHOULD happen.
ws.send(message, function(err) {
return err ? reject(err) : resolve()
})
break
case WebSocket.CLOSING:
// Ok, a 'close' event should remove the client from the set
// soon.
break
case WebSocket.CLOSED:
// This should never happen.
log.warn('Unable to send banner to CLOSED client "%s"', id)
broadcastSet.remove(id)
break
}
})
}
function wsFrameNotifier(frame) {
return new Promise(function(resolve, reject) {
switch (ws.readyState) {
case WebSocket.OPENING:
// This should never happen.
return reject(new Error(util.format(
'Unable to send frame to OPENING client "%s"', id)))
case WebSocket.OPEN:
// This is what SHOULD happen.
ws.send(frame, {
binary: true
}, function(err) {
return err ? reject(err) : resolve()
})
return
case WebSocket.CLOSING:
// Ok, a 'close' event should remove the client from the set
// soon.
return
case WebSocket.CLOSED:
// This should never happen.
broadcastSet.remove(id)
return reject(new Error(util.format(
'Unable to send frame to CLOSED client "%s"', id)))
}
})
}
ws.on('message', function(data) { ws.on('message', function(data) {
var match var match
if ((match = /^(on|off|(size) ([0-9]+)x([0-9]+))$/.exec(data))) { if ((match = /^(on|off|(size) ([0-9]+)x([0-9]+))$/.exec(data))) {
switch (match[2] || match[1]) { switch (match[2] || match[1]) {
case 'on': case 'on':
broadcastSet.insert(id, ws) broadcastSet.insert(id, {
onStart: wsStartNotifier
, onFrame: wsFrameNotifier
})
break break
case 'off': case 'off':
broadcastSet.remove(id) broadcastSet.remove(id)
@ -563,6 +588,7 @@ module.exports = syrup.serial()
lifecycle.observe(function() { lifecycle.observe(function() {
frameProducer.stop() frameProducer.stop()
}) })
return frameProducer
}) })
.return(plugin)
}) })

View file

@ -365,7 +365,7 @@ module.exports = syrup.serial()
plugin.rotate = function(rotation) { plugin.rotate = function(rotation) {
return runAgentCommand( return runAgentCommand(
apk.wire.MessageType.SET_ROTATION apk.wire.MessageType.SET_ROTATION
, new apk.wire.SetRotationRequest(rotation, false) , new apk.wire.SetRotationRequest(rotation, options.lockRotation || false)
) )
} }

View file

@ -0,0 +1,290 @@
var net = require('net')
var util = require('util')
var os = require('os')
var syrup = require('stf-syrup')
var Promise = require('bluebird')
var uuid = require('node-uuid')
var jpeg = require('jpeg-turbo')
var logger = require('../../../../util/logger')
var grouputil = require('../../../../util/grouputil')
var wire = require('../../../../wire')
var wireutil = require('../../../../wire/util')
var lifecycle = require('../../../../util/lifecycle')
var VncServer = require('./util/server')
var VncConnection = require('./util/connection')
var PointerTranslator = require('./util/pointertranslator')
module.exports = syrup.serial()
.dependency(require('../../support/router'))
.dependency(require('../../support/push'))
.dependency(require('../screen/stream'))
.dependency(require('../touch'))
.dependency(require('../group'))
.dependency(require('../solo'))
.define(function(options, router, push, screenStream, touch, group, solo) {
var log = logger.createLogger('device:plugins:vnc')
function vncAuthHandler(data) {
log.info(
'VNC authentication attempt using "%s"'
, data.response.toString('hex')
)
var resolver = Promise.defer()
function notify() {
group.get()
.then(function(currentGroup) {
push.send([
solo.channel
, wireutil.envelope(new wire.JoinGroupByVncAuthResponseMessage(
options.serial
, data.response.toString('hex')
, currentGroup.group
))
])
})
.catch(grouputil.NoGroupError, function() {
push.send([
solo.channel
, wireutil.envelope(new wire.JoinGroupByVncAuthResponseMessage(
options.serial
, data.response.toString('hex')
))
])
})
}
function joinListener(newGroup, identifier) {
if (!data.response.equals(new Buffer(identifier || '', 'hex'))) {
resolver.reject(new Error('Someone else took the device'))
}
}
function autojoinListener(identifier, joined) {
if (data.response.equals(new Buffer(identifier, 'hex'))) {
if (joined) {
resolver.resolve()
}
else {
resolver.reject(new Error('Device is already in use'))
}
}
}
group.on('join', joinListener)
group.on('autojoin', autojoinListener)
router.on(wire.VncAuthResponsesUpdatedMessage, notify)
notify()
return resolver.promise
.timeout(5000)
.finally(function() {
group.removeListener('join', joinListener)
group.removeListener('autojoin', autojoinListener)
router.removeListener(wire.VncAuthResponsesUpdatedMessage, notify)
})
}
function createServer() {
log.info('Starting VNC server on port %d', options.vncPort)
var opts = {
name: options.serial
, width: options.vncInitialSize[0]
, height: options.vncInitialSize[1]
, security: [{
type: VncConnection.SECURITY_VNC
, challenge: new Buffer(16).fill(0)
, auth: vncAuthHandler
}]
}
var vnc = new VncServer(net.createServer({
allowHalfOpen: true
}), opts)
var listeningListener, errorListener
return new Promise(function(resolve, reject) {
listeningListener = function() {
return resolve(vnc)
}
errorListener = function(err) {
return reject(err)
}
vnc.on('listening', listeningListener)
vnc.on('error', errorListener)
vnc.listen(options.vncPort)
})
.finally(function() {
vnc.removeListener('listening', listeningListener)
vnc.removeListener('error', errorListener)
})
}
return createServer()
.then(function(vnc) {
vnc.on('connection', function(conn) {
log.info('New VNC connection from %s', conn.conn.remoteAddress)
var id = util.format('vnc-%s', uuid.v4())
var connState = {
lastFrame: null
, lastFrameTime: null
, frameWidth: 0
, frameHeight: 0
, sentFrameTime: null
, updateRequests: 0
, frameConfig: {
format: jpeg.FORMAT_RGB
}
}
var pointerTranslator = new PointerTranslator()
pointerTranslator.on('touchdown', function(event) {
touch.touchDown(event)
})
pointerTranslator.on('touchmove', function(event) {
touch.touchMove(event)
})
pointerTranslator.on('touchup', function(event) {
touch.touchUp(event)
})
pointerTranslator.on('touchcommit', function() {
touch.touchCommit()
})
function maybeSendFrame() {
if (!connState.updateRequests) {
return
}
if (!connState.lastFrame) {
return
}
if (connState.lastFrameTime === connState.sentFrameTime) {
return
}
var decoded = jpeg.decompressSync(
connState.lastFrame, connState.frameConfig)
conn.writeFramebufferUpdate([
{ xPosition: 0
, yPosition: 0
, width: decoded.width
, height: decoded.height
, encodingType: VncConnection.ENCODING_RAW
, data: decoded.data
}
, { xPosition: 0
, yPosition: 0
, width: decoded.width
, height: decoded.height
, encodingType: VncConnection.ENCODING_DESKTOPSIZE
}
])
connState.updateRequests = 0
connState.sentFrameTime = connState.lastFrameTime
}
function vncStartListener(frameProducer) {
return new Promise(function(resolve/*, reject*/) {
connState.frameWidth = frameProducer.banner.virtualWidth
connState.frameHeight = frameProducer.banner.virtualHeight
resolve()
})
}
function vncFrameListener(frame) {
return new Promise(function(resolve/*, reject*/) {
connState.lastFrame = frame
connState.lastFrameTime = Date.now()
maybeSendFrame()
resolve()
})
}
function groupLeaveListener() {
conn.end()
}
conn.on('authenticated', function() {
screenStream.updateProjection(
options.vncInitialSize[0], options.vncInitialSize[1])
screenStream.broadcastSet.insert(id, {
onStart: vncStartListener
, onFrame: vncFrameListener
})
})
conn.on('fbupdaterequest', function() {
connState.updateRequests += 1
maybeSendFrame()
})
conn.on('formatchange', function(format) {
var same = os.endianness() === 'BE'
=== Boolean(format.bigEndianFlag)
switch (format.bitsPerPixel) {
case 8:
connState.frameConfig = {
format: jpeg.FORMAT_GRAY
}
break
case 24:
connState.frameConfig = {
format: ((format.redShift > format.blueShift) === same)
? jpeg.FORMAT_BGR
: jpeg.FORMAT_RGB
}
break
case 32:
connState.frameConfig = {
format: ((format.redShift > format.blueShift) === same)
? (format.blueShift === 0
? jpeg.FORMAT_BGRX
: jpeg.FORMAT_XBGR)
: (format.redShift === 0
? jpeg.FORMAT_RGBX
: jpeg.FORMAT_XRGB)
}
break
}
})
conn.on('pointer', function(event) {
pointerTranslator.push(event)
})
conn.on('close', function() {
screenStream.broadcastSet.remove(id)
group.removeListener('leave', groupLeaveListener)
})
conn.on('userActivity', function() {
group.keepalive()
})
group.on('leave', groupLeaveListener)
})
lifecycle.observe(function() {
vnc.close()
})
})
})

View file

@ -0,0 +1,535 @@
var util = require('util')
var os = require('os')
var crypto = require('crypto')
var EventEmitter = require('eventemitter3').EventEmitter
var debug = require('debug')('vnc:connection')
var Promise = require('bluebird')
var PixelFormat = require('./pixelformat')
function VncConnection(conn, options) {
this.options = options
this._bound = {
_errorListener: this._errorListener.bind(this)
, _readableListener: this._readableListener.bind(this)
, _endListener: this._endListener.bind(this)
, _closeListener: this._closeListener.bind(this)
}
this._buffer = null
this._state = 0
this._changeState(VncConnection.STATE_NEED_CLIENT_VERSION)
this._serverVersion = VncConnection.V3_008
this._serverSupportedSecurity = this.options.security
this._serverSupportedSecurityByType =
this.options.security.reduce(
function(map, method) {
map[method.type] = method
return map
}
, Object.create(null)
)
this._serverWidth = this.options.width
this._serverHeight = this.options.height
this._serverPixelFormat = new PixelFormat({
bitsPerPixel: 32
, depth: 24
, bigEndianFlag: os.endianness() === 'BE' ? 1 : 0
, trueColorFlag: 1
, redMax: 255
, greenMax: 255
, blueMax: 255
, redShift: 16
, greenShift: 8
, blueShift: 0
})
this._serverName = this.options.name
this._clientVersion = null
this._clientShare = false
this._clientPixelFormat = this._serverPixelFormat
this._clientEncodingCount = 0
this._clientEncodings = []
this._clientCutTextLength = 0
this._authChallenge = this.options.challenge || crypto.randomBytes(16)
this.conn = conn
.on('error', this._bound._errorListener)
.on('readable', this._bound._readableListener)
.on('end', this._bound._endListener)
.on('close', this._bound._closeListener)
this._blockingOps = []
this._writeServerVersion()
this._read()
}
util.inherits(VncConnection, EventEmitter)
VncConnection.V3_003 = 3003
VncConnection.V3_007 = 3007
VncConnection.V3_008 = 3008
VncConnection.SECURITY_NONE = 1
VncConnection.SECURITY_VNC = 2
VncConnection.SECURITYRESULT_OK = 0
VncConnection.SECURITYRESULT_FAIL = 1
VncConnection.CLIENT_MESSAGE_SETPIXELFORMAT = 0
VncConnection.CLIENT_MESSAGE_SETENCODINGS = 2
VncConnection.CLIENT_MESSAGE_FBUPDATEREQUEST = 3
VncConnection.CLIENT_MESSAGE_KEYEVENT = 4
VncConnection.CLIENT_MESSAGE_POINTEREVENT = 5
VncConnection.CLIENT_MESSAGE_CLIENTCUTTEXT = 6
VncConnection.SERVER_MESSAGE_FBUPDATE = 0
var StateReverse = Object.create(null), State = {
STATE_NEED_CLIENT_VERSION: 10
, STATE_NEED_CLIENT_SECURITY: 20
, STATE_NEED_CLIENT_INIT: 30
, STATE_NEED_CLIENT_VNC_AUTH: 31
, STATE_NEED_CLIENT_MESSAGE: 40
, STATE_NEED_CLIENT_MESSAGE_SETPIXELFORMAT: 50
, STATE_NEED_CLIENT_MESSAGE_SETENCODINGS: 60
, STATE_NEED_CLIENT_MESSAGE_SETENCODINGS_VALUE: 61
, STATE_NEED_CLIENT_MESSAGE_FBUPDATEREQUEST: 70
, STATE_NEED_CLIENT_MESSAGE_KEYEVENT: 80
, STATE_NEED_CLIENT_MESSAGE_POINTEREVENT: 90
, STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT: 100
, STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT_VALUE: 101
}
VncConnection.ENCODING_RAW = 0
VncConnection.ENCODING_DESKTOPSIZE = -223
Object.keys(State).map(function(name) {
VncConnection[name] = State[name]
StateReverse[State[name]] = name
})
VncConnection.prototype.end = function() {
this.conn.end()
}
VncConnection.prototype.writeFramebufferUpdate = function(rectangles) {
var chunk = new Buffer(4)
chunk[0] = VncConnection.SERVER_MESSAGE_FBUPDATE
chunk[1] = 0
chunk.writeUInt16BE(rectangles.length, 2)
this._write(chunk)
rectangles.forEach(function(rect) {
var rchunk = new Buffer(12)
rchunk.writeUInt16BE(rect.xPosition, 0)
rchunk.writeUInt16BE(rect.yPosition, 2)
rchunk.writeUInt16BE(rect.width, 4)
rchunk.writeUInt16BE(rect.height, 6)
rchunk.writeInt32BE(rect.encodingType, 8)
this._write(rchunk)
switch (rect.encodingType) {
case VncConnection.ENCODING_RAW:
this._write(rect.data)
break
case VncConnection.ENCODING_DESKTOPSIZE:
this._serverWidth = rect.width
this._serverHeight = rect.height
break
default:
throw new Error(util.format(
'Unsupported encoding type', rect.encodingType))
}
}, this)
}
VncConnection.prototype._error = function(err) {
this.emit('error', err)
this.end()
}
VncConnection.prototype._errorListener = function(err) {
this._error(err)
}
VncConnection.prototype._endListener = function() {
this.emit('end')
}
VncConnection.prototype._closeListener = function() {
this.emit('close')
}
VncConnection.prototype._writeServerVersion = function() {
// Yes, we could just format the string instead. Didn't feel like it.
switch (this._serverVersion) {
case VncConnection.V3_003:
this._write(new Buffer('RFB 003.003\n'))
break
case VncConnection.V3_007:
this._write(new Buffer('RFB 003.007\n'))
break
case VncConnection.V3_008:
this._write(new Buffer('RFB 003.008\n'))
break
}
}
VncConnection.prototype._writeSupportedSecurity = function() {
var chunk = new Buffer(1 + this._serverSupportedSecurity.length)
chunk[0] = this._serverSupportedSecurity.length
this._serverSupportedSecurity.forEach(function(security, i) {
chunk[1 + i] = security.type
})
this._write(chunk)
}
VncConnection.prototype._writeSecurityResult = function(result, reason) {
var chunk
switch (result) {
case VncConnection.SECURITYRESULT_OK:
chunk = new Buffer(4)
chunk.writeUInt32BE(result, 0)
this._write(chunk)
break
case VncConnection.SECURITYRESULT_FAIL:
chunk = new Buffer(4 + 4 + reason.length)
chunk.writeUInt32BE(result, 0)
chunk.writeUInt32BE(reason.length, 4)
chunk.write(reason, 8, reason.length)
this._write(chunk)
break
}
}
VncConnection.prototype._writeServerInit = function() {
debug('server pixel format', this._serverPixelFormat)
var chunk = new Buffer(2 + 2 + 16 + 4 + this._serverName.length)
chunk.writeUInt16BE(this._serverWidth, 0)
chunk.writeUInt16BE(this._serverHeight, 2)
chunk[4] = this._serverPixelFormat.bitsPerPixel
chunk[5] = this._serverPixelFormat.depth
chunk[6] = this._serverPixelFormat.bigEndianFlag
chunk[7] = this._serverPixelFormat.trueColorFlag
chunk.writeUInt16BE(this._serverPixelFormat.redMax, 8)
chunk.writeUInt16BE(this._serverPixelFormat.greenMax, 10)
chunk.writeUInt16BE(this._serverPixelFormat.blueMax, 12)
chunk[14] = this._serverPixelFormat.redShift
chunk[15] = this._serverPixelFormat.greenShift
chunk[16] = this._serverPixelFormat.blueShift
chunk[17] = 0 // padding
chunk[18] = 0 // padding
chunk[19] = 0 // padding
chunk.writeUInt32BE(this._serverName.length, 20)
chunk.write(this._serverName, 24, this._serverName.length)
this._write(chunk)
}
VncConnection.prototype._writeVncAuthChallenge = function() {
var vncSec = this._serverSupportedSecurityByType[VncConnection.SECURITY_VNC]
debug('vnc auth challenge', vncSec.challenge)
this._write(vncSec.challenge)
}
VncConnection.prototype._readableListener = function() {
this._read()
}
VncConnection.prototype._read = function() {
Promise.all(this._blockingOps).bind(this)
.then(this._unguardedRead)
}
VncConnection.prototype._auth = function(type, data) {
var security = this._serverSupportedSecurityByType[type]
this._blockingOps.push(
security.auth(data).bind(this)
.then(function() {
this._changeState(VncConnection.STATE_NEED_CLIENT_INIT)
this._writeSecurityResult(VncConnection.SECURITYRESULT_OK)
this.emit('authenticated')
this._read()
})
.catch(function() {
this._writeSecurityResult(
VncConnection.SECURITYRESULT_FAIL, 'Authentication failure')
this.end()
})
)
}
VncConnection.prototype._unguardedRead = function() {
var chunk, lo, hi
while (this._append(this.conn.read())) {
do {
debug('state', StateReverse[this._state])
chunk = null
switch (this._state) {
case VncConnection.STATE_NEED_CLIENT_VERSION:
if ((chunk = this._consume(12))) {
if ((this._clientVersion = this._parseVersion(chunk)) === null) {
this.end()
return
}
debug('client version', this._clientVersion)
this._writeSupportedSecurity()
this._changeState(VncConnection.STATE_NEED_CLIENT_SECURITY)
}
break
case VncConnection.STATE_NEED_CLIENT_SECURITY:
if ((chunk = this._consume(1))) {
if ((this._clientSecurity = this._parseSecurity(chunk)) === null) {
this._writeSecurityResult(
VncConnection.SECURITYRESULT_FAIL, 'Unimplemented security type')
this.end()
return
}
debug('client security', this._clientSecurity)
if (!(this._clientSecurity in this._serverSupportedSecurityByType)) {
this._writeSecurityResult(
VncConnection.SECURITYRESULT_FAIL, 'Unsupported security type')
this.end()
return
}
switch (this._clientSecurity) {
case VncConnection.SECURITY_NONE:
this._auth(VncConnection.SECURITY_NONE)
return
case VncConnection.SECURITY_VNC:
this._writeVncAuthChallenge()
this._changeState(VncConnection.STATE_NEED_CLIENT_VNC_AUTH)
break
}
}
break
case VncConnection.STATE_NEED_CLIENT_VNC_AUTH:
if ((chunk = this._consume(16))) {
this._auth(VncConnection.SECURITY_VNC, {
response: chunk
})
return
}
break
case VncConnection.STATE_NEED_CLIENT_INIT:
if ((chunk = this._consume(1))) {
this._clientShare = chunk[0]
debug('client shareFlag', this._clientShare)
this._writeServerInit()
this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
}
break
case VncConnection.STATE_NEED_CLIENT_MESSAGE:
if ((chunk = this._consume(1))) {
switch (chunk[0]) {
case VncConnection.CLIENT_MESSAGE_SETPIXELFORMAT:
this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_SETPIXELFORMAT)
break
case VncConnection.CLIENT_MESSAGE_SETENCODINGS:
this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_SETENCODINGS)
break
case VncConnection.CLIENT_MESSAGE_FBUPDATEREQUEST:
this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_FBUPDATEREQUEST)
break
case VncConnection.CLIENT_MESSAGE_KEYEVENT:
this.emit('userActivity')
this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_KEYEVENT)
break
case VncConnection.CLIENT_MESSAGE_POINTEREVENT:
this.emit('userActivity')
this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_POINTEREVENT)
break
case VncConnection.CLIENT_MESSAGE_CLIENTCUTTEXT:
this.emit('userActivity')
this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT)
break
default:
this._error(new Error(util.format(
'Unsupported message type %d', chunk[0])))
return
}
}
break
case VncConnection.STATE_NEED_CLIENT_MESSAGE_SETPIXELFORMAT:
if ((chunk = this._consume(19))) {
// [0b, 3b) padding
this._clientPixelFormat = new PixelFormat({
bitsPerPixel: chunk[3]
, depth: chunk[4]
, bigEndianFlag: chunk[5]
, trueColorFlag: chunk[6]
, redMax: chunk.readUInt16BE(7, true)
, greenMax: chunk.readUInt16BE(9, true)
, blueMax: chunk.readUInt16BE(11, true)
, redShift: chunk[13]
, greenShift: chunk[14]
, blueShift: chunk[15]
})
// [16b, 19b) padding
debug('client pixel format', this._clientPixelFormat)
this.emit('formatchange', this._clientPixelFormat)
this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
}
break
case VncConnection.STATE_NEED_CLIENT_MESSAGE_SETENCODINGS:
if ((chunk = this._consume(3))) {
// [0b, 1b) padding
this._clientEncodingCount = chunk.readUInt16BE(1, true)
this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_SETENCODINGS_VALUE)
}
break
case VncConnection.STATE_NEED_CLIENT_MESSAGE_SETENCODINGS_VALUE:
lo = 0
hi = 4 * this._clientEncodingCount
if ((chunk = this._consume(hi))) {
this._clientEncodings = []
while (lo < hi) {
this._clientEncodings.push(chunk.readInt32BE(lo, true))
lo += 4
}
debug('client encodings', this._clientEncodings)
this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
}
break
case VncConnection.STATE_NEED_CLIENT_MESSAGE_FBUPDATEREQUEST:
if ((chunk = this._consume(9))) {
this.emit('fbupdaterequest', {
incremental: chunk[0]
, xPosition: chunk.readUInt16BE(1, true)
, yPosition: chunk.readUInt16BE(3, true)
, width: chunk.readUInt16BE(5, true)
, height: chunk.readUInt16BE(7, true)
})
this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
}
break
case VncConnection.STATE_NEED_CLIENT_MESSAGE_KEYEVENT:
if ((chunk = this._consume(7))) {
// downFlag = chunk[0]
// [1b, 3b) padding
// key = chunk.readUInt32BE(3, true)
this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
}
break
case VncConnection.STATE_NEED_CLIENT_MESSAGE_POINTEREVENT:
if ((chunk = this._consume(5))) {
this.emit('pointer', {
buttonMask: chunk[0]
, xPosition: chunk.readUInt16BE(1, true) / this._serverWidth
, yPosition: chunk.readUInt16BE(3, true) / this._serverHeight
})
this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
}
break
case VncConnection.STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT:
if ((chunk = this._consume(7))) {
// [0b, 3b) padding
this._clientCutTextLength = chunk.readUInt32BE(3)
this._changeState(
VncConnection.STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT_VALUE)
}
break
case VncConnection.STATE_NEED_CLIENT_MESSAGE_CLIENTCUTTEXT_VALUE:
if ((chunk = this._consume(this._clientCutTextLength))) {
// value = chunk
this._changeState(VncConnection.STATE_NEED_CLIENT_MESSAGE)
}
break
default:
throw new Error(util.format('Impossible state %d', this._state))
}
}
while (chunk)
}
}
VncConnection.prototype._parseVersion = function(chunk) {
if (chunk.equals(new Buffer('RFB 003.008\n'))) {
return VncConnection.V3_008
}
if (chunk.equals(new Buffer('RFB 003.007\n'))) {
return VncConnection.V3_007
}
if (chunk.equals(new Buffer('RFB 003.003\n'))) {
return VncConnection.V3_003
}
return null
}
VncConnection.prototype._parseSecurity = function(chunk) {
switch (chunk[0]) {
case VncConnection.SECURITY_NONE:
case VncConnection.SECURITY_VNC:
return chunk[0]
default:
return null
}
}
VncConnection.prototype._changeState = function(state) {
this._state = state
}
VncConnection.prototype._append = function(chunk) {
if (!chunk) {
return false
}
debug('in', chunk)
if (this._buffer) {
this._buffer = Buffer.concat(
[this._buffer, chunk], this._buffer.length + chunk.length)
}
else {
this._buffer = chunk
}
return true
}
VncConnection.prototype._consume = function(n) {
var chunk
if (!this._buffer) {
return null
}
if (n < this._buffer.length) {
chunk = this._buffer.slice(0, n)
this._buffer = this._buffer.slice(n)
return chunk
}
if (n === this._buffer.length) {
chunk = this._buffer
this._buffer = null
return chunk
}
return null
}
VncConnection.prototype._write = function(chunk) {
debug('out', chunk)
this.conn.write(chunk)
}
module.exports = VncConnection

View file

@ -0,0 +1,14 @@
function PixelFormat(values) {
this.bitsPerPixel = values.bitsPerPixel
this.depth = values.depth
this.bigEndianFlag = values.bigEndianFlag
this.trueColorFlag = values.trueColorFlag
this.redMax = values.redMax
this.greenMax = values.greenMax
this.blueMax = values.blueMax
this.redShift = values.redShift
this.greenShift = values.greenShift
this.blueShift = values.blueShift
}
module.exports = PixelFormat

View file

@ -0,0 +1,66 @@
var util = require('util')
var EventEmitter = require('eventemitter3').EventEmitter
function PointerTranslator() {
this.previousEvent = null
}
util.inherits(PointerTranslator, EventEmitter)
PointerTranslator.prototype.push = function(event) {
if (event.buttonMask & 0xFE) {
// Non-primary buttons included, ignore.
return
}
if (this.previousEvent) {
var buttonChanges = event.buttonMask ^ this.previousEvent.buttonMask
// If the primary button changed, we have an up/down event.
if (buttonChanges & 1) {
// If it's pressed now, that's a down event.
if (event.buttonMask & 1) {
this.emit('touchdown', {
contact: 1
, x: event.xPosition
, y: event.yPosition
})
this.emit('touchcommit')
}
// It's not pressed, so we have an up event.
else {
this.emit('touchup', {
contact: 1
})
this.emit('touchcommit')
}
}
// Otherwise, if we're still holding the primary button down,
// that's a move event.
else if (event.buttonMask & 1) {
this.emit('touchmove', {
contact: 1
, x: event.xPosition
, y: event.yPosition
})
this.emit('touchcommit')
}
}
else {
// If it's the first event we get and the primary button's pressed,
// it's a down event.
if (event.buttonMask & 1) {
this.emit('touchdown', {
contact: 1
, x: event.xPosition
, y: event.yPosition
})
this.emit('touchcommit')
}
}
this.previousEvent = event
}
module.exports = PointerTranslator

View file

@ -0,0 +1,52 @@
var util = require('util')
var EventEmitter = require('eventemitter3').EventEmitter
var debug = require('debug')('vnc:server')
var VncConnection = require('./connection')
function VncServer(server, options) {
this.options = options
this._bound = {
_listeningListener: this._listeningListener.bind(this)
, _connectionListener: this._connectionListener.bind(this)
, _closeListener: this._closeListener.bind(this)
, _errorListener: this._errorListener.bind(this)
}
this.server = server
.on('listening', this._bound._listeningListener)
.on('connection', this._bound._connectionListener)
.on('close', this._bound._closeListener)
.on('error', this._bound._errorListener)
}
util.inherits(VncServer, EventEmitter)
VncServer.prototype.close = function() {
this.server.close()
}
VncServer.prototype.listen = function() {
this.server.listen.apply(this.server, arguments)
}
VncServer.prototype._listeningListener = function() {
this.emit('listening')
}
VncServer.prototype._connectionListener = function(conn) {
debug('connection', conn.remoteAddress, conn.remotePort)
this.emit('connection', new VncConnection(conn, this.options))
}
VncServer.prototype._closeListener = function() {
this.emit('close')
}
VncServer.prototype._errorListener = function(err) {
this.emit('error', err)
}
module.exports = VncServer

View file

@ -14,7 +14,7 @@ module.exports = syrup.serial()
.dependency(require('../support/properties')) .dependency(require('../support/properties'))
.dependency(require('../support/abi')) .dependency(require('../support/abi'))
.define(function(options, adb, properties, abi) { .define(function(options, adb, properties, abi) {
var log = logger.createLogger('device:resources:minicap') logger.createLogger('device:resources:minicap')
var resources = { var resources = {
bin: { bin: {

View file

@ -17,7 +17,7 @@ module.exports = syrup.serial()
pathutil.vendor('STFService/wire.proto')) pathutil.vendor('STFService/wire.proto'))
var resource = { var resource = {
requiredVersion: '1.0.1' requiredVersion: '1.0.2'
, pkg: 'jp.co.cyberagent.stf' , pkg: 'jp.co.cyberagent.stf'
, main: 'jp.co.cyberagent.stf.Agent' , main: 'jp.co.cyberagent.stf.Agent'
, apk: pathutil.vendor('STFService/STFService.apk') , apk: pathutil.vendor('STFService/STFService.apk')
@ -79,9 +79,9 @@ module.exports = syrup.serial()
.then(function() { .then(function() {
return promiseutil.periodicNotify( return promiseutil.periodicNotify(
adb.install(options.serial, resource.apk) adb.install(options.serial, resource.apk)
, 10000 , 20000
) )
.timeout(60000) .timeout(65000)
}) })
.progressed(function() { .progressed(function() {
log.warn( log.warn(

View file

@ -1,17 +1,17 @@
var syrup = require('stf-syrup') var syrup = require('stf-syrup')
var zmq = require('zmq')
var Promise = require('bluebird') var Promise = require('bluebird')
var logger = require('../../../util/logger') var logger = require('../../../util/logger')
var srv = require('../../../util/srv') var srv = require('../../../util/srv')
var zmqutil = require('../../../util/zmqutil')
module.exports = syrup.serial() module.exports = syrup.serial()
.define(function(options) { .define(function(options) {
var log = logger.createLogger('device:support:push') var log = logger.createLogger('device:support:push')
// Output // Output
var push = zmq.socket('push') var push = zmqutil.socket('push')
return Promise.map(options.endpoints.push, function(endpoint) { return Promise.map(options.endpoints.push, function(endpoint) {
return srv.resolve(endpoint).then(function(records) { return srv.resolve(endpoint).then(function(records) {

View file

@ -1,19 +1,19 @@
var syrup = require('stf-syrup') var syrup = require('stf-syrup')
var zmq = require('zmq')
var Promise = require('bluebird') var Promise = require('bluebird')
var logger = require('../../../util/logger') var logger = require('../../../util/logger')
var wireutil = require('../../../wire/util') var wireutil = require('../../../wire/util')
var srv = require('../../../util/srv') var srv = require('../../../util/srv')
var lifecycle = require('../../../util/lifecycle') require('../../../util/lifecycle')
var zmqutil = require('../../../util/zmqutil')
module.exports = syrup.serial() module.exports = syrup.serial()
.define(function(options) { .define(function(options) {
var log = logger.createLogger('device:support:sub') var log = logger.createLogger('device:support:sub')
// Input // Input
var sub = zmq.socket('sub') var sub = zmqutil.socket('sub')
return Promise.map(options.endpoints.sub, function(endpoint) { return Promise.map(options.endpoints.sub, function(endpoint) {
return srv.resolve(endpoint).then(function(records) { return srv.resolve(endpoint).then(function(records) {

View file

@ -1,5 +1,4 @@
var Promise = require('bluebird') var Promise = require('bluebird')
var zmq = require('zmq')
var logger = require('../../util/logger') var logger = require('../../util/logger')
var wire = require('../../wire') var wire = require('../../wire')
@ -8,12 +7,13 @@ var wireutil = require('../../wire/util')
var lifecycle = require('../../util/lifecycle') var lifecycle = require('../../util/lifecycle')
var srv = require('../../util/srv') var srv = require('../../util/srv')
var dbapi = require('../../db/api') var dbapi = require('../../db/api')
var zmqutil = require('../../util/zmqutil')
module.exports = function(options) { module.exports = function(options) {
var log = logger.createLogger('log-db') var log = logger.createLogger('log-db')
// Input // Input
var sub = zmq.socket('sub') var sub = zmqutil.socket('sub')
Promise.map(options.endpoints.sub, function(endpoint) { Promise.map(options.endpoints.sub, function(endpoint) {
return srv.resolve(endpoint).then(function(records) { return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) { return srv.attempt(records, function(record) {

View file

@ -2,7 +2,6 @@ var util = require('util')
var Hipchatter = require('hipchatter') var Hipchatter = require('hipchatter')
var Promise = require('bluebird') var Promise = require('bluebird')
var zmq = require('zmq')
var logger = require('../../util/logger') var logger = require('../../util/logger')
var wire = require('../../wire') var wire = require('../../wire')
@ -10,6 +9,7 @@ var wirerouter = require('../../wire/router')
var wireutil = require('../../wire/util') var wireutil = require('../../wire/util')
var lifecycle = require('../../util/lifecycle') var lifecycle = require('../../util/lifecycle')
var srv = require('../../util/srv') var srv = require('../../util/srv')
var zmqutil = require('../../util/zmqutil')
var COLORS = { var COLORS = {
1: 'gray' 1: 'gray'
@ -28,7 +28,7 @@ module.exports = function(options) {
, timer , timer
// Input // Input
var sub = zmq.socket('sub') var sub = zmqutil.socket('sub')
Promise.map(options.endpoints.sub, function(endpoint) { Promise.map(options.endpoints.sub, function(endpoint) {
return srv.resolve(endpoint).then(function(records) { return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) { return srv.attempt(records, function(record) {

View file

@ -1,5 +1,4 @@
var Promise = require('bluebird') var Promise = require('bluebird')
var zmq = require('zmq')
var logger = require('../../util/logger') var logger = require('../../util/logger')
var wire = require('../../wire') var wire = require('../../wire')
@ -8,6 +7,7 @@ var wireutil = require('../../wire/util')
var dbapi = require('../../db/api') var dbapi = require('../../db/api')
var lifecycle = require('../../util/lifecycle') var lifecycle = require('../../util/lifecycle')
var srv = require('../../util/srv') var srv = require('../../util/srv')
var zmqutil = require('../../util/zmqutil')
module.exports = function(options) { module.exports = function(options) {
var log = logger.createLogger('processor') var log = logger.createLogger('processor')
@ -17,7 +17,7 @@ module.exports = function(options) {
} }
// App side // App side
var appDealer = zmq.socket('dealer') var appDealer = zmqutil.socket('dealer')
Promise.map(options.endpoints.appDealer, function(endpoint) { Promise.map(options.endpoints.appDealer, function(endpoint) {
return srv.resolve(endpoint).then(function(records) { return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) { return srv.attempt(records, function(record) {
@ -37,7 +37,7 @@ module.exports = function(options) {
}) })
// Device side // Device side
var devDealer = zmq.socket('dealer') var devDealer = zmqutil.socket('dealer')
Promise.map(options.endpoints.devDealer, function(endpoint) { Promise.map(options.endpoints.devDealer, function(endpoint) {
return srv.resolve(endpoint).then(function(records) { return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) { return srv.attempt(records, function(record) {
@ -124,12 +124,46 @@ module.exports = function(options) {
}) })
.catch(function(err) { .catch(function(err) {
log.error( log.error(
'Unable to lookup user by fingerprint "%s"' 'Unable to lookup user by ADB fingerprint "%s"'
, message.fingerprint , message.fingerprint
, err.stack , err.stack
) )
}) })
}) })
.on(wire.JoinGroupByVncAuthResponseMessage, function(channel, message) {
dbapi.lookupUserByVncAuthResponse(message.response, message.serial)
.then(function(user) {
if (user) {
devDealer.send([
channel
, wireutil.envelope(new wire.AutoGroupMessage(
new wire.OwnerMessage(
user.email
, user.name
, user.group
)
, message.response
))
])
}
else if (message.currentGroup) {
appDealer.send([
message.currentGroup
, wireutil.envelope(new wire.JoinGroupByVncAuthResponseMessage(
message.serial
, message.response
))
])
}
})
.catch(function(err) {
log.error(
'Unable to lookup user by VNC auth response "%s"'
, message.response
, err.stack
)
})
})
.on(wire.JoinGroupMessage, function(channel, message, data) { .on(wire.JoinGroupMessage, function(channel, message, data) {
dbapi.setDeviceOwner(message.serial, message.owner) dbapi.setDeviceOwner(message.serial, message.owner)
appDealer.send([channel, data]) appDealer.send([channel, data])

View file

@ -1,6 +1,5 @@
var adb = require('adbkit') var adb = require('adbkit')
var Promise = require('bluebird') var Promise = require('bluebird')
var zmq = require('zmq')
var _ = require('lodash') var _ = require('lodash')
var EventEmitter = require('eventemitter3').EventEmitter var EventEmitter = require('eventemitter3').EventEmitter
@ -11,6 +10,7 @@ var wirerouter = require('../../wire/router')
var procutil = require('../../util/procutil') var procutil = require('../../util/procutil')
var lifecycle = require('../../util/lifecycle') var lifecycle = require('../../util/lifecycle')
var srv = require('../../util/srv') var srv = require('../../util/srv')
var zmqutil = require('../../util/zmqutil')
module.exports = function(options) { module.exports = function(options) {
var log = logger.createLogger('provider') var log = logger.createLogger('provider')
@ -70,7 +70,7 @@ module.exports = function(options) {
})() })()
// Output // Output
var push = zmq.socket('push') var push = zmqutil.socket('push')
Promise.map(options.endpoints.push, function(endpoint) { Promise.map(options.endpoints.push, function(endpoint) {
return srv.resolve(endpoint).then(function(records) { return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) { return srv.attempt(records, function(record) {
@ -86,7 +86,7 @@ module.exports = function(options) {
}) })
// Input // Input
var sub = zmq.socket('sub') var sub = zmqutil.socket('sub')
Promise.map(options.endpoints.sub, function(endpoint) { Promise.map(options.endpoints.sub, function(endpoint) {
return srv.resolve(endpoint).then(function(records) { return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) { return srv.attempt(records, function(record) {
@ -316,7 +316,7 @@ module.exports = function(options) {
// Spawn a device worker // Spawn a device worker
function spawn() { function spawn() {
var allocatedPorts = ports.splice(0, 2) var allocatedPorts = ports.splice(0, 4)
, proc = options.fork(device, allocatedPorts) , proc = options.fork(device, allocatedPorts)
, resolver = Promise.defer() , resolver = Promise.defer()

View file

@ -1,5 +1,4 @@
var Promise = require('bluebird') var Promise = require('bluebird')
var zmq = require('zmq')
var logger = require('../../util/logger') var logger = require('../../util/logger')
var wire = require('../../wire') var wire = require('../../wire')
@ -9,6 +8,7 @@ var dbapi = require('../../db/api')
var lifecycle = require('../../util/lifecycle') var lifecycle = require('../../util/lifecycle')
var srv = require('../../util/srv') var srv = require('../../util/srv')
var TtlSet = require('../../util/ttlset') var TtlSet = require('../../util/ttlset')
var zmqutil = require('../../util/zmqutil')
module.exports = function(options) { module.exports = function(options) {
var log = logger.createLogger('reaper') var log = logger.createLogger('reaper')
@ -19,7 +19,7 @@ module.exports = function(options) {
} }
// Input // Input
var sub = zmq.socket('sub') var sub = zmqutil.socket('sub')
Promise.map(options.endpoints.sub, function(endpoint) { Promise.map(options.endpoints.sub, function(endpoint) {
return srv.resolve(endpoint).then(function(records) { return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) { return srv.attempt(records, function(record) {
@ -41,7 +41,7 @@ module.exports = function(options) {
}) })
// Output // Output
var push = zmq.socket('push') var push = zmqutil.socket('push')
Promise.map(options.endpoints.push, function(endpoint) { Promise.map(options.endpoints.push, function(endpoint) {
return srv.resolve(endpoint).then(function(records) { return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) { return srv.attempt(records, function(record) {

View file

@ -138,6 +138,10 @@ module.exports = function(options) {
app.get('/s/blob/:id/:name', function(req, res) { app.get('/s/blob/:id/:name', function(req, res) {
var file = storage.retrieve(req.params.id) var file = storage.retrieve(req.params.id)
if (file) { if (file) {
if (typeof req.query.download !== 'undefined') {
res.set('Content-Disposition',
'attachment; filename="' + path.basename(file.name) + '"')
}
res.set('Content-Type', file.type) res.set('Content-Type', file.type)
res.sendFile(file.path) res.sendFile(file.path)
} }

View file

@ -1,7 +1,6 @@
var zmq = require('zmq')
var logger = require('../../util/logger') var logger = require('../../util/logger')
var lifecycle = require('../../util/lifecycle') var lifecycle = require('../../util/lifecycle')
var zmqutil = require('../../util/zmqutil')
module.exports = function(options) { module.exports = function(options) {
var log = logger.createLogger('triproxy') var log = logger.createLogger('triproxy')
@ -17,18 +16,18 @@ module.exports = function(options) {
} }
// App/device output // App/device output
var pub = zmq.socket('pub') var pub = zmqutil.socket('pub')
pub.bindSync(options.endpoints.pub) pub.bindSync(options.endpoints.pub)
log.info('PUB socket bound on', options.endpoints.pub) log.info('PUB socket bound on', options.endpoints.pub)
// Coordinator input/output // Coordinator input/output
var dealer = zmq.socket('dealer') var dealer = zmqutil.socket('dealer')
dealer.bindSync(options.endpoints.dealer) dealer.bindSync(options.endpoints.dealer)
dealer.on('message', proxy(pub)) dealer.on('message', proxy(pub))
log.info('DEALER socket bound on', options.endpoints.dealer) log.info('DEALER socket bound on', options.endpoints.dealer)
// App/device input // App/device input
var pull = zmq.socket('pull') var pull = zmqutil.socket('pull')
pull.bindSync(options.endpoints.pull) pull.bindSync(options.endpoints.pull)
pull.on('message', proxy(dealer)) pull.on('message', proxy(dealer))
log.info('PULL socket bound on', options.endpoints.pull) log.info('PULL socket bound on', options.endpoints.pull)

View file

@ -3,7 +3,6 @@ var events = require('events')
var util = require('util') var util = require('util')
var socketio = require('socket.io') var socketio = require('socket.io')
var zmq = require('zmq')
var Promise = require('bluebird') var Promise = require('bluebird')
var _ = require('lodash') var _ = require('lodash')
var request = Promise.promisifyAll(require('request')) var request = Promise.promisifyAll(require('request'))
@ -17,6 +16,7 @@ var dbapi = require('../../db/api')
var datautil = require('../../util/datautil') var datautil = require('../../util/datautil')
var srv = require('../../util/srv') var srv = require('../../util/srv')
var lifecycle = require('../../util/lifecycle') var lifecycle = require('../../util/lifecycle')
var zmqutil = require('../../util/zmqutil')
var cookieSession = require('./middleware/cookie-session') var cookieSession = require('./middleware/cookie-session')
var ip = require('./middleware/remote-ip') var ip = require('./middleware/remote-ip')
var auth = require('./middleware/auth') var auth = require('./middleware/auth')
@ -31,7 +31,7 @@ module.exports = function(options) {
, channelRouter = new events.EventEmitter() , channelRouter = new events.EventEmitter()
// Output // Output
var push = zmq.socket('push') var push = zmqutil.socket('push')
Promise.map(options.endpoints.push, function(endpoint) { Promise.map(options.endpoints.push, function(endpoint) {
return srv.resolve(endpoint).then(function(records) { return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) { return srv.attempt(records, function(record) {
@ -47,7 +47,7 @@ module.exports = function(options) {
}) })
// Input // Input
var sub = zmq.socket('sub') var sub = zmqutil.socket('sub')
Promise.map(options.endpoints.sub, function(endpoint) { Promise.map(options.endpoints.sub, function(endpoint) {
return srv.resolve(endpoint).then(function(records) { return srv.resolve(endpoint).then(function(records) {
return srv.attempt(records, function(record) { return srv.attempt(records, function(record) {
@ -826,6 +826,26 @@ module.exports = function(options) {
) )
]) ])
}) })
.on('fs.retrieve', function(channel, responseChannel, data) {
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.FileSystemGetMessage(data)
)
])
})
.on('fs.list', function(channel, responseChannel, data){
joinChannel(responseChannel)
push.send([
channel
, wireutil.transaction(
responseChannel
, new wire.FileSystemListMessage(data)
)
])
})
}) })
.finally(function() { .finally(function() {
// Clean up all listeners and subscriptions // Clean up all listeners and subscriptions

View file

@ -2,12 +2,9 @@ module.exports.list = function(val) {
return val.split(/\s*,\s*/g).filter(Boolean) return val.split(/\s*,\s*/g).filter(Boolean)
} }
module.exports.allUnknownArgs = function(args) { module.exports.size = function(val) {
return [].slice.call(args, 0, -1).filter(Boolean) var match = /^(\d+)x(\d+)$/.exec(val)
} return match ? [+match[1], +match[2]] : undefined
module.exports.lastArg = function(args) {
return args[args.length - 1]
} }
module.exports.range = function(from, to) { module.exports.range = function(from, to) {

44
lib/util/vncauth.js Normal file
View file

@ -0,0 +1,44 @@
var crypto = require('crypto')
// See http://graphics.stanford.edu/~seander/bithacks.html#ReverseByteWith32Bits
function reverseByteBits(b) {
return (((b * 0x0802 & 0x22110) |
(b * 0x8020 & 0x88440)) * 0x10101 >> 16 & 0xFF)
}
function reverseBufferByteBits(b) {
var result = new Buffer(b.length)
for (var i = 0; i < result.length; ++i) {
result[i] = reverseByteBits(b[i])
}
return result
}
function normalizePassword(password) {
var key = new Buffer(8).fill(0)
// Make sure the key is always 8 bytes long. VNC passwords cannot be
// longer than 8 bytes. Shorter passwords are padded with zeroes.
reverseBufferByteBits(password).copy(key, 0, 0, 8)
return key
}
function encrypt(challenge, password) {
var key = normalizePassword(password)
, iv = new Buffer(0).fill(0)
// Note: do not call .final(), .update() is the one that gives us the
// desired result.
return crypto.createCipheriv('des-ecb', key, iv).update(challenge)
}
module.exports.encrypt = encrypt
function verify(response, challenge, password) {
return encrypt(challenge, password).equals(response)
}
module.exports.verify = verify

23
lib/util/zmqutil.js Normal file
View file

@ -0,0 +1,23 @@
// ISSUE-100 (https://github.com/openstf/stf/issues/100)
// In some networks TCP Connection dies if kept idle for long.
// Setting TCP_KEEPALIVE option true, to all the zmq sockets
// won't let it die
var zmq = require('zmq')
var log = require('./logger').createLogger('util:zmqutil')
module.exports.socket = function() {
var sock = zmq.socket.apply(zmq, arguments)
try {
sock.setsockopt(zmq.ZMQ_TCP_KEEPALIVE, 1)
sock.setsockopt(zmq.ZMQ_TCP_KEEPALIVE_IDLE, 300000)
}
catch (err) {
log.warn('ZeroMQ library too old, no support for TCP keepalive options')
}
return sock
}

View file

@ -17,6 +17,8 @@ enum MessageType {
PhysicalIdentifyMessage = 29; PhysicalIdentifyMessage = 29;
JoinGroupMessage = 11; JoinGroupMessage = 11;
JoinGroupByAdbFingerprintMessage = 69; JoinGroupByAdbFingerprintMessage = 69;
JoinGroupByVncAuthResponseMessage = 90;
VncAuthResponsesUpdatedMessage = 91;
AutoGroupMessage = 70; AutoGroupMessage = 70;
AdbKeysUpdatedMessage = 71; AdbKeysUpdatedMessage = 71;
KeyDownMessage = 12; KeyDownMessage = 12;
@ -72,6 +74,16 @@ enum MessageType {
AccountRemoveMessage = 55; AccountRemoveMessage = 55;
SdStatusMessage = 61; SdStatusMessage = 61;
ReverseForwardsEvent = 72; ReverseForwardsEvent = 72;
FileSystemListMessage = 81;
FileSystemGetMessage = 82;
}
message FileSystemListMessage {
required string dir = 1;
}
message FileSystemGetMessage {
required string file = 1;
} }
message Envelope { message Envelope {
@ -261,9 +273,18 @@ message JoinGroupByAdbFingerprintMessage {
optional string currentGroup = 4; optional string currentGroup = 4;
} }
message JoinGroupByVncAuthResponseMessage {
required string serial = 1;
required string response = 2;
optional string currentGroup = 4;
}
message AdbKeysUpdatedMessage { message AdbKeysUpdatedMessage {
} }
message VncAuthResponsesUpdatedMessage {
}
message LeaveGroupMessage { message LeaveGroupMessage {
required string serial = 1; required string serial = 1;
required OwnerMessage owner = 2; required OwnerMessage owner = 2;

View file

@ -1,6 +1,6 @@
{ {
"name": "stf", "name": "stf",
"version": "1.0.9", "version": "1.0.10",
"description": "Smartphone Test Farm", "description": "Smartphone Test Farm",
"keywords": [ "keywords": [
"adb", "adb",
@ -36,39 +36,41 @@
"aws-sdk": "^2.2.3", "aws-sdk": "^2.2.3",
"bluebird": "^2.9.34", "bluebird": "^2.9.34",
"body-parser": "^1.13.3", "body-parser": "^1.13.3",
"chalk": "~1.0.0", "chalk": "~1.1.1",
"commander": "^2.7.1", "commander": "^2.9.0",
"compression": "^1.5.2", "compression": "^1.5.2",
"cookie-session": "^1.2.0", "cookie-session": "^1.2.0",
"csurf": "^1.7.0", "csurf": "^1.7.0",
"debug": "^2.2.0",
"eventemitter3": "^0.1.6", "eventemitter3": "^0.1.6",
"express": "^4.13.3", "express": "^4.13.3",
"express-validator": "^2.14.0", "express-validator": "^2.17.1",
"formidable": "^1.0.17", "formidable": "^1.0.17",
"gm": "^1.17.0", "gm": "^1.17.0",
"hipchatter": "^0.2.0", "hipchatter": "^0.2.0",
"http-proxy": "^1.9.0", "http-proxy": "^1.11.2",
"in-publish": "^2.0.0", "in-publish": "^2.0.0",
"jade": "^1.9.2", "jade": "^1.9.2",
"jpeg-turbo": "^0.3.0",
"jws": "^3.1.0", "jws": "^3.1.0",
"ldapjs": "git+https://github.com/mcavage/node-ldapjs.git#acc1ca8f4314fd9d67561feabc8ce4c235076a5e", "ldapjs": "git+https://github.com/mcavage/node-ldapjs.git#acc1ca8f4314fd9d67561feabc8ce4c235076a5e",
"lodash": "^3.10.1", "lodash": "^3.10.1",
"markdown-serve": "^0.3.2", "markdown-serve": "^0.3.2",
"mime": "^1.3.4", "mime": "^1.3.4",
"minimatch": "^2.0.10", "minimatch": "^3.0.0",
"my-local-ip": "^1.0.0", "my-local-ip": "^1.0.0",
"node-uuid": "^1.4.3", "node-uuid": "^1.4.3",
"passport": "^0.2.1", "passport": "^0.3.0",
"passport-oauth2": "^1.1.2", "passport-oauth2": "^1.1.2",
"protobufjs": "^3.8.2", "protobufjs": "^3.8.2",
"proxy-addr": "^1.0.7", "proxy-addr": "^1.0.7",
"request": "^2.60.0", "request": "^2.65.0",
"request-progress": "^0.3.1", "request-progress": "^0.3.1",
"rethinkdb": "^2.0.2", "rethinkdb": "^2.0.2",
"semver": "^5.0.1", "semver": "^5.0.1",
"serve-favicon": "^2.2.0", "serve-favicon": "^2.2.0",
"serve-static": "^1.9.2", "serve-static": "^1.9.2",
"socket.io": "1.3.6", "socket.io": "1.3.7",
"split": "^1.0.0", "split": "^1.0.0",
"stf-appstore-db": "^1.0.0", "stf-appstore-db": "^1.0.0",
"stf-browser-db": "^1.0.2", "stf-browser-db": "^1.0.2",
@ -77,68 +79,67 @@
"stf-wiki": "^1.0.0", "stf-wiki": "^1.0.0",
"temp": "^0.8.1", "temp": "^0.8.1",
"transliteration": "^0.1.1", "transliteration": "^0.1.1",
"ws": "^0.7.2", "ws": "^0.8.0",
"zmq": "^2.12.0" "zmq": "^2.13.0"
}, },
"devDependencies": { "devDependencies": {
"async": "^1.4.0",
"aws-sdk": "^2.1.46", "aws-sdk": "^2.1.46",
"bower": "^1.3.12", "async": "^1.4.2",
"bower": "^1.6.3",
"chai": "^3.2.0", "chai": "^3.2.0",
"css-loader": "^0.14.0", "css-loader": "^0.20.1",
"del": "^1.2.0", "del": "^2.0.1",
"event-stream": "^3.3.0", "event-stream": "^3.3.2",
"exports-loader": "^0.6.2", "exports-loader": "^0.6.2",
"extract-text-webpack-plugin": "^0.8.2", "extract-text-webpack-plugin": "^0.8.2",
"file-loader": "^0.8.1", "file-loader": "^0.8.1",
"gulp": "^3.8.11", "gulp": "^3.8.11",
"gulp-angular-gettext": "^2.1.0", "gulp-angular-gettext": "^2.1.0",
"gulp-jade": "^1.0.0", "gulp-jade": "^1.0.0",
"gulp-jscs": "^2.0.0", "gulp-jscs": "^3.0.0",
"gulp-jshint": "^1.11.2", "gulp-jshint": "^1.11.2",
"gulp-jsonlint": "^1.0.2", "gulp-jsonlint": "^1.0.2",
"gulp-protractor": "^1.0.0", "gulp-protractor": "^1.0.0",
"gulp-run": "^1.6.10", "gulp-run": "^1.6.10",
"gulp-standard": "^4.5.3", "gulp-standard": "^5.1.0",
"gulp-util": "^3.0.4", "gulp-util": "^3.0.4",
"html-loader": "^0.3.0", "html-loader": "^0.3.0",
"imports-loader": "^0.6.3", "imports-loader": "^0.6.5",
"jasmine-core": "^2.3.4", "jasmine-core": "^2.3.4",
"jasmine-reporters": "^2.0.5", "jasmine-reporters": "^2.0.5",
"jshint": "^2.6.3", "jshint": "^2.6.3",
"jshint-loader": "^0.8.3", "jshint-loader": "^0.8.3",
"jshint-stylish": "^2.0.0", "jshint-stylish": "^2.0.0",
"json-loader": "^0.5.1", "json-loader": "^0.5.1",
"karma": "^0.13.3", "karma": "^0.13.11",
"karma-chrome-launcher": "^0.2.0", "karma-chrome-launcher": "^0.2.0",
"karma-firefox-launcher": "^0.1.4", "karma-firefox-launcher": "^0.1.4",
"karma-ie-launcher": "^0.2.0", "karma-ie-launcher": "^0.2.0",
"karma-jasmine": "^0.3.5", "karma-jasmine": "^0.3.5",
"karma-junit-reporter": "^0.3.3", "karma-junit-reporter": "^0.3.4",
"karma-opera-launcher": "^0.2.0", "karma-opera-launcher": "^0.3.0",
"karma-phantomjs-launcher": "^0.2.0", "karma-phantomjs-launcher": "^0.2.1",
"karma-safari-launcher": "^0.1.1", "karma-safari-launcher": "^0.1.1",
"karma-webpack": "^1.6.0", "karma-webpack": "^1.6.0",
"less": "^2.4.0", "less": "^2.4.0",
"less-loader": "^2.1.0", "less-loader": "^2.1.0",
"ng-annotate-webpack-plugin": "^0.1.2", "memory-fs": "^0.2.0",
"node-libs-browser": "^0.5.2", "node-libs-browser": "^0.5.2",
"node-sass": "^3.2.0", "node-sass": "^3.3.3",
"phantomjs": "^1.9.17", "phantomjs": "^1.9.18",
"protractor": "^2.0.0", "protractor": "^2.5.1",
"protractor-html-screenshot-reporter": "0.0.21", "protractor-html-screenshot-reporter": "0.0.21",
"raw-loader": "^0.5.1", "raw-loader": "^0.5.1",
"run-sequence": "^1.1.2", "sass-loader": "^3.0.0",
"sass-loader": "^1.0.4",
"script-loader": "^0.6.1", "script-loader": "^0.6.1",
"sinon": "^1.14.1", "sinon": "^1.16.1",
"sinon-chai": "^2.7.0", "sinon-chai": "^2.7.0",
"socket.io-client": "1.3.6", "socket.io-client": "1.3.7",
"style-loader": "^0.12.3", "style-loader": "^0.12.3",
"template-html-loader": "^0.0.3", "template-html-loader": "^0.0.3",
"url-loader": "^0.5.5", "url-loader": "^0.5.5",
"webpack": "^1.10.5", "webpack": "^1.12.2",
"webpack-dev-server": "^1.7.0" "webpack-dev-server": "^1.12.1"
}, },
"engines": { "engines": {
"node": ">= 0.10" "node": ">= 0.10"

View file

@ -40,10 +40,3 @@ div[angular-packery]:after {
width: 100%; width: 100%;
} }
} }
.packery-item.is-dragging,
.packery-item.is-positioning-post-drag {
/*border-color: red;*/
/*background: #09F;*/
/*z-index: 2;*/
}

View file

@ -2,7 +2,7 @@ module.exports = function basicModeDirective($rootScope, BrowserInfo) {
return { return {
restrict: 'AE', restrict: 'AE',
link: function (scope, element) { link: function (scope, element) {
$rootScope.basicMode = !!BrowserInfo.mobile // CHECK: use .mobile instead of .small $rootScope.basicMode = !!BrowserInfo.mobile
if ($rootScope.basicMode) { if ($rootScope.basicMode) {
element.addClass('basic-mode') element.addClass('basic-mode')
} }

View file

@ -1,7 +1,3 @@
.basic-mode {
/*background: red;*/
}
.basic-mode .devices-icon-view { .basic-mode .devices-icon-view {
padding: 0; padding: 0;
} }
@ -10,11 +6,6 @@
margin: 3px; margin: 3px;
} }
.basic-mode .stf-vnc-bottom .btn-lg {
/*padding: 5px;*/
/*font-size: 12px;*/
}
.basic-mode .stf-vnc-bottom .btn-primary:hover, .basic-mode .stf-vnc-bottom .btn-primary:hover,
.basic-mode .stf-vnc-bottom .btn-primary.active { .basic-mode .stf-vnc-bottom .btn-primary.active {
background: #007aff; background: #007aff;
@ -27,8 +18,6 @@
.basic-mode .basic-remote-control { .basic-mode .basic-remote-control {
width: 100%; width: 100%;
/*width: 320px;*/
/*height: 485px;*/
} }
.basic-mode .stf-device-list .device-search { .basic-mode .stf-device-list .device-search {

View file

@ -29,8 +29,6 @@ module.exports = function BrowserInfoServiceFactory() {
var windowWidth = window.screen.width < window.outerWidth ? var windowWidth = window.screen.width < window.outerWidth ?
window.screen.width : window.outerWidth window.screen.width : window.outerWidth
return windowWidth < 800 return windowWidth < 800
// return !!(window.matchMedia &&
// window.matchMedia('only screen and (max-width: 760px)').matches)
}) })
addTest('mobile', function () { addTest('mobile', function () {
@ -59,38 +57,6 @@ module.exports = function BrowserInfoServiceFactory() {
addTest('ua', navigator.userAgent) addTest('ua', navigator.userAgent)
//function hasEvent() {
// return (function (undefined) {
// function isEventSupportedInner(eventName, element) {
// var isSupported
// if (!eventName) {
// return false
// }
// if (!element || typeof element === 'string') {
// element = createElement(element || 'div')
// }
// eventName = 'on' + eventName
// isSupported = eventName in element
// return isSupported
// }
//
// return isEventSupportedInner
// })()
//}
// var domPrefixes = 'Webkit Moz O ms'.toLowerCase().split(' ')
// addTest('pointerevents', function () {
// var bool = false
// var i = domPrefixes.length
// bool = hasEvent('pointerdown')
// while (i-- && !bool) {
// if (hasEvent(domPrefixes[i] + 'pointerdown')) {
// bool = true
// }
// }
// return bool
// })
addTest('devicemotion', 'DeviceMotionEvent' in window) addTest('devicemotion', 'DeviceMotionEvent' in window)
addTest('deviceorientation', 'DeviceOrientationEvent' in window) addTest('deviceorientation', 'DeviceOrientationEvent' in window)

View file

@ -1,11 +1,11 @@
describe('BrowserInfo', function() { describe('BrowserInfo', function() {
beforeEach(angular.mock.module(require('./').name)); beforeEach(angular.mock.module(require('./').name))
it('should ...', inject(function(BrowserInfo) { it('should ...', inject(function() {
//expect(BrowserInfo.doSomething()).toEqual('something'); //expect(BrowserInfo.doSomething()).toEqual('something');
})); }))
}) })

View file

@ -1,3 +1,2 @@
div.stf-badge-icon div.stf-badge-icon
//i.fa.fa-warning.stf-badge-icon-warning(popover='I appeared on mouse enter!', popover-placement='bottom', popover-trigger='mouseenter')
i.fa.fa-warning.stf-badge-icon-warning(tooltip-placement='bottom', tooltip='An error has ocurred') i.fa.fa-warning.stf-badge-icon-warning(tooltip-placement='bottom', tooltip='An error has ocurred')

View file

@ -3,8 +3,6 @@ module.exports = function counterDirective($timeout) {
replace: false, replace: false,
scope: true, scope: true,
link: function (scope, element, attrs) { link: function (scope, element, attrs) {
// TODO: use $$rAF later
var el = element[0] var el = element[0]
var num, refreshInterval, duration, steps, step, countTo, increment var num, refreshInterval, duration, steps, step, countTo, increment

View file

@ -1,5 +1,3 @@
require('./help-icon.css')
module.exports = function clearButtonDirective() { module.exports = function clearButtonDirective() {
return { return {
restrict: 'EA', restrict: 'EA',

View file

@ -8,7 +8,6 @@ module.exports = angular.module('stf/common-ui', [
require('./notifications').name, require('./notifications').name,
require('./ng-enter').name, require('./ng-enter').name,
require('./tooltips').name, require('./tooltips').name,
//require('./tree').name,
require('./modals').name, require('./modals').name,
require('./include-cached').name, require('./include-cached').name,
require('./text-focus-select').name, require('./text-focus-select').name,

View file

@ -2,7 +2,7 @@ describe('FatalMessageService', function() {
beforeEach(angular.mock.module(require('./').name)); beforeEach(angular.mock.module(require('./').name));
it('should ...', inject(function(FatalMessageService) { it('should ...', inject(function() {
//expect(FatalMessageService.doSomething()).toEqual('something'); //expect(FatalMessageService.doSomething()).toEqual('something');

View file

@ -1,5 +1,4 @@
module.exports = angular.module('stf.add-adb-key-modal', [ module.exports = angular.module('stf.add-adb-key-modal', [
require('stf/common-ui/modals/common').name, require('stf/common-ui/modals/common').name
//require('stf/keys/add-adb-key').name
]) ])
.factory('AddAdbKeyModalService', require('./add-adb-key-modal-service')) .factory('AddAdbKeyModalService', require('./add-adb-key-modal-service'))

View file

@ -1,5 +1,3 @@
//require('angular-dialog-service/dialogs')
//require('angular-dialog-service/dialogs.css')
require('./modals.css') require('./modals.css')
module.exports = angular.module('stf.modals.common', [ module.exports = angular.module('stf.modals.common', [

View file

@ -50,7 +50,6 @@
.modal-size-80p .modal-dialog { .modal-size-80p .modal-dialog {
width: 80%; width: 80%;
height: 100%; height: 100%;
/*max-height: 800px;*/
} }
.modal-size-80p .modal-body { .modal-size-80p .modal-body {

View file

@ -19,7 +19,6 @@ module.exports = function ServiceFactory($modal, $sce) {
var modalInstance = $modal.open({ var modalInstance = $modal.open({
template: require('./external-url-modal.jade'), template: require('./external-url-modal.jade'),
controller: ModalInstanceCtrl, controller: ModalInstanceCtrl,
// size: 'lg',
windowClass: 'modal-size-80p', windowClass: 'modal-size-80p',
resolve: { resolve: {
title: function() { title: function() {

View file

@ -2,7 +2,7 @@ describe('ExternalUrlModalService', function() {
beforeEach(angular.mock.module(require('./').name)); beforeEach(angular.mock.module(require('./').name));
it('should ...', inject(function(ExternalUrlModalService) { it('should ...', inject(function() {
//expect(FatalMessageService.doSomething()).toEqual('something'); //expect(FatalMessageService.doSomething()).toEqual('something');

View file

@ -10,7 +10,6 @@ module.exports =
$scope.ok = function () { $scope.ok = function () {
$modalInstance.close(true) $modalInstance.close(true)
$route.reload() $route.reload()
//$location.path('/control/' + device.serial)
} }
function update() { function update() {

View file

@ -2,7 +2,7 @@ describe('FatalMessageService', function() {
beforeEach(angular.mock.module(require('./').name)); beforeEach(angular.mock.module(require('./').name));
it('should ...', inject(function(FatalMessageService) { it('should ...', inject(function() {
//expect(FatalMessageService.doSomething()).toEqual('something'); //expect(FatalMessageService.doSomething()).toEqual('something');

View file

@ -18,7 +18,7 @@ module.exports = function ServiceFactory($modal) {
var modalInstance = $modal.open({ var modalInstance = $modal.open({
template: require('./lightbox-image.jade'), template: require('./lightbox-image.jade'),
controller: ModalInstanceCtrl, controller: ModalInstanceCtrl,
windowClass: 'modal-size-xl', // TODO: Make width dynamic adjusting windowClass: 'modal-size-xl',
resolve: { resolve: {
title: function() { title: function() {
return title return title

View file

@ -2,7 +2,7 @@ describe('LightboxImageService', function() {
beforeEach(angular.mock.module(require('./').name)); beforeEach(angular.mock.module(require('./').name));
it('should ...', inject(function(LightboxImageService) { it('should ...', inject(function() {
//expect(XLightboxImageService.doSomething()).toEqual('something'); //expect(XLightboxImageService.doSomething()).toEqual('something');

View file

@ -7,4 +7,3 @@
.modal-body .modal-body
img(ng-if='imageUrl', ng-src='{{imageUrl}}') img(ng-if='imageUrl', ng-src='{{imageUrl}}')
nothing-to-show(message='{{"No photo available"|translate}}', icon='fa-picture-o', ng-if='!imageUrl') nothing-to-show(message='{{"No photo available"|translate}}', icon='fa-picture-o', ng-if='!imageUrl')
// TODO: replace !imageUrl here with a image-not-available='imageIsNotPresent = true' directive

View file

@ -2,7 +2,7 @@ describe('SocketDisconnectedService', function() {
beforeEach(angular.mock.module(require('./index').name)) beforeEach(angular.mock.module(require('./index').name))
it('should ...', inject(function(SocketDisconnectedService) { it('should ...', inject(function() {
//expect(SocketDisconnectedService.doSomething()).toEqual('something') //expect(SocketDisconnectedService.doSomething()).toEqual('something')

View file

@ -3,7 +3,7 @@ describe('VersionUpdateService', function() {
beforeEach(angular.mock.module(require('ui-bootstrap').name)); beforeEach(angular.mock.module(require('ui-bootstrap').name));
beforeEach(angular.mock.module(require('./').name)); beforeEach(angular.mock.module(require('./').name));
it('should ...', inject(function(VersionUpdateService) { it('should ...', inject(function() {
//expect(VersionUpdateService.doSomething()).toEqual('something'); //expect(VersionUpdateService.doSomething()).toEqual('something');

View file

@ -1,6 +0,0 @@
require('./native-autocomplete.css')
module.exports = angular.module('stf.native-autocomplete', [
])
.directive('nativeAutocomplete', require('./native-autocomplete-directive'))

View file

@ -1,13 +0,0 @@
module.exports = function nativeAutocompleteDirective() {
return {
restrict: 'E',
replace: true,
scope: {
},
template: require('./native-autocomplete.jade'),
link: function (scope, element, attrs) {
}
}
}

View file

@ -1,23 +0,0 @@
describe('nativeAutocomplete', function () {
beforeEach(angular.mock.module(require('./').name));
var scope, compile;
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
compile = $compile;
}));
it('should ...', function () {
/*
To test your directive, you need to create some html that would use your directive,
send that through compile() then compare the results.
var element = compile('<div native-autocomplete name="name">hi</div>')(scope);
expect(element.text()).toBe('hello, world');
*/
});
});

View file

@ -1,3 +0,0 @@
.stf-native-autocomplete {
}

View file

@ -1 +0,0 @@
div.stf-native-autocomplete

View file

@ -1,7 +0,0 @@
input(
type='text',
native-autocomplete,
ng-model='text',
typeahead='["text1", "text2"]',
history='20'
)

View file

@ -3,7 +3,6 @@
top: 60px; top: 60px;
right: 15px; right: 15px;
float: right; float: right;
/*width: 320px;*/
z-index: 9999; z-index: 9999;
} }

View file

@ -9,8 +9,6 @@ module.exports = function refreshPageDirective($window) {
scope.reloadWindow = function () { scope.reloadWindow = function () {
$window.location.reload() $window.location.reload()
} }
// TODO: reload with $route.reload()
} }
} }
} }

View file

@ -1,6 +1,5 @@
require('./table.css') require('./table.css')
require('script!ng-table/dist/ng-table') require('script!ng-table/dist/ng-table')
//require('ng-table/ng-table.css')
module.exports = angular.module('stf/common-ui/table', [ module.exports = angular.module('stf/common-ui/table', [
'ngTable' 'ngTable'

View file

@ -1,5 +1,4 @@
.ng-table th { .ng-table th {
/*text-align: center;*/
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;
-khtml-user-select: none; -khtml-user-select: none;

View file

@ -1,8 +0,0 @@
//require('angular-tree-control/css/tree-control.css')
//require('./tree.css')
//require('angular-tree-control')
module.exports = angular.module('stf.tree', [
// 'treeControl'
])
.factory('TreeService', require('./tree-service'))

View file

@ -1,52 +0,0 @@
module.exports = function () {
var treeService = {}
var tree = [
{name: 'glossary', children: [
{name: 'title'}
]}
]
function createTreeFromJSON(tree, json) {
(function updateRecursive(item) {
if (item.iconSrc) {
item.iconSrcFullpath = 'some value..';
}
_.each(item.items, updateRecursive);
})(json);
}
$scope.treeOptions = {
nodeChildren: 'children',
dirSelectable: true,
injectClasses: {
ul: "a1",
li: "a2",
liSelected: "a7",
iExpanded: "a3",
iCollapsed: "a4",
iLeaf: "a5",
label: "a6",
labelSelected: "a8"
}
}
$scope.treeData = [
{ "name": "Joe", "age": "21", "children": [
{ "name": "Smith", "age": "42", "children": [] },
{ "name": "Gary", "age": "21", "children": [
{ "name": "Jenifer", "age": "23", "children": [
{ "name": "Dani", "age": "32", "children": [] },
{ "name": "Max", "age": "34", "children": [] }
]}
]}
]},
{ "name": "Albert", "age": "33", "children": [] },
{ "name": "Ron", "age": "29", "children": [] }
];
return treeService
}

View file

@ -1,3 +0,0 @@
.stf-tree {
}

View file

@ -157,9 +157,10 @@ module.exports = function ControlServiceFactory(
return sendTwoWay('device.reboot') return sendTwoWay('device.reboot')
} }
this.rotate = function(rotation) { this.rotate = function(rotation, lock) {
return sendOneWay('display.rotate', { return sendOneWay('display.rotate', {
rotation: rotation rotation: rotation,
lock: lock
}) })
} }
@ -224,6 +225,18 @@ module.exports = function ControlServiceFactory(
return sendTwoWay('screen.capture') return sendTwoWay('screen.capture')
} }
this.fsretrieve = function(file){
return sendTwoWay('fs.retrieve', {
file: file,
})
}
this.fslist = function(dir){
return sendTwoWay('fs.list', {
dir: dir,
})
}
this.checkAccount = function(type, account) { this.checkAccount = function(type, account) {
return sendTwoWay('account.check', { return sendTwoWay('account.check', {
type: type type: type

View file

@ -1,11 +1,11 @@
describe('FilterStringService', function() { describe('FilterStringService', function() {
beforeEach(angular.mock.module(require('./').name)); beforeEach(angular.mock.module(require('./').name))
it('should ...', inject(function(FilterStringService) { it('should ...', inject(function() {
//expect(FilterStringService.doSomething()).toEqual('something'); //expect(FilterStringService.doSomething()).toEqual('something')
})); }))
}) })

View file

@ -2,9 +2,9 @@ describe('install', function() {
beforeEach(angular.mock.module(require('./').name)) beforeEach(angular.mock.module(require('./').name))
it('should ...', inject(function($filter) { it('should ...', inject(function() {
var filter = $filter('installError') //var filter = $filter('installError')
//expect(filter('input')).toEqual('output') //expect(filter('input')).toEqual('output')

View file

@ -1,11 +1,11 @@
describe('LogcatService', function() { describe('LogcatService', function() {
beforeEach(angular.mock.module(require('./').name)); beforeEach(angular.mock.module(require('./').name))
it('should ...', inject(function(LogcatService) { it('should ...', inject(function() {
//expect(LogcatService.doSomething()).toEqual('something'); //expect(LogcatService.doSomething()).toEqual('something')
})); }))
}) })

View file

@ -2,7 +2,7 @@ describe('NativeUrlService', function() {
beforeEach(angular.mock.module(require('./').name)) beforeEach(angular.mock.module(require('./').name))
it('should ...', inject(function(NativeUrlService) { it('should ...', inject(function() {
//expect(NativeUrlService.doSomething()).toEqual('something') //expect(NativeUrlService.doSomething()).toEqual('something')

View file

@ -1,11 +1,10 @@
describe('PortForwardingService', function() { describe('PortForwardingService', function() {
beforeEach(angular.mock.module(require('./').name)); beforeEach(angular.mock.module(require('./').name))
it('should ...', inject(function(PortForwardingService) { it('should ...', inject(function() {
expect(1).toBe(1) //expect(PortForwardingService.doSomething()).toEqual('something')
//expect(PortForwardingService.doSomething()).toEqual('something');
})); }))
}) })

View file

@ -1,11 +1,11 @@
describe('ScopedHotkeysService', function() { describe('ScopedHotkeysService', function() {
beforeEach(angular.mock.module(require('./').name)); beforeEach(angular.mock.module(require('./').name))
it('should ...', inject(function(ScopedHotkeysService) { it('should ...', inject(function() {
//expect(ScopedHotkeysService.doSomething()).toEqual('something'); //expect(ScopedHotkeysService.doSomething()).toEqual('something')
})); }))
}) })

View file

@ -7,6 +7,10 @@ var frame = {
current: 0 current: 0
} }
function FastImageRender () {
}
var imageRender = new FastImageRender( var imageRender = new FastImageRender(
canvasElement canvasElement
, { , {

View file

@ -3,8 +3,7 @@ module.exports = function screenKeyboardDirective() {
restrict: 'E', restrict: 'E',
template: require('./screen-keyboard.jade'), template: require('./screen-keyboard.jade'),
link: function (scope, element) { link: function (scope, element) {
var input = element.find('input') element.find('input')
} }
} }

View file

@ -1,7 +1,7 @@
module.exports = function screenTouchDirective() { module.exports = function screenTouchDirective() {
return { return {
restrict: 'A', restrict: 'A',
link: function (scope, element, attrs) { link: function () {
} }
} }

View file

@ -5,7 +5,7 @@ module.exports = function textHistoryDirective() {
return { return {
restrict: 'A', restrict: 'A',
template: '', template: '',
link: function (scope, element, attrs) { link: function () {
} }
} }

View file

@ -2,7 +2,7 @@ describe('TimelineService', function() {
beforeEach(angular.mock.module(require('./').name)); beforeEach(angular.mock.module(require('./').name));
it('should ...', inject(function(TimelineService) { it('should ...', inject(function() {
//expect(TimelineService.doSomething()).toEqual('something'); //expect(TimelineService.doSomething()).toEqual('something');

View file

@ -2,9 +2,9 @@ describe('upload', function() {
beforeEach(angular.mock.module(require('./').name)) beforeEach(angular.mock.module(require('./').name))
it('should ...', inject(function($filter) { it('should ...', inject(function() {
var filter = $filter('uploadError') //var filter = $filter('uploadError')
//expect(filter('input')).toEqual('output') //expect(filter('input')).toEqual('output')

View file

@ -25,13 +25,6 @@ module.exports = function ActivityCtrl($scope, gettext, TimelineService) {
serial: $scope.device.serial serial: $scope.device.serial
}) })
// $scope.timeline.push({
// title: title,
// message: message,
// serial: angular.copy($scope.device.serial),
// time: Date.now()
// })
} }

View file

@ -1,7 +1,4 @@
.widget-container.scrollableX.messages.stf-activity(ng-controller='ActivityCtrl') .widget-container.scrollableX.messages.stf-activity(ng-controller='ActivityCtrl')
//.heading
i.fa
span(translate) Activity
.widget-content.padded .widget-content.padded
ul(ng-repeat='line in timeline.lines') ul(ng-repeat='line in timeline.lines')
@ -24,27 +21,3 @@
div div
refresh-page refresh-page
//a(href='/')
.status.unread
//i.fa.fa-exclamation-triangle.fa-2x.activity-icon
h2.activity-title WebSocket Disconnected
span.activity-date 2014/04/30 18:33:22
p.pull-left Socket connection was lost, try again reloading the page.
.activity-buttons.pull-right
refresh-page
.clearfix
//li.list-group-item
.reviewer-info
i.fa.fa-mobile.fa-2x.activity-icon
h5.activity-title 'Nexus 5' Disconnected
em.activity-date.pull-right 2014/04/30 15:33:22
.review-text
p.pull-left Device was disconnected because it timed out.
.activity-buttons.pull-right
button.btn.btn-sm.btn-primary-outline(ng-click='')
i.fa.fa-refresh
span(translate) Reconnect device

View file

@ -5,5 +5,9 @@
div(ng-include='"control-panes/advanced/input/input.jade"') div(ng-include='"control-panes/advanced/input/input.jade"')
.col-md-6 .col-md-6
div(ng-include='"control-panes/advanced/port-forwarding/port-forwarding.jade"') div(ng-include='"control-panes/advanced/port-forwarding/port-forwarding.jade"')
.row
.col-md-6
div(ng-include='"control-panes/advanced/vnc/vnc.jade"')
.col-md-6 .col-md-6
div(ng-include='"control-panes/advanced/maintenance/maintenance.jade"') div(ng-include='"control-panes/advanced/maintenance/maintenance.jade"')

View file

@ -4,6 +4,7 @@ module.exports = angular.module('stf.advanced', [
require('./input').name, require('./input').name,
// require('./run-js').name, // require('./run-js').name,
// require('./usb').name, // require('./usb').name,
require('./vnc').name,
require('./port-forwarding').name, require('./port-forwarding').name,
require('./maintenance').name require('./maintenance').name
]) ])

View file

@ -6,13 +6,13 @@
div div
h6(translate) Special Keys h6(translate) Special Keys
div.special-keys-buttons div.special-keys-buttons
button(tooltip='{{ "Power" | translate }}', ng-click='press("power")').btn.btn-danger button(tooltip='{{ "Power" | translate }}', ng-click='press("power")').btn.btn-danger.btn-xs
i.fa.fa-power-off i.fa.fa-power-off
button(tooltip='{{ "Camera" | translate }}', ng-click='press("camera")').btn.btn-primary button(tooltip='{{ "Camera" | translate }}', ng-click='press("camera")').btn.btn-primary.btn-xs
i.fa.fa-camera i.fa.fa-camera
button(tooltip='{{ "Switch Charset" | translate }}', ng-click='press("switch_charset")').btn.btn-primary.btn-info button(tooltip='{{ "Switch Charset" | translate }}', ng-click='press("switch_charset")').btn.btn-primary.btn-info.btn-xs
i.fa Aa i.fa Aa
button(tooltip='{{ "Search" | translate }}', ng-click='press("search")').btn.btn-primary button(tooltip='{{ "Search" | translate }}', ng-click='press("search")').btn.btn-primary.btn-xs
i.fa.fa-search i.fa.fa-search
h6(translate) Volume h6(translate) Volume
@ -38,58 +38,8 @@
i.fa.fa-step-forward i.fa.fa-step-forward
button(tooltip='{{ "Fast Forward" | translate }}', ng-click='press("media_fast_forward")').btn.btn-primary.btn-xs button(tooltip='{{ "Fast Forward" | translate }}', ng-click='press("media_fast_forward")').btn.btn-primary.btn-xs
i.fa.fa-fast-forward i.fa.fa-fast-forward
//h6 Physical Media //h6 D-pad
//.btn-group //table.special-keys-dpad-buttons
button(tooltip='{{ "Play" | translate }}', ng-click='press("KEYCODE_MEDIA_PLAY")').btn.btn-primary.btn-xs
i.fa.fa-play
button(tooltip='{{ "Pause" | translate }}', ng-click='press("KEYCODE_MEDIA_PAUSE")').btn.btn-primary.btn-xs
i.fa.fa-pause
button(tooltip='{{ "Close" | translate }}', ng-click='press("KEYCODE_MEDIA_CLOSE")').btn.btn-primary.btn-xs
i.fa.fa-sign-out
button(tooltip='{{ "Eject" | translate }}', ng-click='press("KEYCODE_MEDIA_EJECT")').btn.btn-primary.btn-xs
i.fa.fa-eject
button(tooltip='{{ "Record" | translate }}', ng-click='press("KEYCODE_MEDIA_RECORD")').btn.btn-primary.btn-xs
i.fa.fa-circle
//h6(translate) Other Keys
//div.special-other-keys-buttons
button(ng-click='press("KEYCODE_APP_SWITCH")').btn.btn-default.btn-xs
i.fa App Switch
button(ng-click='press("KEYCODE_MANNER_MODE")').btn.btn-default.btn-xs
i.fa Manner Mode
button(ng-click='press("KEYCODE_3D_MODE")').btn.btn-default.btn-xs
i.fa 3D Mode
button(ng-click='press("KEYCODE_CONTACTS")').btn.btn-default.btn-xs
i.fa Contacts
button(ng-click='press("KEYCODE_CALENDAR")').btn.btn-default.btn-xs
i.fa Calendar
button(ng-click='press("KEYCODE_MUSIC")').btn.btn-default.btn-xs
i.fa Music
button(ng-click='press("KEYCODE_CALCULATOR")').btn.btn-default.btn-xs
i.fa Calculator
button(ng-click='press("KEYCODE_ZENKAKU_HANKAKU")').btn.btn-default.btn-xs
i.fa 全角/半角
button(ng-click='press("KEYCODE_EISU")').btn.btn-default.btn-xs
i.fa 英数
button(ng-click='press("KEYCODE_MUHENKAN")').btn.btn-default.btn-xs
i.fa 無変換
button(ng-click='press("KEYCODE_HENKAN")').btn.btn-default.btn-xs
i.fa 変換
button(ng-click='press("KEYCODE_KATAKANA_HIRAGANA")').btn.btn-default.btn-xs
i.fa カタかナ/ひらがな
button(ng-click='press("KEYCODE_YEN")').btn.btn-default.btn-xs
i.fa ¥
button(ng-click='press("KEYCODE_RO")').btn.btn-default.btn-xs
i.fa RO
button(ng-click='press("KEYCODE_KANA")').btn.btn-default.btn-xs
i.fa かな
button(ng-click='press("KEYCODE_ASSIST")').btn.btn-default.btn-xs
i.fa Assist
//button(tooltip='{{ "Switch Charset" | translate }}', ng-click='press(80)').btn.btn-primary
i.fa TST
//button(ng-click='press("KEYCODE_CLEAR")').btn.btn-primary.btn-sm
i.fa Clear
h6 D-pad
table.special-keys-dpad-buttons
tr tr
td td
td td

View file

@ -1,7 +1,7 @@
.widget-container.fluid-height.stf-port-forwarding(ng-controller='PortForwardingCtrl') .widget-container.fluid-height.stf-port-forwarding(ng-controller='PortForwardingCtrl')
.heading .heading
span span
stacked-icon(icon='fa-random', color='color-darkgreen') stacked-icon(icon='fa-random', color='color-orange')
span(translate, ng-click='isCollapsed = !isCollapsed').pointer Port Forwarding span(translate, ng-click='isCollapsed = !isCollapsed').pointer Port Forwarding
button.btn.pull-right.btn-sm.btn-primary-outline( button.btn.pull-right.btn-sm.btn-primary-outline(
@ -44,7 +44,3 @@
td td
button.btn.btn-sm.btn-danger-outline(ng-click='removeRow(forward)') button.btn.btn-sm.btn-danger-outline(ng-click='removeRow(forward)')
i.fa.fa-trash-o i.fa.fa-trash-o
//.checkbox
label
input(type='checkbox', value='')
span(translate) Always forward on connect

View file

@ -1,3 +1,3 @@
module.exports = function RunJsCtrl($scope) { module.exports = function RunJsCtrl() {
} }

View file

@ -3,11 +3,7 @@
.heading .heading
i.fa.fa-code i.fa.fa-code
span(translate) Run JavaScript span(translate) Run JavaScript
//form.form-inline
.btn-group .btn-group
//button(ng-disabled='true').btn.btn-sm.btn-default-outline
i.fa.fa-upload
| Load File...
script(type='text/ng-template', id='saveSnippetModal.html') script(type='text/ng-template', id='saveSnippetModal.html')
.modal-header .modal-header
h2 Save snippet h2 Save snippet
@ -29,24 +25,6 @@
li.divider li.divider
li li
a(ng-click='clearSnippets()', type='button', translate).btn-link Clear a(ng-click='clearSnippets()', type='button', translate).btn-link Clear
//span.form-inline.form-group.unselectable
.checkbox
label
input(type='checkbox', ng-model='snippet.safe')
span(tooltip='Execute code in a safe way') Safe
span
.checkbox
label
input(type='checkbox', nxg-model='snippet.evaluate')
span(tooltip='Evaluate code') Evaluate
.checkbox
label
input(type='checkbox', ng-model='snippet.async')
span(tooltip='Execute code in an async way') Async
.checkbox
label
input(type='checkbox', ng-model='snippet.scriptTag', ng-disabled='true')
span(tooltip='{{scriptTagPopover}}') Script tag
.btn-group.pull-right .btn-group.pull-right
button.btn.btn-sm.btn-primary-outline(ng-click='injectJS()', ng-disabled='!snippet.editorText') button.btn.btn-sm.btn-primary-outline(ng-click='injectJS()', ng-disabled='!snippet.editorText')
i.fa.fa-play i.fa.fa-play
@ -64,14 +42,12 @@
span {{ result.deviceName }} span {{ result.deviceName }}
td(width='30%', title="'Returns'", sortable='prettyValue') td(width='30%', title="'Returns'", sortable='prettyValue')
div(ng-show='result.isObject') div(ng-show='result.isObject')
//ace-json-viewer(ng-model='result.prettyValue')
//div(ui-ace="miniAceOptions", ng-model='result.prettyValue').stf-mini-ace-viewer
code.value-next-to-progress {{ result.prettyValue }} code.value-next-to-progress {{ result.prettyValue }}
div(ng-hide='result.isObject') div(ng-hide='result.isObject')
.value-next-to-progress {{ result.value }} .value-next-to-progress {{ result.value }}
td(width='40%', ng-show='result.isSpecialValue') td(width='40%', ng-show='result.isSpecialValue')
div(ng-show='result.isNumber') div(ng-show='result.isNumber')
//progressbar.table-progress(value='result.percentage', max='100') progressbar.table-progress(value='result.percentage', max='100')
div(ng-show='result.isObject') div(ng-show='result.isObject')
div.label.label-info Object div.label.label-info Object
div(ng-show='result.isFunction') div(ng-show='result.isFunction')
@ -84,7 +60,6 @@
div.label(style='width=100%', ng-class="{'label-success': result.value, 'label-important': !result.value}") div.label(style='width=100%', ng-class="{'label-success': result.value, 'label-important': !result.value}")
i.fa(ng-class="{'fa-check': result.value, 'fa-times-circle': !result.value }") i.fa(ng-class="{'fa-check': result.value, 'fa-times-circle': !result.value }")
span {{ result.value.toString() }} span {{ result.value.toString() }}
//span {{ result.value.toString() | capitalize }}
tab(heading='Raw') tab(heading='Raw')
pre.selectable {{results | json}} pre.selectable {{results | json}}
clear-button(ng-click='clear()', ng-disabled='!results.length') clear-button(ng-click='clear()', ng-disabled='!results.length')

View file

@ -1,3 +1,3 @@
module.exports = function UsbCtrl($scope) { module.exports = function UsbCtrl() {
} }

View file

@ -0,0 +1,12 @@
require('./vnc.css')
module.exports = angular.module('stf.vnc', [
require('gettext').name
])
.run(["$templateCache", function ($templateCache) {
$templateCache.put(
'control-panes/advanced/vnc/vnc.jade',
require('./vnc.jade')
)
}])
.controller('VNCCtrl', require('./vnc-controller'))

View file

@ -0,0 +1,11 @@
module.exports = function RemoteDebugCtrl($scope) {
$scope.vnc = {}
$scope.generateVNCLogin = function () {
$scope.vnc = {
serverHost: 'localhost'
, serverPort: '7042'
, serverPassword: '12345678'
}
}
}

Some files were not shown because too many files have changed in this diff Show more