feat: init commit

This commit is contained in:
Simon Chan 2020-01-31 16:39:49 +08:00
parent a0f5107a60
commit 1e7e66670d
11 changed files with 1035 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules
lib

43
README.md Normal file
View file

@ -0,0 +1,43 @@
# Yet Another WebADB
Connect to your Android phones from everything that can run (supported) web browser, including PC, mac, and even another Android phone.
Inspired by [webadb.js](https://github.com/webadb/webadb.js), but completely rewritten.
## How does it work
Currently only the interactive shell (`adb shell`) is implemented, but I think it's the most difficult but interesting part.
WebUSB API gives JavaScript running in supported web browsers access to USB devices, including Android phones.
ADB uses a fairly simple protocol to commnunicate, so it's pretty easy to reimplement with JavaScript.
`adb shell`, the interactive shell, uses plain PTY protocol, and [xterm.js](https://github.com/xtermjs/xterm.js/) can handle it very well.
## Build
```shell
npm run build
```
## Run
```shell
npm start
```
And navigate to `http://localhost:8080/test.html`.
WebUSB API requires a [secure context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts) (basicly means HTTPS).
Chrome will treat `localhost` as one, but if you want to access test server running on another machine, you can configure you Chrome as following:
1. Open `chrome://flags/#unsafely-treat-insecure-origin-as-secure`
2. Add the protocol and domain part of your url (e.g. `http://192.168.0.100:8080`) to the input box
3. Choose `Enable` from the dropdown menu
4. Restart your browser
## Useful links
* [ADB protocol overview](https://github.com/aosp-mirror/platform_system_core/blob/master/adb/OVERVIEW.TXT)
* [ADB commands](https://github.com/aosp-mirror/platform_system_core/blob/d7c1bc73dc5b4e43b8288d43052a8b8890c4bf5a/adb/SERVICES.TXT#L145)

404
package-lock.json generated Normal file
View file

@ -0,0 +1,404 @@
{
"name": "webadb.js",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@rollup/plugin-commonjs": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-11.0.1.tgz",
"integrity": "sha512-SaVUoaLDg3KnIXC5IBNIspr1APTYDzk05VaYcI6qz+0XX3ZlSCwAkfAhNSOxfd5GAdcm/63Noi4TowOY9MpcDg==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.0.0",
"estree-walker": "^0.6.1",
"is-reference": "^1.1.2",
"magic-string": "^0.25.2",
"resolve": "^1.11.0"
},
"dependencies": {
"estree-walker": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
"integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
"dev": true
}
}
},
"@rollup/plugin-node-resolve": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.0.0.tgz",
"integrity": "sha512-+vOx2+WMBMFotYKM3yYeDGZxIvcQ7yO4g+SuKDFsjKaq8Lw3EPgfB6qNlp8Z/3ceDCEhHvC9/b+PgBGwDQGbzQ==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.0.0",
"@types/resolve": "0.0.8",
"builtin-modules": "^3.1.0",
"is-module": "^1.0.0",
"resolve": "^1.11.1"
}
},
"@rollup/plugin-typescript": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-3.0.0.tgz",
"integrity": "sha512-O6915Ril3+Q0B4P898PULAcPFZfPuatEB/4nox7bnK48ekGrmamMYhMB5tOqWjihEWrw4oz/NL+c+/kS3Fk95g==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.0.1",
"resolve": "^1.14.1"
}
},
"@rollup/pluginutils": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.0.6.tgz",
"integrity": "sha512-Nb6U7sg11v8D+E4mxRxwT+UumUL7MSnwI8V1SJB3THyW2MOGD/Q6GyxLtpnjrbT3zTRPSozzDMyVZwemgldO3w==",
"dev": true,
"requires": {
"estree-walker": "^1.0.1"
}
},
"@types/estree": {
"version": "0.0.42",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.42.tgz",
"integrity": "sha512-K1DPVvnBCPxzD+G51/cxVIoc2X8uUVl1zpJeE6iKcgHMj4+tbat5Xu4TjV7v2QSDbIeAfLi2hIk+u2+s0MlpUQ==",
"dev": true
},
"@types/node": {
"version": "13.5.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.5.2.tgz",
"integrity": "sha512-Fr6a47c84PRLfd7M7u3/hEknyUdQrrBA6VoPmkze0tcflhU5UnpWEX2kn12ktA/lb+MNHSqFlSiPHIHsaErTPA==",
"dev": true
},
"@types/resolve": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
"integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/w3c-web-usb": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@types/w3c-web-usb/-/w3c-web-usb-1.0.4.tgz",
"integrity": "sha512-aaOB3EL5WCWBBOYX7W1MKuzspOM9ZJI9s3iziRVypr1N+QyvIgXzCM4lm1iiOQ1VFzZioUPX9bsa23myCbKK4A==",
"dev": true
},
"@yume-chan/async-operation-manager": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@yume-chan/async-operation-manager/-/async-operation-manager-1.0.1.tgz",
"integrity": "sha512-ju38Oa9HKwpr5WgpuASCWQFak/02XuF1S8dET7n2eKYMmdxnMXIJHM+eLWqpwFouqkHu7mkeeJavvBWXGz257Q=="
},
"acorn": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz",
"integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==",
"dev": true
},
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
}
},
"basic-auth": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz",
"integrity": "sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ=",
"dev": true
},
"builtin-modules": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz",
"integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==",
"dev": true
},
"colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==",
"dev": true
},
"corser": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
"integrity": "sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=",
"dev": true
},
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
},
"ecstatic": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/ecstatic/-/ecstatic-3.3.2.tgz",
"integrity": "sha512-fLf9l1hnwrHI2xn9mEDT7KIi22UDqA2jaCwyCbSUJh9a1V+LEUSL/JO/6TIz/QyuBURWUHrFL5Kg2TtO1bkkog==",
"dev": true,
"requires": {
"he": "^1.1.1",
"mime": "^1.6.0",
"minimist": "^1.1.0",
"url-join": "^2.0.5"
}
},
"estree-walker": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
"dev": true
},
"eventemitter3": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz",
"integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==",
"dev": true
},
"follow-redirects": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.10.0.tgz",
"integrity": "sha512-4eyLK6s6lH32nOvLLwlIOnr9zrL8Sm+OvW4pVTJNoXeGzYIkHVf+pADQi+OJ0E67hiuSLezPVPyBcIZO50TmmQ==",
"dev": true,
"requires": {
"debug": "^3.0.0"
}
},
"he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
"http-proxy": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz",
"integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==",
"dev": true,
"requires": {
"eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
"requires-port": "^1.0.0"
}
},
"http-server": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/http-server/-/http-server-0.12.1.tgz",
"integrity": "sha512-T0jB+7J7GJ2Vo+a4/T7P7SbQ3x2GPDnqRqQXdfEuPuUOmES/9NBxPnDm7dh1HGEeUWqUmLUNtGV63ZC5Uy3tGA==",
"dev": true,
"requires": {
"basic-auth": "^1.0.3",
"colors": "^1.3.3",
"corser": "^2.0.1",
"ecstatic": "^3.3.2",
"http-proxy": "^1.17.0",
"opener": "^1.5.1",
"optimist": "~0.6.1",
"portfinder": "^1.0.20",
"secure-compare": "3.0.1",
"union": "~0.5.0"
}
},
"is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
"dev": true
},
"is-reference": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.1.4.tgz",
"integrity": "sha512-uJA/CDPO3Tao3GTrxYn6AwkM4nUPJiGGYu5+cB8qbC7WGFlrKZbiRo7SFKxUAEpFUfiHofWCXBUNhvYJMh+6zw==",
"dev": true,
"requires": {
"@types/estree": "0.0.39"
},
"dependencies": {
"@types/estree": {
"version": "0.0.39",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
"dev": true
}
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"dev": true
},
"magic-string": {
"version": "0.25.6",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.6.tgz",
"integrity": "sha512-3a5LOMSGoCTH5rbqobC2HuDNRtE2glHZ8J7pK+QZYppyWA36yuNpsX994rIY2nCuyP7CZYy7lQq/X2jygiZ89g==",
"dev": true,
"requires": {
"sourcemap-codec": "^1.4.4"
}
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true
},
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"dev": true,
"requires": {
"minimist": "0.0.8"
},
"dependencies": {
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"dev": true
}
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
},
"opener": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz",
"integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==",
"dev": true
},
"optimist": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
"integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
"dev": true,
"requires": {
"minimist": "~0.0.1",
"wordwrap": "~0.0.2"
},
"dependencies": {
"minimist": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=",
"dev": true
}
}
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"portfinder": {
"version": "1.0.25",
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.25.tgz",
"integrity": "sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg==",
"dev": true,
"requires": {
"async": "^2.6.2",
"debug": "^3.1.1",
"mkdirp": "^0.5.1"
}
},
"qs": {
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.1.tgz",
"integrity": "sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA==",
"dev": true
},
"requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
"dev": true
},
"resolve": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.0.tgz",
"integrity": "sha512-+hTmAldEGE80U2wJJDC1lebb5jWqvTYAfm3YZ1ckk1gBr0MnCqUKlwK1e+anaFljIl+F5tR5IoZcm4ZDA1zMQw==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
}
},
"rollup": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-1.30.1.tgz",
"integrity": "sha512-Uus8mwQXwaO+ZVoNwBcXKhT0AvycFCBW/W8VZtkpVGsotRllWk9oldfCjqWmTnFRI0y7x6BnEqSqc65N+/YdBw==",
"dev": true,
"requires": {
"@types/estree": "*",
"@types/node": "*",
"acorn": "^7.1.0"
}
},
"secure-compare": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
"integrity": "sha1-8aAymzCLIh+uN7mXTz1XjQypmeM=",
"dev": true
},
"sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
"tslib": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
},
"typescript": {
"version": "3.7.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz",
"integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==",
"dev": true
},
"union": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
"integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
"dev": true,
"requires": {
"qs": "^6.4.0"
}
},
"url-join": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/url-join/-/url-join-2.0.5.tgz",
"integrity": "sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=",
"dev": true
},
"wordwrap": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
"integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=",
"dev": true
},
"xterm": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/xterm/-/xterm-4.3.0.tgz",
"integrity": "sha512-6dnrC4nxgnRKQzIWwC5HA0mnT9/rpDPZflUIr24gdcdSMTKM1QQcor4qQ/xz4Zerz6AIL/CuuBPypFfzsB63dQ=="
}
}
}

