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

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

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

14
.eslintrc Normal file
View file

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

View file

@ -2,6 +2,7 @@
.DS_Store
/*.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/

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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"
}
}

View file

@ -56,6 +56,12 @@ The app role can contain any of the following units. You may distribute them as
* [stf-triproxy-dev.service](#stf-triproxy-devservice)
* [stf-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
View file

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

View file

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

View file

@ -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)
})

View file

@ -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, [

View file

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

View file

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

View file

@ -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')

View file

@ -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

View file

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

View file

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

View file

@ -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)
})

View file

@ -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)
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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(

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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) {

View file

@ -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])

View file

@ -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()

View file

@ -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) {

View file

@ -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)
}

View file

@ -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)

View file

@ -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

View file

@ -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
View file

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

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

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

View file

@ -17,6 +17,8 @@ enum MessageType {
PhysicalIdentifyMessage = 29;
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;

View file

@ -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"

View file

@ -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;*/
}

View file

@ -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')
}

View file

@ -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 {

View file

@ -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)

View file

@ -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');
}));
}))
})

View file

@ -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')

View file

@ -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

View file

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

View file

@ -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,

View file

@ -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');

View file

@ -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'))

View file

@ -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', [

View file

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

View file

@ -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() {

View file

@ -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');

View file

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

View file

@ -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');

View file

@ -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

View file

@ -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');

View file

@ -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

View file

@ -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')

View file

@ -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');

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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'

View file

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

View file

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

View file

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

View file

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

View file

@ -157,9 +157,10 @@ module.exports = function ControlServiceFactory(
return sendTwoWay('device.reboot')
}
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

View file

@ -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')
}));
}))
})

View file

@ -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')

View file

@ -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')
}));
}))
})

View file

@ -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')

View file

@ -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')
}));
}))
})

View file

@ -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')
}));
}))
})

View file

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

View file

@ -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')
}
}

View file

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

View file

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

View file

@ -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');

View file

@ -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')

View file

@ -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()
// })
}

View file

@ -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

View file

@ -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"')

View file

@ -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
])

View file

@ -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

View file

@ -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

View file

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

View file

@ -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')

View file

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

View file

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

View file

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

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