mirror of
https://github.com/openstf/stf
synced 2025-10-04 18:29:17 +02:00
Merge branch 'master' of https://github.com/openstf/stf into s3insteadoftmp
This commit is contained in:
commit
bbbd8f688c
180 changed files with 5434 additions and 1532 deletions
14
.eslintrc
Normal file
14
.eslintrc
Normal 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"]
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
.DS_Store
|
||||
/*.tgz
|
||||
/.bowerrc
|
||||
/.dockerignore
|
||||
/.editorconfig
|
||||
/.env
|
||||
/.gitignore
|
||||
|
@ -9,11 +10,12 @@
|
|||
/.jscsrc
|
||||
/.npmignore
|
||||
/.npmrc
|
||||
/.travis.yml
|
||||
/docker
|
||||
/Dockerfile
|
||||
/bower.json
|
||||
/component.json
|
||||
/gulpfile.js
|
||||
/node_modules/
|
||||
/npm-debug.log
|
||||
/res/bower_components/
|
||||
/res/test/
|
||||
|
|
55
.travis.yml
55
.travis.yml
|
@ -1,24 +1,61 @@
|
|||
language: node_js
|
||||
language: cpp
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
sudo: false
|
||||
node_js:
|
||||
- "0.12"
|
||||
- "0.10"
|
||||
- "iojs"
|
||||
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- libzmq3-dev
|
||||
- libprotobuf-dev
|
||||
- graphicsmagick
|
||||
- rethinkdb
|
||||
script:
|
||||
- gulp build
|
||||
- g++-4.9
|
||||
- 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:
|
||||
- npm install -g bower
|
||||
- bower install
|
||||
# - rethinkdb --daemon
|
||||
|
||||
script:
|
||||
- npm test
|
||||
# - ./bin/stf local
|
||||
- gulp build
|
||||
|
||||
after_script:
|
||||
# - killall rethinkdb
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
- res/bower_components
|
||||
|
||||
notifications:
|
||||
slack: openstf:qu01BtEgttJOrGGsRxKBJwki
|
||||
|
|
39
Dockerfile
39
Dockerfile
|
@ -1,11 +1,4 @@
|
|||
FROM openstf/base:v1.0.1
|
||||
|
||||
# Add a user for the app.
|
||||
RUN useradd --system \
|
||||
--no-create-home \
|
||||
--shell /usr/sbin/nologin \
|
||||
--home-dir /app \
|
||||
stf
|
||||
FROM openstf/base:v1.0.6
|
||||
|
||||
# Sneak the stf executable into $PATH.
|
||||
ENV PATH /app/bin:$PATH
|
||||
|
@ -18,15 +11,31 @@ WORKDIR /app
|
|||
EXPOSE 3000
|
||||
|
||||
# Copy app source.
|
||||
COPY . /app/
|
||||
COPY . /tmp/build/
|
||||
|
||||
# Get the rest of the dependencies and build.
|
||||
RUN export PATH=/app/node_modules/.bin:$PATH && \
|
||||
npm install && \
|
||||
bower install --allow-root && \
|
||||
gulp build
|
||||
# Give permissions to our build user.
|
||||
RUN mkdir -p /app && \
|
||||
chown -R stf-build:stf-build /tmp/build /app
|
||||
|
||||
# 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
|
||||
|
||||
# Show help by default.
|
||||
|
|
|
@ -17,7 +17,7 @@ It is currently being used at [CyberAgent](https://www.cyberagent.co.jp/en/) to
|
|||
|
||||
* OS support
|
||||
- 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 Fire OS, CyanogenMod, and other heavily Android based distributions
|
||||
* `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
|
||||
* [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)
|
||||
- Experimental VNC support (work in progress)
|
||||
* Manage your device inventory
|
||||
- See which devices are connected, offline/unavailable (indicating a weak USB connection), unauthorized or unplugged
|
||||
- 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)
|
||||
* [ZeroMQ](http://zeromq.org/) 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
|
||||
|
||||
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:
|
||||
|
||||
```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.
|
||||
|
|
35
bower.json
35
bower.json
|
@ -2,47 +2,46 @@
|
|||
"name": "stf",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"angular": "1.4.3",
|
||||
"angular-cookies": "1.4.3",
|
||||
"angular-route": "1.4.3",
|
||||
"angular-sanitize": "1.4.3",
|
||||
"angular-animate": "1.4.3",
|
||||
"angular-touch": "1.4.3",
|
||||
"angular": "~1.4.7",
|
||||
"angular-cookies": "~1.4.7",
|
||||
"angular-route": "~1.4.7",
|
||||
"angular-sanitize": "~1.4.7",
|
||||
"angular-animate": "~1.4.7",
|
||||
"angular-touch": "~1.4.7",
|
||||
"lodash": "~3.10.1",
|
||||
"oboe": "~2.1.2",
|
||||
"ng-table": "~0.8.1",
|
||||
"ng-table": "~1.0.0-beta.7",
|
||||
"angular-gettext": "~2.1.0",
|
||||
"angular-ui-ace": "~0.2.3",
|
||||
"angular-bootstrap": "~0.13.2",
|
||||
"angular-dialog-service": "~5.2.6",
|
||||
"ng-file-upload": "~2.0.5",
|
||||
"angular-growl-v2": "JanStevens/angular-growl-2#~0.7.3",
|
||||
"underscore.string": "~3.1.1",
|
||||
"underscore.string": "~3.2.2",
|
||||
"bootstrap": "~3.3.5",
|
||||
"font-lato-2-subset": "~0.4.0",
|
||||
"packery": "~1.4.2",
|
||||
"packery": "~1.4.3",
|
||||
"draggabilly": "~1.2.4",
|
||||
"angular-elastic": "~2.5.0",
|
||||
"angular-hotkeys": "chieffancypants/angular-hotkeys#~1.4.5",
|
||||
"angular-elastic": "~2.5.1",
|
||||
"angular-hotkeys": "chieffancypants/angular-hotkeys#~1.6.0",
|
||||
"angular-borderlayout": "git://github.com/filearts/angular-borderlayout.git#7c9716aebd9260763f798561ca49d6fbfd4a5c67",
|
||||
"angular-ui-bootstrap": "~0.13.2",
|
||||
"ng-context-menu": "~1.0.1",
|
||||
"angular-ui-bootstrap": "~0.14.2",
|
||||
"ng-context-menu": "AdiDahan/ng-context-menu#~1.0.5",
|
||||
"components-font-awesome": "~4.4.0",
|
||||
"epoch": "~0.6.0",
|
||||
"ng-epoch": "~1.0.7",
|
||||
"eventEmitter": "~4.2.11",
|
||||
"eventEmitter": "~4.3.0",
|
||||
"angular-ladda": "~0.3.1",
|
||||
"d3": "~3.5.6",
|
||||
"spin.js": "~2.3.2"
|
||||
},
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"angular-mocks": "1.4.3"
|
||||
"angular-mocks": "~1.4.7"
|
||||
},
|
||||
"resolutions": {
|
||||
"angular": "1.4.3",
|
||||
"angular": "~1.4.7",
|
||||
"d3": "~3.5.5",
|
||||
"spin.js": "~2.3.2",
|
||||
"angular-bootstrap": "~0.13.2"
|
||||
"eventEmitter": "~4.3.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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-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
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
### `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
|
||||
[Unit]
|
||||
Description=RethinkDB proxy/28015
|
||||
Description=RethinkDB
|
||||
After=docker.service
|
||||
Requires=docker.service
|
||||
|
||||
|
@ -105,7 +127,39 @@ Requires=docker.service
|
|||
EnvironmentFile=/etc/environment
|
||||
TimeoutStartSec=0
|
||||
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 rm %p
|
||||
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" \
|
||||
-p 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
|
||||
```
|
||||
|
||||
|
@ -146,7 +200,7 @@ ExecStart=/usr/bin/docker run --rm \
|
|||
--name %p-%i \
|
||||
--link rethinkdb-proxy-28015:rethinkdb \
|
||||
-e "SECRET=YOUR_SESSION_SECRET_HERE" \
|
||||
-p 127.0.0.1:%i:3000 \
|
||||
-p %i:3000 \
|
||||
openstf/stf:latest \
|
||||
stf app --port 3000 \
|
||||
--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.
|
||||
|
||||
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.
|
||||
|
||||
|
@ -180,7 +234,7 @@ ExecStartPre=-/usr/bin/docker rm %p-%i
|
|||
ExecStart=/usr/bin/docker run --rm \
|
||||
--name %p-%i \
|
||||
-e "SECRET=YOUR_SESSION_SECRET_HERE" \
|
||||
-p 127.0.0.1:%i:3000 \
|
||||
-p %i:3000 \
|
||||
openstf/stf:latest \
|
||||
stf auth-mock --port 3000 \
|
||||
--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.
|
||||
|
||||
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
|
||||
[Unit]
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
@ -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.
|
||||
|
||||
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
|
||||
[Unit]
|
||||
Description=STF APK storage plugin
|
||||
|
@ -342,7 +400,7 @@ ExecStartPre=-/usr/bin/docker kill %p-%i
|
|||
ExecStartPre=-/usr/bin/docker rm %p-%i
|
||||
ExecStart=/usr/bin/docker run --rm \
|
||||
--name %p-%i \
|
||||
-p 127.0.0.1:%i:3000 \
|
||||
-p %i:3000 \
|
||||
openstf/stf:latest \
|
||||
stf storage-plugin-apk --port 3000 \
|
||||
--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.
|
||||
|
||||
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
|
||||
[Unit]
|
||||
Description=STF image storage plugin
|
||||
|
@ -370,7 +430,7 @@ ExecStartPre=-/usr/bin/docker kill %p-%i
|
|||
ExecStartPre=-/usr/bin/docker rm %p-%i
|
||||
ExecStart=/usr/bin/docker run --rm \
|
||||
--name %p-%i \
|
||||
-p 127.0.0.1:%i:3000 \
|
||||
-p %i:3000 \
|
||||
openstf/stf:latest \
|
||||
stf storage-plugin-image --port 3000 \
|
||||
--storage-url https://stf.example.org/
|
||||
|
@ -397,7 +457,7 @@ ExecStartPre=-/usr/bin/docker rm %p-%i
|
|||
ExecStart=/usr/bin/docker run --rm \
|
||||
--name %p-%i \
|
||||
-v /mnt/storage:/data \
|
||||
-p 127.0.0.1:%i:3000 \
|
||||
-p %i:3000 \
|
||||
openstf/stf:latest \
|
||||
stf storage-temp --port 3000 \
|
||||
--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.
|
||||
|
||||
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
|
||||
[Unit]
|
||||
Description=STF websocket
|
||||
|
@ -493,7 +555,7 @@ ExecStart=/usr/bin/docker run --rm \
|
|||
--name %p-%i \
|
||||
--link rethinkdb-proxy-28015:rethinkdb \
|
||||
-e "SECRET=YOUR_SESSION_SECRET_HERE" \
|
||||
-p 127.0.0.1:%i:3000 \
|
||||
-p %i:3000 \
|
||||
openstf/stf:latest \
|
||||
stf websocket --port 3000 \
|
||||
--storage-url https://stf.example.org/ \
|
||||
|
@ -688,8 +750,8 @@ http {
|
|||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location /auth/mock/ {
|
||||
proxy_pass http://stf_auth/auth/mock/;
|
||||
location /auth/ {
|
||||
proxy_pass http://stf_auth/auth/;
|
||||
}
|
||||
|
||||
location /s/image/ {
|
||||
|
|
17
doc/VNC.md
Normal file
17
doc/VNC.md
Normal 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.
|
|
@ -24,7 +24,8 @@ COPY . /app/
|
|||
RUN export PATH=/app/node_modules/.bin:$PATH && \
|
||||
npm install && \
|
||||
bower install --allow-root && \
|
||||
gulp build
|
||||
gulp build && \
|
||||
npm prune --production
|
||||
|
||||
# Switch to weak user.
|
||||
USER stf
|
||||
|
|
127
gulpfile.js
127
gulpfile.js
|
@ -1,16 +1,16 @@
|
|||
var path = require('path')
|
||||
|
||||
var gulp = require('gulp')
|
||||
var gutil = require('gulp-util')
|
||||
var jshint = require('gulp-jshint')
|
||||
var jsonlint = require('gulp-jsonlint')
|
||||
var standard = require('gulp-standard')
|
||||
var webpack = require('webpack')
|
||||
var ngAnnotatePlugin = require('ng-annotate-webpack-plugin')
|
||||
var webpackConfig = require('./webpack.config').webpack
|
||||
var webpackStatusConfig = require('./res/common/status/webpack.config')
|
||||
var gettext = require('gulp-angular-gettext')
|
||||
var jade = require('gulp-jade')
|
||||
var del = require('del')
|
||||
var runSequence = require('run-sequence').use(gulp)
|
||||
//var protractor = require('gulp-protractor')
|
||||
var protractor = require('./res/test/e2e/helpers/gulp-protractor-adv')
|
||||
var protractorConfig = './res/test/protractor.conf'
|
||||
|
@ -22,9 +22,13 @@ var run = require('gulp-run')
|
|||
|
||||
gulp.task('jshint', function () {
|
||||
return gulp.src([
|
||||
'lib/**/*.js', 'res/app/**/*.js', 'res/auth-ldap/**/*.js',
|
||||
'res/auth-mock/**/*.js', 'res/common/**/*.js', 'res/test/**/*.js',
|
||||
'*.js'
|
||||
'lib/**/*.js'
|
||||
, 'res/app/**/*.js'
|
||||
, 'res/auth-ldap/**/*.js'
|
||||
, 'res/auth-mock/**/*.js'
|
||||
, 'res/common/**/*.js'
|
||||
, 'res/test/**/*.js'
|
||||
, '*.js'
|
||||
])
|
||||
.pipe(jshint())
|
||||
.pipe(jshint.reporter('jshint-stylish'))
|
||||
|
@ -32,7 +36,11 @@ gulp.task('jshint', function () {
|
|||
|
||||
gulp.task('jsonlint', function () {
|
||||
return gulp.src([
|
||||
'.jshintrc', 'res/.jshintrc', '.bowerrc', '.yo-rc.json', '*.json'
|
||||
'.jshintrc'
|
||||
, 'res/.jshintrc'
|
||||
, '.bowerrc'
|
||||
, '.yo-rc.json'
|
||||
, '*.json'
|
||||
])
|
||||
.pipe(jsonlint())
|
||||
.pipe(jsonlint.reporter())
|
||||
|
@ -40,12 +48,16 @@ gulp.task('jsonlint', function () {
|
|||
|
||||
gulp.task('jscs', function () {
|
||||
return gulp.src([
|
||||
'lib/**/*.js', 'res/app/**/*.js', 'res/auth-ldap/**/*.js',
|
||||
'res/auth-mock/**/*.js', 'res/common/**/*.js', 'res/test/**/*.js',
|
||||
'*.js'
|
||||
'lib/**/*.js'
|
||||
, 'res/app/**/*.js'
|
||||
, 'res/auth-ldap/**/*.js'
|
||||
, 'res/auth-mock/**/*.js'
|
||||
, 'res/common/**/*.js'
|
||||
, 'res/test/**/*.js'
|
||||
, '*.js'
|
||||
])
|
||||
.pipe(jscs())
|
||||
});
|
||||
})
|
||||
|
||||
gulp.task('standard', function () {
|
||||
// Check res/app for now
|
||||
|
@ -58,27 +70,23 @@ gulp.task('standard', function () {
|
|||
|
||||
gulp.task('lint', ['jshint', 'jsonlint'])
|
||||
gulp.task('test', ['lint', 'run:checkversion'])
|
||||
|
||||
gulp.task('build', function (cb) {
|
||||
runSequence('clean', 'webpack:build', cb)
|
||||
})
|
||||
gulp.task('build', ['clean', 'webpack:build'])
|
||||
|
||||
gulp.task('run:checkversion', function () {
|
||||
gutil.log('Checking STF version...')
|
||||
|
||||
return run('./bin/stf -V').exec()
|
||||
})
|
||||
|
||||
gulp.task('karma_ci', function (done) {
|
||||
karma.start({
|
||||
configFile: __dirname + karmaConfig,
|
||||
singleRun: true
|
||||
configFile: path.join(__dirname, karmaConfig)
|
||||
, singleRun: true
|
||||
}, done)
|
||||
})
|
||||
|
||||
gulp.task('karma', function (done) {
|
||||
karma.start({
|
||||
configFile: __dirname + karmaConfig
|
||||
configFile: path.join(__dirname, karmaConfig)
|
||||
}, done)
|
||||
})
|
||||
|
||||
|
@ -88,32 +96,37 @@ if (gutil.env.multi) {
|
|||
protractorConfig = './res/test/protractor-appium.conf'
|
||||
}
|
||||
|
||||
gulp.task('webdriver-update', protractor.webdriver_update)
|
||||
gulp.task('webdriver-standalone', protractor.webdriver_standalone)
|
||||
gulp.task('webdriver-update', protractor.webdriverUpdate)
|
||||
gulp.task('webdriver-standalone', protractor.webdriverStandalone)
|
||||
gulp.task('protractor-explorer', function (callback) {
|
||||
protractor.protractor_explorer({
|
||||
protractor.protractorExplorer({
|
||||
url: require(protractorConfig).config.baseUrl
|
||||
}, callback)
|
||||
})
|
||||
|
||||
gulp.task('protractor', ['webdriver-update'], function (callback) {
|
||||
gulp.src(["./res/test/e2e/**/*.js"])
|
||||
gulp.src(['./res/test/e2e/**/*.js'])
|
||||
.pipe(protractor.protractor({
|
||||
configFile: protractorConfig,
|
||||
debug: gutil.env.debug,
|
||||
suite: gutil.env.suite
|
||||
configFile: protractorConfig
|
||||
, debug: gutil.env.debug
|
||||
, suite: gutil.env.suite
|
||||
}))
|
||||
.on('error', function (e) {
|
||||
console.log(e)
|
||||
}).on('end', callback)
|
||||
})
|
||||
.on('end', callback)
|
||||
})
|
||||
|
||||
// For piping strings
|
||||
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 () {
|
||||
this.push(new gutil.File({
|
||||
cwd: '', base: '', path: filename, contents: new Buffer(string)
|
||||
cwd: ''
|
||||
, base: ''
|
||||
, path: filename
|
||||
, contents: new Buffer(string)
|
||||
}))
|
||||
this.push(null)
|
||||
}
|
||||
|
@ -122,20 +135,14 @@ function fromString(filename, string) {
|
|||
|
||||
|
||||
// For production
|
||||
gulp.task("webpack:build", function (callback) {
|
||||
gulp.task('webpack:build', function (callback) {
|
||||
var myConfig = Object.create(webpackConfig)
|
||||
myConfig.plugins = myConfig.plugins.concat(
|
||||
new webpack.DefinePlugin({
|
||||
"process.env": {
|
||||
"NODE_ENV": JSON.stringify('production')
|
||||
'process.env': {
|
||||
'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
|
||||
|
||||
|
@ -144,7 +151,7 @@ gulp.task("webpack:build", function (callback) {
|
|||
throw new gutil.PluginError('webpack:build', err)
|
||||
}
|
||||
|
||||
gutil.log("[webpack:build]", stats.toString({
|
||||
gutil.log('[webpack:build]', stats.toString({
|
||||
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)
|
||||
myConfig.plugins = myConfig.plugins.concat(
|
||||
new webpack.DefinePlugin({
|
||||
"process.env": {
|
||||
"NODE_ENV": JSON.stringify('production')
|
||||
'process.env': {
|
||||
'NODE_ENV': JSON.stringify('production')
|
||||
}
|
||||
}),
|
||||
new webpack.optimize.DedupePlugin()
|
||||
// new ngminPlugin(),
|
||||
// new webpack.optimize.UglifyJsPlugin({mangle: false})
|
||||
})
|
||||
)
|
||||
myConfig.devtool = false
|
||||
|
||||
|
@ -176,18 +180,24 @@ gulp.task("webpack:others", function (callback) {
|
|||
throw new gutil.PluginError('webpack:others', err)
|
||||
}
|
||||
|
||||
gutil.log("[webpack:others]", stats.toString({
|
||||
gutil.log('[webpack:others]', stats.toString({
|
||||
colors: true
|
||||
}))
|
||||
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([
|
||||
'./res/**/*.jade', '!./res/bower_components/**'
|
||||
'./res/**/*.jade'
|
||||
, '!./res/bower_components/**'
|
||||
])
|
||||
.pipe(jade({
|
||||
locals: {
|
||||
|
@ -201,16 +211,18 @@ gulp.task('jade', function (cb) {
|
|||
.pipe(gulp.dest('./tmp/html/'))
|
||||
})
|
||||
|
||||
gulp.task('translate:extract', ['jade'], function (cb) {
|
||||
gulp.task('translate:extract', ['jade'], function () {
|
||||
return gulp.src([
|
||||
'./tmp/html/**/*.html', './res/**/*.js', '!./res/bower_components/**',
|
||||
'!./res/build/**'
|
||||
'./tmp/html/**/*.html'
|
||||
, './res/**/*.js'
|
||||
, '!./res/bower_components/**'
|
||||
, '!./res/build/**'
|
||||
])
|
||||
.pipe(gettext.extract('stf.pot'))
|
||||
.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')
|
||||
.pipe(gettext.compile({
|
||||
format: 'json'
|
||||
|
@ -218,21 +230,16 @@ gulp.task('translate:compile', ['translate:pull'], function (cb) {
|
|||
.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...')
|
||||
|
||||
return run('tx push -s').exec()
|
||||
})
|
||||
|
||||
gulp.task('translate:pull', ['translate:push'], function () {
|
||||
gulp.task('translate:pull', function () {
|
||||
gutil.log('Pulling translations from Transifex...')
|
||||
|
||||
return run('tx pull').exec()
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
gulp.task('clean', function (cb) {
|
||||
del(['./tmp', './res/build'], cb)
|
||||
})
|
||||
|
|
49
lib/cli.js
49
lib/cli.js
|
@ -15,7 +15,7 @@ program
|
|||
.version(pkg.version)
|
||||
|
||||
program
|
||||
.command('provider [serial..]')
|
||||
.command('provider [serial...]')
|
||||
.description('start provider')
|
||||
.option('-s, --connect-sub <endpoint>'
|
||||
, 'sub endpoint'
|
||||
|
@ -68,12 +68,15 @@ program
|
|||
, 'adb connect URL pattern'
|
||||
, String
|
||||
, '${publicIp}:${publicPort}')
|
||||
.option('--vnc-initial-size <size>'
|
||||
, 'initial VNC size'
|
||||
, cliutil.size
|
||||
, [600, 800])
|
||||
.option('--mute-master'
|
||||
, 'whether to mute master volume when devices are being used')
|
||||
.action(function() {
|
||||
var serials = cliutil.allUnknownArgs(arguments)
|
||||
, options = cliutil.lastArg(arguments)
|
||||
|
||||
.option('--lock-rotation'
|
||||
, 'whether to lock rotation when devices are being used')
|
||||
.action(function(serials, options) {
|
||||
if (!options.connectSub) {
|
||||
this.missingArgument('--connect-sub')
|
||||
}
|
||||
|
@ -101,6 +104,7 @@ program
|
|||
, '--connect-push', options.connectPush.join(',')
|
||||
, '--screen-port', ports.shift()
|
||||
, '--connect-port', ports.shift()
|
||||
, '--vnc-port', ports.shift()
|
||||
, '--public-ip', options.publicIp
|
||||
, '--group-timeout', options.groupTimeout
|
||||
, '--storage-url', options.storageUrl
|
||||
|
@ -109,8 +113,10 @@ program
|
|||
, '--screen-ws-url-pattern', options.screenWsUrlPattern
|
||||
, '--connect-url-pattern', options.connectUrlPattern
|
||||
, '--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: {
|
||||
sub: options.connectSub
|
||||
|
@ -139,6 +145,13 @@ program
|
|||
.option('--connect-port <port>'
|
||||
, 'port allocated to adb connect'
|
||||
, 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>'
|
||||
, 'adb connect URL pattern'
|
||||
, String
|
||||
|
@ -172,6 +185,8 @@ program
|
|||
, 10000)
|
||||
.option('--mute-master'
|
||||
, '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) {
|
||||
if (!options.connectSub) {
|
||||
this.missingArgument('--connect-sub')
|
||||
|
@ -188,6 +203,9 @@ program
|
|||
if (!options.connectPort) {
|
||||
this.missingArgument('--connect-port')
|
||||
}
|
||||
if (!options.vncPort) {
|
||||
this.missingArgument('--vnc-port')
|
||||
}
|
||||
if (!options.storageUrl) {
|
||||
this.missingArgument('--storage-url')
|
||||
}
|
||||
|
@ -208,8 +226,11 @@ program
|
|||
, screenPort: options.screenPort
|
||||
, connectUrlPattern: options.connectUrlPattern
|
||||
, connectPort: options.connectPort
|
||||
, vncPort: options.vncPort
|
||||
, vncInitialSize: options.vncInitialSize
|
||||
, heartbeatInterval: options.heartbeatInterval
|
||||
, muteMaster: options.muteMaster
|
||||
, lockRotation: options.lockRotation
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -866,7 +887,7 @@ program
|
|||
})
|
||||
|
||||
program
|
||||
.command('local [serial..]')
|
||||
.command('local [serial...]')
|
||||
.description('start everything locally')
|
||||
.option('--bind-app-pub <endpoint>'
|
||||
, 'app pub endpoint'
|
||||
|
@ -976,6 +997,10 @@ program
|
|||
.option('--user-profile-url <url>'
|
||||
, 'URL to external user profile page'
|
||||
, String)
|
||||
.option('--vnc-initial-size <size>'
|
||||
, 'initial VNC size'
|
||||
, cliutil.size
|
||||
, [600, 800])
|
||||
.option('--mute-master'
|
||||
, 'whether to mute master volume when devices are being used')
|
||||
.option('--use-s3'
|
||||
|
@ -991,10 +1016,10 @@ program
|
|||
, 's3 endpoint'
|
||||
, String
|
||||
, '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')
|
||||
, args = arguments
|
||||
, options = cliutil.lastArg(args)
|
||||
, procutil = require('./util/procutil')
|
||||
|
||||
// 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)
|
||||
, '--adb-host', options.adbHost
|
||||
, '--adb-port', options.adbPort
|
||||
, '--vnc-initial-size', options.vncInitialSize.join('x')
|
||||
]
|
||||
.concat(options.allowRemote ? ['--allow-remote'] : [])
|
||||
.concat(options.muteMaster ? ['--mute-master'] : [])
|
||||
.concat(cliutil.allUnknownArgs(args)))
|
||||
.concat(options.lockRotation ? ['--lock-rotation'] : [])
|
||||
.concat(serials))
|
||||
|
||||
// auth
|
||||
, procutil.fork(__filename, [
|
||||
|
|
|
@ -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) {
|
||||
return db.run(r.table('devices').getAll(email, {
|
||||
index: 'owner'
|
||||
|
|
|
@ -14,6 +14,17 @@ module.exports = {
|
|||
}
|
||||
}
|
||||
}
|
||||
, vncauth: {
|
||||
primaryKey: 'password'
|
||||
, indexes: {
|
||||
response: null
|
||||
, responsePerDevice: {
|
||||
indexFunction: function(row) {
|
||||
return [row('response'), row('deviceId')]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
, devices: {
|
||||
primaryKey: 'serial'
|
||||
, indexes: {
|
||||
|
|
|
@ -5,7 +5,7 @@ var webpack = require('webpack')
|
|||
var mime = require('mime')
|
||||
var Promise = require('bluebird')
|
||||
var _ = require('lodash')
|
||||
var MemoryFileSystem = require('webpack/node_modules/memory-fs')
|
||||
var MemoryFileSystem = require('memory-fs')
|
||||
|
||||
var logger = require('../../../util/logger')
|
||||
var lifecycle = require('../../../util/lifecycle')
|
||||
|
|
|
@ -20,6 +20,7 @@ module.exports = function(options) {
|
|||
.dependency(require('./plugins/solo'))
|
||||
.dependency(require('./plugins/screen/stream'))
|
||||
.dependency(require('./plugins/screen/capture'))
|
||||
.dependency(require('./plugins/vnc'))
|
||||
.dependency(require('./plugins/service'))
|
||||
.dependency(require('./plugins/browser'))
|
||||
.dependency(require('./plugins/store'))
|
||||
|
@ -38,6 +39,7 @@ module.exports = function(options) {
|
|||
.dependency(require('./plugins/ringer'))
|
||||
.dependency(require('./plugins/wifi'))
|
||||
.dependency(require('./plugins/sd'))
|
||||
.dependency(require('./plugins/filesystem'))
|
||||
.define(function(options, heartbeat, solo) {
|
||||
if (process.send) {
|
||||
// Only if we have a parent process
|
||||
|
|
74
lib/units/device/plugins/filesystem.js
Normal file
74
lib/units/device/plugins/filesystem.js
Normal 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
|
||||
})
|
|
@ -104,6 +104,7 @@ module.exports = syrup.serial()
|
|||
|
||||
plugin.on('leave', function() {
|
||||
service.pressKey('home')
|
||||
service.thawRotation()
|
||||
service.releaseWakeLock()
|
||||
})
|
||||
|
||||
|
|
|
@ -25,7 +25,6 @@ module.exports = syrup.serial()
|
|||
.dependency(require('./options'))
|
||||
.define(function(options, adb, minicap, display, screenOptions) {
|
||||
var log = logger.createLogger('device:plugins:screen:stream')
|
||||
var plugin = Object.create(null)
|
||||
|
||||
function FrameProducer(config) {
|
||||
EventEmitter.call(this)
|
||||
|
@ -443,9 +442,9 @@ module.exports = syrup.serial()
|
|||
|
||||
return createServer()
|
||||
.then(function(wss) {
|
||||
var broadcastSet = new BroadcastSet()
|
||||
var frameProducer = new FrameProducer(
|
||||
new FrameConfig(display.properties, display.properties))
|
||||
var broadcastSet = frameProducer.broadcastSet = new BroadcastSet()
|
||||
|
||||
broadcastSet.on('nonempty', function() {
|
||||
frameProducer.start()
|
||||
|
@ -455,18 +454,56 @@ module.exports = syrup.serial()
|
|||
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) {
|
||||
frameProducer.updateRotation(newRotation)
|
||||
})
|
||||
|
||||
frameProducer.on('start', function() {
|
||||
broadcastSet.keys().map(function(id) {
|
||||
return broadcastSet.get(id).onStart(frameProducer)
|
||||
})
|
||||
})
|
||||
|
||||
frameProducer.on('readable', function next() {
|
||||
var frame
|
||||
if ((frame = frameProducer.nextFrame())) {
|
||||
Promise.settle([broadcastSet.keys().map(function(id) {
|
||||
return broadcastSet.get(id).onFrame(frame)
|
||||
})]).then(next)
|
||||
}
|
||||
else {
|
||||
frameProducer.needFrame()
|
||||
}
|
||||
})
|
||||
|
||||
frameProducer.on('error', function(err) {
|
||||
log.fatal('Frame producer had an error', err.stack)
|
||||
lifecycle.fatal()
|
||||
})
|
||||
|
||||
wss.on('connection', function(ws) {
|
||||
var id = uuid.v4()
|
||||
|
||||
function wsStartNotifier() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var message = util.format(
|
||||
'start %s'
|
||||
, JSON.stringify(frameProducer.banner)
|
||||
)
|
||||
|
||||
broadcastSet.keys().forEach(function(id) {
|
||||
var ws = broadcastSet.get(id)
|
||||
switch (ws.readyState) {
|
||||
case WebSocket.OPENING:
|
||||
// This should never happen.
|
||||
|
@ -474,7 +511,9 @@ module.exports = syrup.serial()
|
|||
break
|
||||
case WebSocket.OPEN:
|
||||
// This is what SHOULD happen.
|
||||
ws.send(message)
|
||||
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
|
||||
|
@ -487,14 +526,10 @@ module.exports = syrup.serial()
|
|||
break
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
frameProducer.on('readable', function next() {
|
||||
var frame
|
||||
if ((frame = frameProducer.nextFrame())) {
|
||||
Promise.settle([broadcastSet.keys().map(function(id) {
|
||||
function wsFrameNotifier(frame) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var ws = broadcastSet.get(id)
|
||||
switch (ws.readyState) {
|
||||
case WebSocket.OPENING:
|
||||
// This should never happen.
|
||||
|
@ -519,27 +554,17 @@ module.exports = syrup.serial()
|
|||
'Unable to send frame to CLOSED client "%s"', id)))
|
||||
}
|
||||
})
|
||||
})]).then(next)
|
||||
}
|
||||
else {
|
||||
frameProducer.needFrame()
|
||||
}
|
||||
})
|
||||
|
||||
frameProducer.on('error', function(err) {
|
||||
log.fatal('Frame producer had an error', err.stack)
|
||||
lifecycle.fatal()
|
||||
})
|
||||
|
||||
wss.on('connection', function(ws) {
|
||||
var id = uuid.v4()
|
||||
|
||||
ws.on('message', function(data) {
|
||||
var match
|
||||
if ((match = /^(on|off|(size) ([0-9]+)x([0-9]+))$/.exec(data))) {
|
||||
switch (match[2] || match[1]) {
|
||||
case 'on':
|
||||
broadcastSet.insert(id, ws)
|
||||
broadcastSet.insert(id, {
|
||||
onStart: wsStartNotifier
|
||||
, onFrame: wsFrameNotifier
|
||||
})
|
||||
break
|
||||
case 'off':
|
||||
broadcastSet.remove(id)
|
||||
|
@ -563,6 +588,7 @@ module.exports = syrup.serial()
|
|||
lifecycle.observe(function() {
|
||||
frameProducer.stop()
|
||||
})
|
||||
|
||||
return frameProducer
|
||||
})
|
||||
.return(plugin)
|
||||
})
|
||||
|
|
|
@ -365,7 +365,7 @@ module.exports = syrup.serial()
|
|||
plugin.rotate = function(rotation) {
|
||||
return runAgentCommand(
|
||||
apk.wire.MessageType.SET_ROTATION
|
||||
, new apk.wire.SetRotationRequest(rotation, false)
|
||||
, new apk.wire.SetRotationRequest(rotation, options.lockRotation || false)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
290
lib/units/device/plugins/vnc/index.js
Normal file
290
lib/units/device/plugins/vnc/index.js
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
535
lib/units/device/plugins/vnc/util/connection.js
Normal file
535
lib/units/device/plugins/vnc/util/connection.js
Normal 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
|
14
lib/units/device/plugins/vnc/util/pixelformat.js
Normal file
14
lib/units/device/plugins/vnc/util/pixelformat.js
Normal 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
|
66
lib/units/device/plugins/vnc/util/pointertranslator.js
Normal file
66
lib/units/device/plugins/vnc/util/pointertranslator.js
Normal 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
|
52
lib/units/device/plugins/vnc/util/server.js
Normal file
52
lib/units/device/plugins/vnc/util/server.js
Normal 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
|
|
@ -14,7 +14,7 @@ module.exports = syrup.serial()
|
|||
.dependency(require('../support/properties'))
|
||||
.dependency(require('../support/abi'))
|
||||
.define(function(options, adb, properties, abi) {
|
||||
var log = logger.createLogger('device:resources:minicap')
|
||||
logger.createLogger('device:resources:minicap')
|
||||
|
||||
var resources = {
|
||||
bin: {
|
||||
|
|
|
@ -17,7 +17,7 @@ module.exports = syrup.serial()
|
|||
pathutil.vendor('STFService/wire.proto'))
|
||||
|
||||
var resource = {
|
||||
requiredVersion: '1.0.1'
|
||||
requiredVersion: '1.0.2'
|
||||
, pkg: 'jp.co.cyberagent.stf'
|
||||
, main: 'jp.co.cyberagent.stf.Agent'
|
||||
, apk: pathutil.vendor('STFService/STFService.apk')
|
||||
|
@ -79,9 +79,9 @@ module.exports = syrup.serial()
|
|||
.then(function() {
|
||||
return promiseutil.periodicNotify(
|
||||
adb.install(options.serial, resource.apk)
|
||||
, 10000
|
||||
, 20000
|
||||
)
|
||||
.timeout(60000)
|
||||
.timeout(65000)
|
||||
})
|
||||
.progressed(function() {
|
||||
log.warn(
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
var syrup = require('stf-syrup')
|
||||
|
||||
var zmq = require('zmq')
|
||||
var Promise = require('bluebird')
|
||||
|
||||
var logger = require('../../../util/logger')
|
||||
var srv = require('../../../util/srv')
|
||||
var zmqutil = require('../../../util/zmqutil')
|
||||
|
||||
module.exports = syrup.serial()
|
||||
.define(function(options) {
|
||||
var log = logger.createLogger('device:support:push')
|
||||
|
||||
// Output
|
||||
var push = zmq.socket('push')
|
||||
var push = zmqutil.socket('push')
|
||||
|
||||
return Promise.map(options.endpoints.push, function(endpoint) {
|
||||
return srv.resolve(endpoint).then(function(records) {
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
var syrup = require('stf-syrup')
|
||||
|
||||
var zmq = require('zmq')
|
||||
var Promise = require('bluebird')
|
||||
|
||||
var logger = require('../../../util/logger')
|
||||
var wireutil = require('../../../wire/util')
|
||||
var srv = require('../../../util/srv')
|
||||
var lifecycle = require('../../../util/lifecycle')
|
||||
require('../../../util/lifecycle')
|
||||
var zmqutil = require('../../../util/zmqutil')
|
||||
|
||||
module.exports = syrup.serial()
|
||||
.define(function(options) {
|
||||
var log = logger.createLogger('device:support:sub')
|
||||
|
||||
// Input
|
||||
var sub = zmq.socket('sub')
|
||||
var sub = zmqutil.socket('sub')
|
||||
|
||||
return Promise.map(options.endpoints.sub, function(endpoint) {
|
||||
return srv.resolve(endpoint).then(function(records) {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
var Promise = require('bluebird')
|
||||
var zmq = require('zmq')
|
||||
|
||||
var logger = require('../../util/logger')
|
||||
var wire = require('../../wire')
|
||||
|
@ -8,12 +7,13 @@ var wireutil = require('../../wire/util')
|
|||
var lifecycle = require('../../util/lifecycle')
|
||||
var srv = require('../../util/srv')
|
||||
var dbapi = require('../../db/api')
|
||||
var zmqutil = require('../../util/zmqutil')
|
||||
|
||||
module.exports = function(options) {
|
||||
var log = logger.createLogger('log-db')
|
||||
|
||||
// Input
|
||||
var sub = zmq.socket('sub')
|
||||
var sub = zmqutil.socket('sub')
|
||||
Promise.map(options.endpoints.sub, function(endpoint) {
|
||||
return srv.resolve(endpoint).then(function(records) {
|
||||
return srv.attempt(records, function(record) {
|
||||
|
|
|
@ -2,7 +2,6 @@ var util = require('util')
|
|||
|
||||
var Hipchatter = require('hipchatter')
|
||||
var Promise = require('bluebird')
|
||||
var zmq = require('zmq')
|
||||
|
||||
var logger = require('../../util/logger')
|
||||
var wire = require('../../wire')
|
||||
|
@ -10,6 +9,7 @@ var wirerouter = require('../../wire/router')
|
|||
var wireutil = require('../../wire/util')
|
||||
var lifecycle = require('../../util/lifecycle')
|
||||
var srv = require('../../util/srv')
|
||||
var zmqutil = require('../../util/zmqutil')
|
||||
|
||||
var COLORS = {
|
||||
1: 'gray'
|
||||
|
@ -28,7 +28,7 @@ module.exports = function(options) {
|
|||
, timer
|
||||
|
||||
// Input
|
||||
var sub = zmq.socket('sub')
|
||||
var sub = zmqutil.socket('sub')
|
||||
Promise.map(options.endpoints.sub, function(endpoint) {
|
||||
return srv.resolve(endpoint).then(function(records) {
|
||||
return srv.attempt(records, function(record) {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
var Promise = require('bluebird')
|
||||
var zmq = require('zmq')
|
||||
|
||||
var logger = require('../../util/logger')
|
||||
var wire = require('../../wire')
|
||||
|
@ -8,6 +7,7 @@ var wireutil = require('../../wire/util')
|
|||
var dbapi = require('../../db/api')
|
||||
var lifecycle = require('../../util/lifecycle')
|
||||
var srv = require('../../util/srv')
|
||||
var zmqutil = require('../../util/zmqutil')
|
||||
|
||||
module.exports = function(options) {
|
||||
var log = logger.createLogger('processor')
|
||||
|
@ -17,7 +17,7 @@ module.exports = function(options) {
|
|||
}
|
||||
|
||||
// App side
|
||||
var appDealer = zmq.socket('dealer')
|
||||
var appDealer = zmqutil.socket('dealer')
|
||||
Promise.map(options.endpoints.appDealer, function(endpoint) {
|
||||
return srv.resolve(endpoint).then(function(records) {
|
||||
return srv.attempt(records, function(record) {
|
||||
|
@ -37,7 +37,7 @@ module.exports = function(options) {
|
|||
})
|
||||
|
||||
// Device side
|
||||
var devDealer = zmq.socket('dealer')
|
||||
var devDealer = zmqutil.socket('dealer')
|
||||
Promise.map(options.endpoints.devDealer, function(endpoint) {
|
||||
return srv.resolve(endpoint).then(function(records) {
|
||||
return srv.attempt(records, function(record) {
|
||||
|
@ -124,12 +124,46 @@ module.exports = function(options) {
|
|||
})
|
||||
.catch(function(err) {
|
||||
log.error(
|
||||
'Unable to lookup user by fingerprint "%s"'
|
||||
'Unable to lookup user by ADB fingerprint "%s"'
|
||||
, message.fingerprint
|
||||
, 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) {
|
||||
dbapi.setDeviceOwner(message.serial, message.owner)
|
||||
appDealer.send([channel, data])
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
var adb = require('adbkit')
|
||||
var Promise = require('bluebird')
|
||||
var zmq = require('zmq')
|
||||
var _ = require('lodash')
|
||||
var EventEmitter = require('eventemitter3').EventEmitter
|
||||
|
||||
|
@ -11,6 +10,7 @@ var wirerouter = require('../../wire/router')
|
|||
var procutil = require('../../util/procutil')
|
||||
var lifecycle = require('../../util/lifecycle')
|
||||
var srv = require('../../util/srv')
|
||||
var zmqutil = require('../../util/zmqutil')
|
||||
|
||||
module.exports = function(options) {
|
||||
var log = logger.createLogger('provider')
|
||||
|
@ -70,7 +70,7 @@ module.exports = function(options) {
|
|||
})()
|
||||
|
||||
// Output
|
||||
var push = zmq.socket('push')
|
||||
var push = zmqutil.socket('push')
|
||||
Promise.map(options.endpoints.push, function(endpoint) {
|
||||
return srv.resolve(endpoint).then(function(records) {
|
||||
return srv.attempt(records, function(record) {
|
||||
|
@ -86,7 +86,7 @@ module.exports = function(options) {
|
|||
})
|
||||
|
||||
// Input
|
||||
var sub = zmq.socket('sub')
|
||||
var sub = zmqutil.socket('sub')
|
||||
Promise.map(options.endpoints.sub, function(endpoint) {
|
||||
return srv.resolve(endpoint).then(function(records) {
|
||||
return srv.attempt(records, function(record) {
|
||||
|
@ -316,7 +316,7 @@ module.exports = function(options) {
|
|||
|
||||
// Spawn a device worker
|
||||
function spawn() {
|
||||
var allocatedPorts = ports.splice(0, 2)
|
||||
var allocatedPorts = ports.splice(0, 4)
|
||||
, proc = options.fork(device, allocatedPorts)
|
||||
, resolver = Promise.defer()
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
var Promise = require('bluebird')
|
||||
var zmq = require('zmq')
|
||||
|
||||
var logger = require('../../util/logger')
|
||||
var wire = require('../../wire')
|
||||
|
@ -9,6 +8,7 @@ var dbapi = require('../../db/api')
|
|||
var lifecycle = require('../../util/lifecycle')
|
||||
var srv = require('../../util/srv')
|
||||
var TtlSet = require('../../util/ttlset')
|
||||
var zmqutil = require('../../util/zmqutil')
|
||||
|
||||
module.exports = function(options) {
|
||||
var log = logger.createLogger('reaper')
|
||||
|
@ -19,7 +19,7 @@ module.exports = function(options) {
|
|||
}
|
||||
|
||||
// Input
|
||||
var sub = zmq.socket('sub')
|
||||
var sub = zmqutil.socket('sub')
|
||||
Promise.map(options.endpoints.sub, function(endpoint) {
|
||||
return srv.resolve(endpoint).then(function(records) {
|
||||
return srv.attempt(records, function(record) {
|
||||
|
@ -41,7 +41,7 @@ module.exports = function(options) {
|
|||
})
|
||||
|
||||
// Output
|
||||
var push = zmq.socket('push')
|
||||
var push = zmqutil.socket('push')
|
||||
Promise.map(options.endpoints.push, function(endpoint) {
|
||||
return srv.resolve(endpoint).then(function(records) {
|
||||
return srv.attempt(records, function(record) {
|
||||
|
|
|
@ -138,6 +138,10 @@ module.exports = function(options) {
|
|||
app.get('/s/blob/:id/:name', function(req, res) {
|
||||
var file = storage.retrieve(req.params.id)
|
||||
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.sendFile(file.path)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
var zmq = require('zmq')
|
||||
|
||||
var logger = require('../../util/logger')
|
||||
var lifecycle = require('../../util/lifecycle')
|
||||
var zmqutil = require('../../util/zmqutil')
|
||||
|
||||
module.exports = function(options) {
|
||||
var log = logger.createLogger('triproxy')
|
||||
|
@ -17,18 +16,18 @@ module.exports = function(options) {
|
|||
}
|
||||
|
||||
// App/device output
|
||||
var pub = zmq.socket('pub')
|
||||
var pub = zmqutil.socket('pub')
|
||||
pub.bindSync(options.endpoints.pub)
|
||||
log.info('PUB socket bound on', options.endpoints.pub)
|
||||
|
||||
// Coordinator input/output
|
||||
var dealer = zmq.socket('dealer')
|
||||
var dealer = zmqutil.socket('dealer')
|
||||
dealer.bindSync(options.endpoints.dealer)
|
||||
dealer.on('message', proxy(pub))
|
||||
log.info('DEALER socket bound on', options.endpoints.dealer)
|
||||
|
||||
// App/device input
|
||||
var pull = zmq.socket('pull')
|
||||
var pull = zmqutil.socket('pull')
|
||||
pull.bindSync(options.endpoints.pull)
|
||||
pull.on('message', proxy(dealer))
|
||||
log.info('PULL socket bound on', options.endpoints.pull)
|
||||
|
|
|
@ -3,7 +3,6 @@ var events = require('events')
|
|||
var util = require('util')
|
||||
|
||||
var socketio = require('socket.io')
|
||||
var zmq = require('zmq')
|
||||
var Promise = require('bluebird')
|
||||
var _ = require('lodash')
|
||||
var request = Promise.promisifyAll(require('request'))
|
||||
|
@ -17,6 +16,7 @@ var dbapi = require('../../db/api')
|
|||
var datautil = require('../../util/datautil')
|
||||
var srv = require('../../util/srv')
|
||||
var lifecycle = require('../../util/lifecycle')
|
||||
var zmqutil = require('../../util/zmqutil')
|
||||
var cookieSession = require('./middleware/cookie-session')
|
||||
var ip = require('./middleware/remote-ip')
|
||||
var auth = require('./middleware/auth')
|
||||
|
@ -31,7 +31,7 @@ module.exports = function(options) {
|
|||
, channelRouter = new events.EventEmitter()
|
||||
|
||||
// Output
|
||||
var push = zmq.socket('push')
|
||||
var push = zmqutil.socket('push')
|
||||
Promise.map(options.endpoints.push, function(endpoint) {
|
||||
return srv.resolve(endpoint).then(function(records) {
|
||||
return srv.attempt(records, function(record) {
|
||||
|
@ -47,7 +47,7 @@ module.exports = function(options) {
|
|||
})
|
||||
|
||||
// Input
|
||||
var sub = zmq.socket('sub')
|
||||
var sub = zmqutil.socket('sub')
|
||||
Promise.map(options.endpoints.sub, function(endpoint) {
|
||||
return srv.resolve(endpoint).then(function(records) {
|
||||
return srv.attempt(records, function(record) {
|
||||
|
@ -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() {
|
||||
// Clean up all listeners and subscriptions
|
||||
|
|
|
@ -2,12 +2,9 @@ module.exports.list = function(val) {
|
|||
return val.split(/\s*,\s*/g).filter(Boolean)
|
||||
}
|
||||
|
||||
module.exports.allUnknownArgs = function(args) {
|
||||
return [].slice.call(args, 0, -1).filter(Boolean)
|
||||
}
|
||||
|
||||
module.exports.lastArg = function(args) {
|
||||
return args[args.length - 1]
|
||||
module.exports.size = function(val) {
|
||||
var match = /^(\d+)x(\d+)$/.exec(val)
|
||||
return match ? [+match[1], +match[2]] : undefined
|
||||
}
|
||||
|
||||
module.exports.range = function(from, to) {
|
||||
|
|
44
lib/util/vncauth.js
Normal file
44
lib/util/vncauth.js
Normal 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
23
lib/util/zmqutil.js
Normal 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
|
||||
}
|
|
@ -17,6 +17,8 @@ enum MessageType {
|
|||
PhysicalIdentifyMessage = 29;
|
||||
JoinGroupMessage = 11;
|
||||
JoinGroupByAdbFingerprintMessage = 69;
|
||||
JoinGroupByVncAuthResponseMessage = 90;
|
||||
VncAuthResponsesUpdatedMessage = 91;
|
||||
AutoGroupMessage = 70;
|
||||
AdbKeysUpdatedMessage = 71;
|
||||
KeyDownMessage = 12;
|
||||
|
@ -72,6 +74,16 @@ enum MessageType {
|
|||
AccountRemoveMessage = 55;
|
||||
SdStatusMessage = 61;
|
||||
ReverseForwardsEvent = 72;
|
||||
FileSystemListMessage = 81;
|
||||
FileSystemGetMessage = 82;
|
||||
}
|
||||
|
||||
message FileSystemListMessage {
|
||||
required string dir = 1;
|
||||
}
|
||||
|
||||
message FileSystemGetMessage {
|
||||
required string file = 1;
|
||||
}
|
||||
|
||||
message Envelope {
|
||||
|
@ -261,9 +273,18 @@ message JoinGroupByAdbFingerprintMessage {
|
|||
optional string currentGroup = 4;
|
||||
}
|
||||
|
||||
message JoinGroupByVncAuthResponseMessage {
|
||||
required string serial = 1;
|
||||
required string response = 2;
|
||||
optional string currentGroup = 4;
|
||||
}
|
||||
|
||||
message AdbKeysUpdatedMessage {
|
||||
}
|
||||
|
||||
message VncAuthResponsesUpdatedMessage {
|
||||
}
|
||||
|
||||
message LeaveGroupMessage {
|
||||
required string serial = 1;
|
||||
required OwnerMessage owner = 2;
|
||||
|
|
67
package.json
67
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "stf",
|
||||
"version": "1.0.9",
|
||||
"version": "1.0.10",
|
||||
"description": "Smartphone Test Farm",
|
||||
"keywords": [
|
||||
"adb",
|
||||
|
@ -36,39 +36,41 @@
|
|||
"aws-sdk": "^2.2.3",
|
||||
"bluebird": "^2.9.34",
|
||||
"body-parser": "^1.13.3",
|
||||
"chalk": "~1.0.0",
|
||||
"commander": "^2.7.1",
|
||||
"chalk": "~1.1.1",
|
||||
"commander": "^2.9.0",
|
||||
"compression": "^1.5.2",
|
||||
"cookie-session": "^1.2.0",
|
||||
"csurf": "^1.7.0",
|
||||
"debug": "^2.2.0",
|
||||
"eventemitter3": "^0.1.6",
|
||||
"express": "^4.13.3",
|
||||
"express-validator": "^2.14.0",
|
||||
"express-validator": "^2.17.1",
|
||||
"formidable": "^1.0.17",
|
||||
"gm": "^1.17.0",
|
||||
"hipchatter": "^0.2.0",
|
||||
"http-proxy": "^1.9.0",
|
||||
"http-proxy": "^1.11.2",
|
||||
"in-publish": "^2.0.0",
|
||||
"jade": "^1.9.2",
|
||||
"jpeg-turbo": "^0.3.0",
|
||||
"jws": "^3.1.0",
|
||||
"ldapjs": "git+https://github.com/mcavage/node-ldapjs.git#acc1ca8f4314fd9d67561feabc8ce4c235076a5e",
|
||||
"lodash": "^3.10.1",
|
||||
"markdown-serve": "^0.3.2",
|
||||
"mime": "^1.3.4",
|
||||
"minimatch": "^2.0.10",
|
||||
"minimatch": "^3.0.0",
|
||||
"my-local-ip": "^1.0.0",
|
||||
"node-uuid": "^1.4.3",
|
||||
"passport": "^0.2.1",
|
||||
"passport": "^0.3.0",
|
||||
"passport-oauth2": "^1.1.2",
|
||||
"protobufjs": "^3.8.2",
|
||||
"proxy-addr": "^1.0.7",
|
||||
"request": "^2.60.0",
|
||||
"request": "^2.65.0",
|
||||
"request-progress": "^0.3.1",
|
||||
"rethinkdb": "^2.0.2",
|
||||
"semver": "^5.0.1",
|
||||
"serve-favicon": "^2.2.0",
|
||||
"serve-static": "^1.9.2",
|
||||
"socket.io": "1.3.6",
|
||||
"socket.io": "1.3.7",
|
||||
"split": "^1.0.0",
|
||||
"stf-appstore-db": "^1.0.0",
|
||||
"stf-browser-db": "^1.0.2",
|
||||
|
@ -77,68 +79,67 @@
|
|||
"stf-wiki": "^1.0.0",
|
||||
"temp": "^0.8.1",
|
||||
"transliteration": "^0.1.1",
|
||||
"ws": "^0.7.2",
|
||||
"zmq": "^2.12.0"
|
||||
"ws": "^0.8.0",
|
||||
"zmq": "^2.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"async": "^1.4.0",
|
||||
"aws-sdk": "^2.1.46",
|
||||
"bower": "^1.3.12",
|
||||
"async": "^1.4.2",
|
||||
"bower": "^1.6.3",
|
||||
"chai": "^3.2.0",
|
||||
"css-loader": "^0.14.0",
|
||||
"del": "^1.2.0",
|
||||
"event-stream": "^3.3.0",
|
||||
"css-loader": "^0.20.1",
|
||||
"del": "^2.0.1",
|
||||
"event-stream": "^3.3.2",
|
||||
"exports-loader": "^0.6.2",
|
||||
"extract-text-webpack-plugin": "^0.8.2",
|
||||
"file-loader": "^0.8.1",
|
||||
"gulp": "^3.8.11",
|
||||
"gulp-angular-gettext": "^2.1.0",
|
||||
"gulp-jade": "^1.0.0",
|
||||
"gulp-jscs": "^2.0.0",
|
||||
"gulp-jscs": "^3.0.0",
|
||||
"gulp-jshint": "^1.11.2",
|
||||
"gulp-jsonlint": "^1.0.2",
|
||||
"gulp-protractor": "^1.0.0",
|
||||
"gulp-run": "^1.6.10",
|
||||
"gulp-standard": "^4.5.3",
|
||||
"gulp-standard": "^5.1.0",
|
||||
"gulp-util": "^3.0.4",
|
||||
"html-loader": "^0.3.0",
|
||||
"imports-loader": "^0.6.3",
|
||||
"imports-loader": "^0.6.5",
|
||||
"jasmine-core": "^2.3.4",
|
||||
"jasmine-reporters": "^2.0.5",
|
||||
"jshint": "^2.6.3",
|
||||
"jshint-loader": "^0.8.3",
|
||||
"jshint-stylish": "^2.0.0",
|
||||
"json-loader": "^0.5.1",
|
||||
"karma": "^0.13.3",
|
||||
"karma": "^0.13.11",
|
||||
"karma-chrome-launcher": "^0.2.0",
|
||||
"karma-firefox-launcher": "^0.1.4",
|
||||
"karma-ie-launcher": "^0.2.0",
|
||||
"karma-jasmine": "^0.3.5",
|
||||
"karma-junit-reporter": "^0.3.3",
|
||||
"karma-opera-launcher": "^0.2.0",
|
||||
"karma-phantomjs-launcher": "^0.2.0",
|
||||
"karma-junit-reporter": "^0.3.4",
|
||||
"karma-opera-launcher": "^0.3.0",
|
||||
"karma-phantomjs-launcher": "^0.2.1",
|
||||
"karma-safari-launcher": "^0.1.1",
|
||||
"karma-webpack": "^1.6.0",
|
||||
"less": "^2.4.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-sass": "^3.2.0",
|
||||
"phantomjs": "^1.9.17",
|
||||
"protractor": "^2.0.0",
|
||||
"node-sass": "^3.3.3",
|
||||
"phantomjs": "^1.9.18",
|
||||
"protractor": "^2.5.1",
|
||||
"protractor-html-screenshot-reporter": "0.0.21",
|
||||
"raw-loader": "^0.5.1",
|
||||
"run-sequence": "^1.1.2",
|
||||
"sass-loader": "^1.0.4",
|
||||
"sass-loader": "^3.0.0",
|
||||
"script-loader": "^0.6.1",
|
||||
"sinon": "^1.14.1",
|
||||
"sinon": "^1.16.1",
|
||||
"sinon-chai": "^2.7.0",
|
||||
"socket.io-client": "1.3.6",
|
||||
"socket.io-client": "1.3.7",
|
||||
"style-loader": "^0.12.3",
|
||||
"template-html-loader": "^0.0.3",
|
||||
"url-loader": "^0.5.5",
|
||||
"webpack": "^1.10.5",
|
||||
"webpack-dev-server": "^1.7.0"
|
||||
"webpack": "^1.12.2",
|
||||
"webpack-dev-server": "^1.12.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
|
|
|
@ -40,10 +40,3 @@ div[angular-packery]:after {
|
|||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.packery-item.is-dragging,
|
||||
.packery-item.is-positioning-post-drag {
|
||||
/*border-color: red;*/
|
||||
/*background: #09F;*/
|
||||
/*z-index: 2;*/
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ module.exports = function basicModeDirective($rootScope, BrowserInfo) {
|
|||
return {
|
||||
restrict: 'AE',
|
||||
link: function (scope, element) {
|
||||
$rootScope.basicMode = !!BrowserInfo.mobile // CHECK: use .mobile instead of .small
|
||||
$rootScope.basicMode = !!BrowserInfo.mobile
|
||||
if ($rootScope.basicMode) {
|
||||
element.addClass('basic-mode')
|
||||
}
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
.basic-mode {
|
||||
/*background: red;*/
|
||||
}
|
||||
|
||||
.basic-mode .devices-icon-view {
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -10,11 +6,6 @@
|
|||
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.active {
|
||||
background: #007aff;
|
||||
|
@ -27,8 +18,6 @@
|
|||
|
||||
.basic-mode .basic-remote-control {
|
||||
width: 100%;
|
||||
/*width: 320px;*/
|
||||
/*height: 485px;*/
|
||||
}
|
||||
|
||||
.basic-mode .stf-device-list .device-search {
|
||||
|
|
|
@ -29,8 +29,6 @@ module.exports = function BrowserInfoServiceFactory() {
|
|||
var windowWidth = window.screen.width < window.outerWidth ?
|
||||
window.screen.width : window.outerWidth
|
||||
return windowWidth < 800
|
||||
// return !!(window.matchMedia &&
|
||||
// window.matchMedia('only screen and (max-width: 760px)').matches)
|
||||
})
|
||||
|
||||
addTest('mobile', function () {
|
||||
|
@ -59,38 +57,6 @@ module.exports = function BrowserInfoServiceFactory() {
|
|||
|
||||
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('deviceorientation', 'DeviceOrientationEvent' in window)
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
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');
|
||||
|
||||
}));
|
||||
}))
|
||||
|
||||
})
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
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')
|
||||
|
|
|
@ -3,8 +3,6 @@ module.exports = function counterDirective($timeout) {
|
|||
replace: false,
|
||||
scope: true,
|
||||
link: function (scope, element, attrs) {
|
||||
// TODO: use $$rAF later
|
||||
|
||||
var el = element[0]
|
||||
var num, refreshInterval, duration, steps, step, countTo, increment
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
require('./help-icon.css')
|
||||
|
||||
module.exports = function clearButtonDirective() {
|
||||
return {
|
||||
restrict: 'EA',
|
||||
|
|
|
@ -8,7 +8,6 @@ module.exports = angular.module('stf/common-ui', [
|
|||
require('./notifications').name,
|
||||
require('./ng-enter').name,
|
||||
require('./tooltips').name,
|
||||
//require('./tree').name,
|
||||
require('./modals').name,
|
||||
require('./include-cached').name,
|
||||
require('./text-focus-select').name,
|
||||
|
|
|
@ -2,7 +2,7 @@ describe('FatalMessageService', function() {
|
|||
|
||||
beforeEach(angular.mock.module(require('./').name));
|
||||
|
||||
it('should ...', inject(function(FatalMessageService) {
|
||||
it('should ...', inject(function() {
|
||||
|
||||
//expect(FatalMessageService.doSomething()).toEqual('something');
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
module.exports = angular.module('stf.add-adb-key-modal', [
|
||||
require('stf/common-ui/modals/common').name,
|
||||
//require('stf/keys/add-adb-key').name
|
||||
require('stf/common-ui/modals/common').name
|
||||
])
|
||||
.factory('AddAdbKeyModalService', require('./add-adb-key-modal-service'))
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
//require('angular-dialog-service/dialogs')
|
||||
//require('angular-dialog-service/dialogs.css')
|
||||
require('./modals.css')
|
||||
|
||||
module.exports = angular.module('stf.modals.common', [
|
||||
|
|
|
@ -50,7 +50,6 @@
|
|||
.modal-size-80p .modal-dialog {
|
||||
width: 80%;
|
||||
height: 100%;
|
||||
/*max-height: 800px;*/
|
||||
}
|
||||
|
||||
.modal-size-80p .modal-body {
|
||||
|
|
|
@ -19,7 +19,6 @@ module.exports = function ServiceFactory($modal, $sce) {
|
|||
var modalInstance = $modal.open({
|
||||
template: require('./external-url-modal.jade'),
|
||||
controller: ModalInstanceCtrl,
|
||||
// size: 'lg',
|
||||
windowClass: 'modal-size-80p',
|
||||
resolve: {
|
||||
title: function() {
|
||||
|
|
|
@ -2,7 +2,7 @@ describe('ExternalUrlModalService', function() {
|
|||
|
||||
beforeEach(angular.mock.module(require('./').name));
|
||||
|
||||
it('should ...', inject(function(ExternalUrlModalService) {
|
||||
it('should ...', inject(function() {
|
||||
|
||||
//expect(FatalMessageService.doSomething()).toEqual('something');
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ module.exports =
|
|||
$scope.ok = function () {
|
||||
$modalInstance.close(true)
|
||||
$route.reload()
|
||||
//$location.path('/control/' + device.serial)
|
||||
}
|
||||
|
||||
function update() {
|
||||
|
|
|
@ -2,7 +2,7 @@ describe('FatalMessageService', function() {
|
|||
|
||||
beforeEach(angular.mock.module(require('./').name));
|
||||
|
||||
it('should ...', inject(function(FatalMessageService) {
|
||||
it('should ...', inject(function() {
|
||||
|
||||
//expect(FatalMessageService.doSomething()).toEqual('something');
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ module.exports = function ServiceFactory($modal) {
|
|||
var modalInstance = $modal.open({
|
||||
template: require('./lightbox-image.jade'),
|
||||
controller: ModalInstanceCtrl,
|
||||
windowClass: 'modal-size-xl', // TODO: Make width dynamic adjusting
|
||||
windowClass: 'modal-size-xl',
|
||||
resolve: {
|
||||
title: function() {
|
||||
return title
|
||||
|
|
|
@ -2,7 +2,7 @@ describe('LightboxImageService', function() {
|
|||
|
||||
beforeEach(angular.mock.module(require('./').name));
|
||||
|
||||
it('should ...', inject(function(LightboxImageService) {
|
||||
it('should ...', inject(function() {
|
||||
|
||||
//expect(XLightboxImageService.doSomething()).toEqual('something');
|
||||
|
||||
|
|
|
@ -7,4 +7,3 @@
|
|||
.modal-body
|
||||
img(ng-if='imageUrl', ng-src='{{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
|
||||
|
|
|
@ -2,7 +2,7 @@ describe('SocketDisconnectedService', function() {
|
|||
|
||||
beforeEach(angular.mock.module(require('./index').name))
|
||||
|
||||
it('should ...', inject(function(SocketDisconnectedService) {
|
||||
it('should ...', inject(function() {
|
||||
|
||||
//expect(SocketDisconnectedService.doSomething()).toEqual('something')
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ describe('VersionUpdateService', function() {
|
|||
beforeEach(angular.mock.module(require('ui-bootstrap').name));
|
||||
beforeEach(angular.mock.module(require('./').name));
|
||||
|
||||
it('should ...', inject(function(VersionUpdateService) {
|
||||
it('should ...', inject(function() {
|
||||
|
||||
//expect(VersionUpdateService.doSomething()).toEqual('something');
|
||||
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
require('./native-autocomplete.css')
|
||||
|
||||
module.exports = angular.module('stf.native-autocomplete', [
|
||||
|
||||
])
|
||||
.directive('nativeAutocomplete', require('./native-autocomplete-directive'))
|
|
@ -1,13 +0,0 @@
|
|||
module.exports = function nativeAutocompleteDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
replace: true,
|
||||
scope: {
|
||||
|
||||
},
|
||||
template: require('./native-autocomplete.jade'),
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
*/
|
||||
|
||||
});
|
||||
});
|
|
@ -1,3 +0,0 @@
|
|||
.stf-native-autocomplete {
|
||||
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
div.stf-native-autocomplete
|
|
@ -1,7 +0,0 @@
|
|||
input(
|
||||
type='text',
|
||||
native-autocomplete,
|
||||
ng-model='text',
|
||||
typeahead='["text1", "text2"]',
|
||||
history='20'
|
||||
)
|
|
@ -3,7 +3,6 @@
|
|||
top: 60px;
|
||||
right: 15px;
|
||||
float: right;
|
||||
/*width: 320px;*/
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,8 +9,6 @@ module.exports = function refreshPageDirective($window) {
|
|||
scope.reloadWindow = function () {
|
||||
$window.location.reload()
|
||||
}
|
||||
|
||||
// TODO: reload with $route.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
require('./table.css')
|
||||
require('script!ng-table/dist/ng-table')
|
||||
//require('ng-table/ng-table.css')
|
||||
|
||||
module.exports = angular.module('stf/common-ui/table', [
|
||||
'ngTable'
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
.ng-table th {
|
||||
/*text-align: center;*/
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
|
|
|
@ -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'))
|
|
@ -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
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
.stf-tree {
|
||||
|
||||
}
|
|
@ -157,9 +157,10 @@ module.exports = function ControlServiceFactory(
|
|||
return sendTwoWay('device.reboot')
|
||||
}
|
||||
|
||||
this.rotate = function(rotation) {
|
||||
this.rotate = function(rotation, lock) {
|
||||
return sendOneWay('display.rotate', {
|
||||
rotation: rotation
|
||||
rotation: rotation,
|
||||
lock: lock
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -224,6 +225,18 @@ module.exports = function ControlServiceFactory(
|
|||
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) {
|
||||
return sendTwoWay('account.check', {
|
||||
type: type
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
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')
|
||||
|
||||
}));
|
||||
}))
|
||||
|
||||
})
|
||||
|
|
|
@ -2,9 +2,9 @@ describe('install', function() {
|
|||
|
||||
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')
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
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')
|
||||
|
||||
}));
|
||||
}))
|
||||
|
||||
})
|
||||
|
|
|
@ -2,7 +2,7 @@ describe('NativeUrlService', function() {
|
|||
|
||||
beforeEach(angular.mock.module(require('./').name))
|
||||
|
||||
it('should ...', inject(function(NativeUrlService) {
|
||||
it('should ...', inject(function() {
|
||||
|
||||
//expect(NativeUrlService.doSomething()).toEqual('something')
|
||||
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
describe('PortForwardingService', function() {
|
||||
|
||||
beforeEach(angular.mock.module(require('./').name));
|
||||
beforeEach(angular.mock.module(require('./').name))
|
||||
|
||||
it('should ...', inject(function(PortForwardingService) {
|
||||
expect(1).toBe(1)
|
||||
//expect(PortForwardingService.doSomething()).toEqual('something');
|
||||
it('should ...', inject(function() {
|
||||
//expect(PortForwardingService.doSomething()).toEqual('something')
|
||||
|
||||
}));
|
||||
}))
|
||||
|
||||
})
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
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')
|
||||
|
||||
}));
|
||||
}))
|
||||
|
||||
})
|
||||
|
|
|
@ -7,6 +7,10 @@ var frame = {
|
|||
current: 0
|
||||
}
|
||||
|
||||
function FastImageRender () {
|
||||
|
||||
}
|
||||
|
||||
var imageRender = new FastImageRender(
|
||||
canvasElement
|
||||
, {
|
||||
|
|
|
@ -3,8 +3,7 @@ module.exports = function screenKeyboardDirective() {
|
|||
restrict: 'E',
|
||||
template: require('./screen-keyboard.jade'),
|
||||
link: function (scope, element) {
|
||||
var input = element.find('input')
|
||||
|
||||
element.find('input')
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module.exports = function screenTouchDirective() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function (scope, element, attrs) {
|
||||
link: function () {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ module.exports = function textHistoryDirective() {
|
|||
return {
|
||||
restrict: 'A',
|
||||
template: '',
|
||||
link: function (scope, element, attrs) {
|
||||
link: function () {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ describe('TimelineService', function() {
|
|||
|
||||
beforeEach(angular.mock.module(require('./').name));
|
||||
|
||||
it('should ...', inject(function(TimelineService) {
|
||||
it('should ...', inject(function() {
|
||||
|
||||
//expect(TimelineService.doSomething()).toEqual('something');
|
||||
|
||||
|
|
|
@ -2,9 +2,9 @@ describe('upload', function() {
|
|||
|
||||
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')
|
||||
|
||||
|
|
|
@ -25,13 +25,6 @@ module.exports = function ActivityCtrl($scope, gettext, TimelineService) {
|
|||
serial: $scope.device.serial
|
||||
})
|
||||
|
||||
// $scope.timeline.push({
|
||||
// title: title,
|
||||
// message: message,
|
||||
// serial: angular.copy($scope.device.serial),
|
||||
// time: Date.now()
|
||||
// })
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
.widget-container.scrollableX.messages.stf-activity(ng-controller='ActivityCtrl')
|
||||
//.heading
|
||||
i.fa
|
||||
span(translate) Activity
|
||||
.widget-content.padded
|
||||
|
||||
ul(ng-repeat='line in timeline.lines')
|
||||
|
@ -24,27 +21,3 @@
|
|||
div
|
||||
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
|
||||
|
||||
|
||||
|
|
|
@ -5,5 +5,9 @@
|
|||
div(ng-include='"control-panes/advanced/input/input.jade"')
|
||||
.col-md-6
|
||||
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
|
||||
div(ng-include='"control-panes/advanced/maintenance/maintenance.jade"')
|
||||
|
|
|
@ -4,6 +4,7 @@ module.exports = angular.module('stf.advanced', [
|
|||
require('./input').name,
|
||||
// require('./run-js').name,
|
||||
// require('./usb').name,
|
||||
require('./vnc').name,
|
||||
require('./port-forwarding').name,
|
||||
require('./maintenance').name
|
||||
])
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
div
|
||||
h6(translate) Special Keys
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
h6(translate) Volume
|
||||
|
@ -38,58 +38,8 @@
|
|||
i.fa.fa-step-forward
|
||||
button(tooltip='{{ "Fast Forward" | translate }}', ng-click='press("media_fast_forward")').btn.btn-primary.btn-xs
|
||||
i.fa.fa-fast-forward
|
||||
//h6 Physical Media
|
||||
//.btn-group
|
||||
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
|
||||
//h6 D-pad
|
||||
//table.special-keys-dpad-buttons
|
||||
tr
|
||||
td
|
||||
td
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.widget-container.fluid-height.stf-port-forwarding(ng-controller='PortForwardingCtrl')
|
||||
.heading
|
||||
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
|
||||
|
||||
button.btn.pull-right.btn-sm.btn-primary-outline(
|
||||
|
@ -44,7 +44,3 @@
|
|||
td
|
||||
button.btn.btn-sm.btn-danger-outline(ng-click='removeRow(forward)')
|
||||
i.fa.fa-trash-o
|
||||
//.checkbox
|
||||
label
|
||||
input(type='checkbox', value='')
|
||||
span(translate) Always forward on connect
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
module.exports = function RunJsCtrl($scope) {
|
||||
module.exports = function RunJsCtrl() {
|
||||
|
||||
}
|
||||
|
|
|
@ -3,11 +3,7 @@
|
|||
.heading
|
||||
i.fa.fa-code
|
||||
span(translate) Run JavaScript
|
||||
//form.form-inline
|
||||
.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')
|
||||
.modal-header
|
||||
h2 Save snippet
|
||||
|
@ -29,24 +25,6 @@
|
|||
li.divider
|
||||
li
|
||||
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
|
||||
button.btn.btn-sm.btn-primary-outline(ng-click='injectJS()', ng-disabled='!snippet.editorText')
|
||||
i.fa.fa-play
|
||||
|
@ -64,14 +42,12 @@
|
|||
span {{ result.deviceName }}
|
||||
td(width='30%', title="'Returns'", sortable='prettyValue')
|
||||
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 }}
|
||||
div(ng-hide='result.isObject')
|
||||
.value-next-to-progress {{ result.value }}
|
||||
td(width='40%', ng-show='result.isSpecialValue')
|
||||
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.label.label-info Object
|
||||
div(ng-show='result.isFunction')
|
||||
|
@ -84,7 +60,6 @@
|
|||
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 }")
|
||||
span {{ result.value.toString() }}
|
||||
//span {{ result.value.toString() | capitalize }}
|
||||
tab(heading='Raw')
|
||||
pre.selectable {{results | json}}
|
||||
clear-button(ng-click='clear()', ng-disabled='!results.length')
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
module.exports = function UsbCtrl($scope) {
|
||||
module.exports = function UsbCtrl() {
|
||||
|
||||
}
|
||||
|
|
12
res/app/control-panes/advanced/vnc/index.js
Normal file
12
res/app/control-panes/advanced/vnc/index.js
Normal 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'))
|
11
res/app/control-panes/advanced/vnc/vnc-controller.js
Normal file
11
res/app/control-panes/advanced/vnc/vnc-controller.js
Normal 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
Loading…
Add table
Add a link
Reference in a new issue