35
package.json Normal file
View file

@ -0,0 +1,35 @@
{
"name": "webadb.js",
"version": "1.0.0",
"description": "``` let webusb = await Adb.open(\"WebUSB\"); let adb = await webusb.connectAdb(\"host::\"); let shell = await adb.shell(\"uname -a\"); console.log(await shell.receive()); ```",
"main": "webadb.js",
"scripts": {
"start": "hs",
"build": "rollup -c"
},
"repository": {
"type": "git",
"url": "git+https://github.com/webadb/webadb.js.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/webadb/webadb.js/issues"
},
"homepage": "https://github.com/webadb/webadb.js#readme",
"devDependencies": {
"@rollup/plugin-commonjs": "^11.0.1",
"@rollup/plugin-node-resolve": "^7.0.0",
"@rollup/plugin-typescript": "^3.0.0",
"@types/w3c-web-usb": "^1.0.4",
"http-server": "^0.12.1",
"rollup": "^1.30.1",
"typescript": "^3.7.5"
},
"dependencies": {
"@yume-chan/async-operation-manager": "^1.0.1",
"tslib": "^1.10.0",
"xterm": "^4.3.0"
}
}

22
rollup.config.js Normal file
View file

@ -0,0 +1,22 @@
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
export default {
input: 'src/test.ts',
plugins: [
resolve(),
typescript(),
commonjs({
extensions: ['.js', '.ts'],
namedExports: {
'xterm': ['Terminal'],
},
}),
],
output: {
dir: 'lib',
format: 'iife',
sourcemap: true,
},
};

70
src/packet.ts Normal file
View file

@ -0,0 +1,70 @@
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
export class AdbPacket {
public static parse(buffer: ArrayBuffer): AdbPacket {
const command = textDecoder.decode(buffer.slice(0, 4));
const view = new DataView(buffer);
const arg0 = view.getUint32(4, true);
const arg1 = view.getUint32(8, true);
const payloadLength = view.getUint32(12, true);
const packet = new AdbPacket(command, arg0, arg1, undefined);
packet._payloadLength = payloadLength;
return packet;
}
public command: string;
public arg0: number;
public arg1: number;
private _payloadLength!: number;
public get payloadLength(): number { return this._payloadLength; }
private _payload: ArrayBuffer | undefined;
public get payload(): ArrayBuffer | undefined { return this._payload; }
public set payload(value: ArrayBuffer | undefined) {
if (value !== undefined) {
this._payloadLength = value.byteLength;
this._payload = value;
} else {
this._payloadLength = 0;
this._payload = undefined;
}
}
public constructor(command: string, arg0: number, arg1: number, payload?: string | ArrayBuffer) {
if (command.length !== 4) {
throw new TypeError('length of command must be 4');
}
this.command = command;
this.arg0 = arg0;
this.arg1 = arg1;
if (typeof payload === "string") {
this.payload = textEncoder.encode(payload + '\0').buffer;
} else {
this.payload = payload;
}
}
public toBuffer(): ArrayBuffer {
const buffer = new ArrayBuffer(24);
const array = new Uint8Array(buffer);
const view = new DataView(buffer);
textEncoder.encodeInto(this.command, array);
view.setUint32(4, this.arg0, true);
view.setUint32(8, this.arg1, true);
view.setUint32(12, this.payloadLength, true);
view.setUint32(16, /* checksum */ 0, true);
view.setUint32(20, /* magic */ view.getUint32(0, true) ^ 0xFFFFFFFF, true);
return buffer;
}
}

114
src/stream.ts Normal file
View file

@ -0,0 +1,114 @@
import { PromiseResolver } from '@yume-chan/async-operation-manager';
import { WebAdb } from './webadb';
import { IEvent } from 'xterm';
interface IListener<T> {
(e: T): void;
}
export interface IEventEmitter<T> {
event: IEvent<T>;
fire(data: T): void;
dispose(): void;
}
export class EventEmitter<T> implements IEventEmitter<T> {
private _listeners: IListener<T>[] = [];
private _event?: IEvent<T>;
private _disposed: boolean = false;
public get event(): IEvent<T> {
if (!this._event) {
this._event = (listener: (e: T) => any) => {
this._listeners.push(listener);
const disposable = {
dispose: () => {
if (!this._disposed) {
for (let i = 0; i < this._listeners.length; i++) {
if (this._listeners[i] === listener) {
this._listeners.splice(i, 1);
return;
}
}
}
}
};
return disposable;
};
}
return this._event;
}
public fire(data: T): void {
const queue: IListener<T>[] = [];
for (let i = 0; i < this._listeners.length; i++) {
queue.push(this._listeners[i]);
}
for (let i = 0; i < queue.length; i++) {
queue[i].call(undefined, data);
}
}
public dispose(): void {
if (this._listeners) {
this._listeners.length = 0;
}
this._disposed = true;
}
}
class AutoResetEvent {
private _list: PromiseResolver<void>[] = [];
private _blocking: boolean = false;
public wait(): Promise<void> {
if (!this._blocking) {
this._blocking = true;
if (this._list.length === 0) {
return Promise.resolve();
}
}
const resolver = new PromiseResolver<void>();
this._list.push(resolver);
return resolver.promise;
}
public notify() {
if (this._list.length !== 0) {
this._list.pop()!.resolve();
} else {
this._blocking = false;
}
}
}
export class AdbStream {
private _writeMutex = new AutoResetEvent();
public onDataEvent: EventEmitter<ArrayBuffer> = new EventEmitter();
public get onData(): IEvent<ArrayBuffer> { return this.onDataEvent.event; }
private _adb: WebAdb;
public localId: number;
public remoteId: number;
public constructor(adb: WebAdb, localId: number, remoteId: number) {
this._adb = adb;
this.localId = localId;
this.remoteId = remoteId;
}
public async write(data: ArrayBuffer): Promise<void> {
await this._writeMutex.wait();
await this._adb.sendMessage('WRTE', this.localId, this.remoteId, data);
}
public ack(): void {
this._writeMutex.notify();
}
}

22
src/test.ts Normal file
View file

@ -0,0 +1,22 @@
import { Terminal } from 'xterm';
import { WebAdb } from './webadb.js';
document.getElementById('start')!.onclick = async () => {
const adb = await WebAdb.open();
await adb.connect();
const textEncoder = new TextEncoder();
const shell = await adb.shell();
const terminal = new Terminal({
});
terminal.open(document.getElementById('terminal')!);
terminal.onData(data => {
const { buffer } = textEncoder.encode(data);
shell.write(buffer);
});
shell.onData(data => {
terminal.write(new Uint8Array(data));
});
};

244
src/webadb.ts Normal file
View file

@ -0,0 +1,244 @@
import { AdbPacket } from './packet';
import { AdbStream } from './stream';
import AsyncOperationManager from '@yume-chan/async-operation-manager';
export const AdbDeviceFilter: USBDeviceFilter = { classCode: 0xFF, subclassCode: 0x42, protocolCode: 1 };
export class WebAdb {
public static async open() {
const device = await navigator.usb.requestDevice({ filters: [] });
await device.open();
const webadb = new WebAdb(device);
await webadb.initialize();
return webadb;
}
private _device: USBDevice;
private _inEndpointNumber!: number;
private _outEndpointNumber!: number;
private _alive = true;
private _looping = false;
private _streamInitializer = new AsyncOperationManager();
private _streams = new Map<number, AdbStream>();
public constructor(device: USBDevice) {
this._device = device;
}
private async initialize(): Promise<void> {
for (const configuration of this._device.configurations) {
for (const interface_ of configuration.interfaces) {
for (const alternate of interface_.alternates) {
if (alternate.interfaceProtocol === AdbDeviceFilter.protocolCode &&
alternate.interfaceClass === AdbDeviceFilter.classCode &&
alternate.interfaceSubclass === AdbDeviceFilter.subclassCode) {
if (this._device.configuration?.configurationValue !== configuration.configurationValue) {
await this._device.selectConfiguration(configuration.configurationValue);
}
if (!interface_.claimed) {
await this._device.claimInterface(interface_.interfaceNumber);
}
if (interface_.alternate.alternateSetting !== alternate.alternateSetting) {
await this._device.selectAlternateInterface(interface_.interfaceNumber, alternate.alternateSetting);
}
this._inEndpointNumber = this.getEndpointNumber(alternate.endpoints, 'in');
this._outEndpointNumber = this.getEndpointNumber(alternate.endpoints, 'out');
return;
}
}
}
}
}
private async receiveLoop(): Promise<void> {
if (this._looping) {
return;
}
this._looping = true;
while (this._alive) {
const response = await this.receiveMessage();
switch (response.command) {
case 'OKAY':
this._streamInitializer.resolve(response.arg1, response.arg0);
this._streams.get(response.arg1)?.ack();
break;
case 'CLSE':
if (response.arg0 === 0) {
this._streamInitializer.reject(response.arg1, new Error('open failed'));
} else {
this._streams.delete(response.arg1);
}
break;
case 'WRTE':
this._streams.get(response.arg1)?.onDataEvent.fire(response.payload!);
await this.sendMessage('OKAY', response.arg1, response.arg0);
break;
default:
this._device.close();
throw new Error('unknown command');
}
}
}
public async connect() {
const version = 0x01000001;
await this.sendMessage('CNXN', version, 256 * 1024, 'host::');
while (true) {
const response = await this.receiveMessage();
switch (response.command) {
case 'CNXN':
if (response.arg0 !== version) {
this._device.close();
throw new Error('version mismatch');
}
return;
case 'AUTH':
if (response.arg0 !== 1) {
this._device.close();
throw new Error('unknwon auth type');
}
const key = await this.generateKey();
const publicKey = await crypto.subtle.exportKey('spki', key.publicKey);
await this.sendMessage('AUTH', 3, 0, this.toBase64(publicKey));
break;
default:
this._device.close();
throw new Error('unknown command');
}
}
}
public async shell(): Promise<AdbStream> {
const { id: localId, promise: initializer } = this._streamInitializer.add<number>();
await this.sendMessage('OPEN', localId, 0, 'shell:');
this.receiveLoop();
const remoteId = await initializer;
const stream = new AdbStream(this, localId, remoteId);
this._streams.set(localId, stream);
return stream;
}
public async sendMessage(command: string, arg0: number, arg1: number, payload?: string | ArrayBuffer): Promise<void> {
const packet = new AdbPacket(command, arg0, arg1, payload);
console.log('send', command, arg0, arg1, payload);
await this._device.transferOut(this._outEndpointNumber, packet.toBuffer());
if (packet.payloadLength !== 0) {
await this._device.transferOut(this._outEndpointNumber, packet.payload!);
}
}
public async receiveMessage() {
console.log('receiving');
const header = await this.receiveData(24);
const packet = AdbPacket.parse(header);
if (packet.payloadLength !== 0) {
packet.payload = await this.receiveData(packet.payloadLength);
}
console.log('receive', packet.command, packet.arg0, packet.arg1, packet.payload);
return packet;
}
private getEndpointNumber(endpoints: USBEndpoint[], direction: USBDirection, type: USBEndpointType = 'bulk') {
for (const endpoint of endpoints) {
if (endpoint.direction === direction &&
endpoint.type === type) {
return endpoint.endpointNumber;
}
}
throw new Error('Cannot find endpoint');
}
private async receiveData(length: number): Promise<ArrayBuffer> {
const result = await this._device.transferIn(this._inEndpointNumber, length);
if (result.status === 'stall') {
console.log('clear halt in');
await this._device.clearHalt('in', this._inEndpointNumber);
}
return result.data!.buffer;
}
private async generateKey() {
return await crypto.subtle.generateKey({
name: 'RSASSA-PKCS1-v1_5',
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: { name: 'SHA-1' },
}, false, ['sign', 'verify']);
}
private toBase64(arraybuffer: ArrayBuffer) {
let characterSet = [];
const pairs = [
['A', 'Z'],
['a', 'z'],
['0', '9'],
].map(pair => pair.map(character => character.charCodeAt(0)));
for (const [begin, end] of pairs) {
for (let i = begin; i <= end; i += 1) {
characterSet.push(String.fromCharCode(i));
}
}
characterSet.push('+', '/');
const array = new Uint8Array(arraybuffer);
const length = arraybuffer.byteLength;
const remainder = length % 3;
let result = '';
for (let i = 0; i < length - remainder; i += 3) {
// aaaaaabb
const x = array[i];
// bbbbcccc
const y = array[i + 1];
// ccdddddd
const z = array[i + 2];
const a = x >> 2;
const b = ((x & 0b11) << 4) | (y >> 4);
const c = ((y & 0b1111) << 2) | (z >> 6);
const d = z & 0b111111;
result += characterSet[a] + characterSet[b] + characterSet[c] + characterSet[d];
}
if (remainder === 1) {
// aaaaaabb
const x = array[length - 1];
const a = x >> 2;
const b = ((x & 0b11) << 4);
result += characterSet[a] + characterSet[b] + '==';
} else if (remainder === 2) {
// aaaaaabb
const x = array[length - 2];
// bbbbcccc
const y = array[length - 1];
const a = x >> 2;
const b = ((x & 0b11) << 4) | (y >> 4);
const c = ((y & 0b1111) << 2);
result = characterSet[a] + characterSet[b] + characterSet[c] + '=';
}
return result;
}
}

13
test.html Normal file
View file

@ -0,0 +1,13 @@
<html>
<head>
<link rel="stylesheet" href="./node_modules/xterm/css/xterm.css">
</head>
<body>
<button id="start">Start</button>
<div id="terminal"></div>
<script src="lib/test.js"></script>
</body>
</html>

66
tsconfig.json Normal file
View file

@ -0,0 +1,66 @@
{
"compilerOptions": {
/* Basic Options */
"incremental": true, // /* Enable incremental compilation */
"target": "ES2018", // /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "ESNext", // /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": [ // /* Specify library files to be included in the compilation. */
"DOM",
"ESNext"
],
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"declaration": true, // /* Generates corresponding '.d.ts' file. */
"declarationMap": true, // /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "lib/index.js", // /* Concatenate and emit output to single file. */
"outDir": "lib", // /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, // /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", // /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, // /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"forceConsistentCasingInFileNames": true ///* Disallow inconsistently-cased references to the same file. */
},
"include": [
"src"
]
}