89 Commits
Author SHA1 Message Date
David Ventura 23f765371b Add build action 2024-04-26 10:32:56 +02:00
David Ventura e77a3c27fe move actions to package.json 2024-04-26 10:32:56 +02:00
David Ventura 04baa7ad9a add usage notes 2024-04-26 10:32:56 +02:00
David Ventura 639a78419e refactor to allow bundling 2024-04-26 10:32:56 +02:00
David Ventura 1c6312e0c9 Merge pull request #7 from adrcunha/adrcunha-patch-2
Set discovery_ip in http_server.ts back to 192.168.1.255
2024-04-24 09:03:26 +02:00
David Ventura 1dea1373e3 Merge pull request #6 from adrcunha/patch-1
Update opts in pair.ts
2024-04-24 09:03:00 +02:00
Adriano Cunha 783e61cba2 Set discovery_ip in http_server.ts back to 192.168.1.255
The previous value (192.168.40.104) seems to be leftover debugging, as it's not a broadcast address and thus causes the server not to find any cameras.
2024-04-23 22:12:41 -07:00
Adriano Cunha 69a8b2e172 Update opts in pair.ts
Update opts in pair.ts to include attempt_to_fix_packet_loss.

Otherwise it will fail with:

```
cmd/pair.ts:16:29 - error TS2345: Argument of type '{ debug: boolean; ansi: boolean; discovery_ip: string; }' is not assignable to parameter of type 'opt'.
  Property 'attempt_to_fix_packet_loss' is missing in type '{ debug: boolean; ansi: boolean; discovery_ip: string; }' but required in type 'opt'.
```
2024-04-23 22:05:11 -07:00
DavidVentura cf45216693 impl stopvideo 2024-02-02 12:07:07 +01:00
DavidVentura 737f0248b2 add support for audio streaming 2024-02-02 12:06:44 +01:00
DavidVentura 75f559a3ce consider the packet sequence in audio frames, as they are shared with jpeg frames 2024-02-02 12:06:16 +01:00
DavidVentura 2db1e8d9f3 add IRToggle 2024-02-01 18:09:19 +01:00
DavidVentura 2a55a7048f add note on packet loss 2024-02-01 18:06:47 +01:00
DavidVentura 25ae4fa72e attempt to re-stitch together JPEGs at the recovery markers upon dataloss 2024-02-01 17:56:27 +01:00
DavidVentura 2f2b6d302a refactor curimage into a slice of buffers & pass options via session 2024-02-01 17:56:08 +01:00
DavidVentura 92dbd8354c add vlan note 2024-02-01 17:54:11 +01:00
DavidVentura cfbdeaa0d0 cleanup dissector 2024-02-01 16:25:30 +01:00
DavidVentura 94070b6e33 stop masking commands 2024-02-01 13:14:39 +01:00
DavidVentura 85a4b71e46 debug XqStrDec 2024-02-01 13:14:29 +01:00
DavidVentura 2215cf5b5c validate env vars are set 2024-02-01 13:14:14 +01:00
DavidVentura dcbf6bf925 braindump 2024-02-01 13:08:27 +01:00
DavidVentura edd4f5d6f5 add node with server decoder 2024-02-01 12:51:46 +01:00
DavidVentura bc2856eb07 add server decode script 2024-02-01 12:51:36 +01:00
DavidVentura 599488f0bb add pic 2024-01-31 18:49:40 +01:00
DavidVentura 89435cd4ec fix import 2024-01-31 18:28:27 +01:00
DavidVentura 9a37f97960 add build step 2024-01-31 18:28:16 +01:00
DavidVentura 7d2160f07f support multiple streams concurrently 2024-01-31 18:06:52 +01:00
DavidVentura 5379266fc8 implement pair command 2024-01-31 16:38:23 +01:00
DavidVentura a93c0badff actually use the onlogin callback 2024-01-31 16:38:00 +01:00
DavidVentura 9685f8008e allow onLogin CB to be specified 2024-01-31 16:32:06 +01:00
DavidVentura f7eb287d5b add a trivial home page to link multiple cameras 2024-01-31 16:21:03 +01:00
DavidVentura 33e7aeaacc cleanup punchpkt 2024-01-31 16:13:20 +01:00
DavidVentura c12c432e71 refactor for multiple sessions 2024-01-31 16:11:22 +01:00
DavidVentura 2924eaab79 typo 2024-01-31 14:34:38 +01:00
DavidVentura 5f6d5fefe3 remove unnecessary outgoing port 2024-01-31 14:33:43 +01:00
DavidVentura 89534822ab more refactor 2024-01-31 14:30:30 +01:00
DavidVentura 2d2e56231a rename 2024-01-31 12:16:22 +01:00
DavidVentura 98800d97bf rename 2024-01-31 12:16:10 +01:00
DavidVentura eecae5c9bf nicer connected check 2024-01-31 12:14:20 +01:00
DavidVentura 0b8c3a1cc5 handle connect/disconnect events 2024-01-31 12:14:06 +01:00
DavidVentura d70bef918f make audio optional 2024-01-31 12:11:32 +01:00
DavidVentura 7bdcc54bc1 abstract server a bit better 2024-01-31 12:01:00 +01:00
DavidVentura 9d671f18c9 abstract server a bit better 2024-01-31 11:56:33 +01:00
DavidVentura 0b2ac1a118 cleanup 2024-01-31 11:47:22 +01:00
DavidVentura 3045c7663a try raw 2024-01-31 10:59:55 +01:00
DavidVentura 7229f5781e add WifiSettingsSet 2024-01-31 10:58:44 +01:00
DavidVentura 5204020e7b better alignments 2024-01-31 10:33:57 +01:00
DavidVentura 51ec333ad2 add "decrypter" 2024-01-31 10:24:06 +01:00
DavidVentura a5734e47cc add note on bricking 2024-01-31 10:06:22 +01:00
DavidVentura 7530664fc9 update readme 2024-01-31 10:05:40 +01:00
DavidVentura 2beb5e97e5 add serial note 2024-01-30 20:14:36 +01:00
DavidVentura baef683b66 note on bricking 2024-01-30 20:13:46 +01:00
DavidVentura 30df465edf implement SetVideoResolution 2024-01-30 15:19:34 +01:00
DavidVentura 369ffc8461 add videoparamset/get 2024-01-30 15:19:00 +01:00
DavidVentura e789491f49 do not log data payloads 2024-01-30 15:18:48 +01:00
DavidVentura 971f741d35 refuse connections if no cameras are available 2024-01-30 13:46:52 +01:00
DavidVentura 550f805c62 clean the p2palive from debug output 2024-01-30 13:46:23 +01:00
DavidVentura c5764b71a6 handle duplicate/missing packets when dealing with frames 2024-01-30 13:43:09 +01:00
DavidVentura 9b78eb6c27 add note on capture delay 2024-01-30 13:36:00 +01:00
DavidVentura fb8fae8e96 add u32LE u16LE 2024-01-30 11:37:38 +01:00
DavidVentura 7e3fbbfa47 impl reboot & p2pclose 2024-01-30 11:37:04 +01:00
DavidVentura b9b37c803d cleanup handlers 2024-01-30 11:36:42 +01:00
DavidVentura c5b9fdfa6c make hooks cleaner 2024-01-30 11:35:47 +01:00
DavidVentura 01245454c5 add comments to commands 2024-01-30 11:35:35 +01:00
DavidVentura 795db8a2a7 cleanup 2024-01-29 22:19:54 +01:00
DavidVentura 4559b44cc2 add some new commands 2024-01-29 22:19:32 +01:00
DavidVentura ebb9d8b081 send P2pAlive if havent heard from the other side for 500ms 2024-01-29 22:05:53 +01:00
DavidVentura c8259c95fa strip null bytes on readString 2024-01-29 16:31:56 +01:00
DavidVentura 3d5d250807 refactor 2024-01-29 16:31:56 +01:00
DavidVentura 8671b32475 add note on frida server 2024-01-29 12:22:08 +01:00
DavidVentura a8bf3a09f1 adjust readme for http server 2024-01-29 12:16:39 +01:00
DavidVentura 3f217d5c32 add mjpeg server 2024-01-29 12:16:28 +01:00
DavidVentura 419e5fcc82 update readme 2024-01-29 11:43:09 +01:00
DavidVentura a049029734 rename proto 2024-01-29 11:42:46 +01:00
DavidVentura 8963125bcc fix types 2024-01-29 11:39:15 +01:00
DavidVentura 0857d220d5 consider remoteinfo on connect 2024-01-29 11:39:01 +01:00
DavidVentura 1cfe82ee7a pass rinfo around 2024-01-29 11:38:23 +01:00
DavidVentura 38256ae465 update dissector 2024-01-29 11:03:01 +01:00
DavidVentura 058e3776c8 add notes on battery levels 2024-01-28 20:16:57 +01:00
DavidVentura 936257c562 tolerate da dataview 2024-01-28 20:16:42 +01:00
DavidVentura 323d82eaaf handle DevStatusAck and print a message 2024-01-28 20:16:31 +01:00
DavidVentura 3c0f56fa8a print decrypted values for 0xf1d0 2024-01-28 20:16:12 +01:00
DavidVentura 07342d6629 add DevStatus type 2024-01-28 20:16:00 +01:00
DavidVentura 3ca36ef542 implement devStatus 2024-01-28 20:15:49 +01:00
DavidVentura 3a34b60734 reset outgoingCommandId on connect 2024-01-28 20:15:11 +01:00
DavidVentura 4fd3cb9125 u32 swap 2024-01-28 20:14:50 +01:00
DavidVentura 95671dff0f fix add on dataview missing byteOffset from previous dv 2024-01-28 20:14:41 +01:00
DavidVentura 7d0b9260eb add missing import 2024-01-28 20:14:10 +01:00
DavidVentura affeb2967e add ip cam pdf 2024-01-28 17:02:27 +01:00
34 changed files with 2011 additions and 392 deletions
+36
View File
@@ -0,0 +1,36 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Node.js CI
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [16.x, 18.x, 20.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run build
- run: npm test
- uses: actions/upload-artifact@v4
with:
name: bundle-${{ matrix.node-version }}
path: dist/bin.cjs
+2
View File
@@ -1,3 +1,5 @@
bundle.js
venv
node_modules
build
dist/
+9 -2
View File
@@ -1,4 +1,4 @@
.PHONY: run hook install-wireshark-dissector test
.PHONY: run hook install-wireshark-dissector test build
bundle.js: frida-hooks.js func_replacements.js
~/node_modules/.bin/frida-compile -o $@ frida-hooks.js
@@ -8,8 +8,15 @@ venv: requirements.txt
./venv/bin/pip install -r requirements.txt
touch venv
build: node_modules
npm run tsc
npm run build
run: node_modules
./node_modules/.bin/ts-node --esm server.ts
./node_modules/.bin/ts-node --esm cmd/bin http_server --port=1234
pair: node_modules
./node_modules/.bin/ts-node --esm cmd/bin pair
hook: bundle.js venv
./venv/bin/python3 -u loader3.py
+140 -15
View File
@@ -1,26 +1,59 @@
Re-implementation of the "ilnk" protocol used on some cheap (\<$5) IP cameras (sometimes branded as 'X5' or 'A9').
Re-implementation of the "iLnk"/"iLnkP2P"/"PPPP" protocol used on some cheap (\<$5) IP cameras (sometimes branded as 'X5' or 'A9').
* Bought [here](https://www.aliexpress.com/item/1005006287788979.html).
* Waiting for [this A9 camera](https://www.aliexpress.com/item/1005006117593880.html) to validate support.
* Bought [this X5](https://www.aliexpress.com/item/1005006287788979.html) and [this A9](https://www.aliexpress.com/item/1005006117593880.html).
* App is [YsxLite](https://play.google.com/store/apps/details?id=com.ysxlite.cam&hl=en&gl=US)
Per [pictures](https://github.com/DavidVentura/cam-reverse/blob/master/pics/pcb.jpg?raw=true) the main chip is TXW817 ([chinese](https://www.taixin-semi.com/Product/ProductDetail?productId=306), [eng, google translate](https://www-taixin--semi-com.translate.goog/Product/ProductDetail?productId=306&_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=wapp))
Per pictures of the [X5](https://github.com/DavidVentura/cam-reverse/blob/master/pics/pcb.jpg?raw=true), [A9](https://github.com/DavidVentura/cam-reverse/blob/master/pics/pcb_a9.jpg?raw=true) the main chip is TXW817 ([chinese](https://www.taixin-semi.com/Product/ProductDetail?productId=306), [eng, google translate](https://www-taixin--semi-com.translate.goog/Product/ProductDetail?productId=306&_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=wapp))
The interesting implementation is in `libvdp.so`, part of the apk bundle.
Protocol reversing was done with a combination of static analysis of the shared object with [Ghidra](https://ghidra-sre.org/) and dynamic analysis with [Frida](https://frida.re/docs/javascript-api/).
The headers reversed with Ghidra are at `types/all.h`. They are almost not used by this minimal implementation though.
The hooks used with frida are at `frida-hooks.js`, but it's mostly a playground - some useful functions got deleted once I understood the protocol.
There's also a pretty crappy Wireshark dissector at `dissector.lua`. You can install it with `make install-wireshark-dissector`.
## Running
To execute the server, run `make run`; JPEG files and `audio.pcm` will be created in a folder named `captures`.
To execute the HTTP server, run `node bin.cjs`; you can access the JPEG stream at http://localhost:5000/ and (optionally) a file `audio.pcm` will be created.
There's no live-stream server built into this project yet.
```bash
$ node dist/bin.cjs http_server --help
start http server
Options:
--help Show help [boolean]
--version Show version number [boolean]
--debug [boolean] [default: false]
--ansi [boolean] [default: false]
--audio [boolean] [default: false]
--discovery_ip [default: "192.168.1.255"]
--attempt_to_fix_packet_loss [default: false]
--port HTTP Port to listen on [number] [default: 5000]
```
The roundtrip delay when using MJPEG is [~350ms](pics/delay.jpg?raw=true).
There's a basic UI which can display multiple cameras:
![](pics/web-ui.jpg?raw=true)
The server will send a broadcast packet every few seconds to discover all the cameras available; this means that it *must* run in the same broadcast domain (VLAN) as your cameras.
Clicking on the image will take you to a page that has audio streaming enabled.
## Pairing a new camera
Connect to the device's access point and run `node bin.cjs pair --ssid <SSID> --password <PASSWORD>`.
```bash
$ node dist/bin.cjs pair --help
configure a camera
Options:
--help Show help [boolean]
--version Show version number [boolean]
--debug [boolean] [default: false]
--ansi [boolean] [default: false]
--discovery_ip [default: "192.168.1.255"]
--attempt_to_fix_packet_loss [default: false]
--ssid [string] [required]
--password [string] [required]
```
## Protocol
@@ -89,6 +122,93 @@ sequenceDiagram
end
```
### Serial
The A9 cameras have a TX/RX test points - connecting with UART at 921600 8N1 gives _read only_ access to some debug logs.
### Discrepancies between cameras
1. Wifi Strength
- A9 reports '100%' strength
- X5 reports different strength values
I bricked two cameras by patching out part of the WiFi setup - unclear yet which commands.
After bricking itself, it reports very broken configuration via serial:
```
network interface: ƀ (Default)
MTU: 51050
MAC: 06 18 40 06 3e 51 b4 e2 c6 80 06 3f 77 30 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 74 00 00 00 01 00 00 00 9c ea 01 20 00 00 00 00 00 00 00 00 00 28 60 00 00 00 00 00 00 00 00 00 06 4e 00 20 2a 00 2a 00 80 00 00 00 00 00 ff ff ff ff ff ff 3e 51 b4 e2 c6 80 08 06 00 01 08 00 06 04 00 01 3e 51 b4 e2 c6 80 01 01 01 01 00 00 00 00 00 00 01 01 01 01 00 28 74 00 00 00 00 00 00 00 00 00 58 4e 00 20 48 00 48 00 80 00 00 00 00 00 ff ff ff ff ff ff 3e 51 b4 e2 c6 80 08 06 45 00 00 48 00 51 00 00 ff 11 c8 be 01 01 01 01 23 9c cc f7 7d 6c
FLAGS: DOWN LINK_DOWN IGMP
ip address: 1.1.1.1
gw address: 1.1.1.1
net mask : 1.1.1.1
network i
nterface: ^@^@
MTU: 0
MAC:
FLAGS: DOWN LINK_DOWN
ip address: 127.0.0.1
gw address: 127.0.0.1
net mask : 255.0.0.0
```
## Spyware
When connecting the camera to a network, it tries to send a HELLO (?) to 4 IP addresses:
```
139.155.68.77 - Shenzhen Tencent Computer Systems Company Limited
119.45.114.92 - Shenzhen Tencent Computer Systems Company Limited
162.62.63.154 - Tencent Building, Kejizhongyi Avenue
3.132.215.40 - ec2-3-132-215-40.us-east-2.compute.amazonaws.com
```
With the payload
```
0000 f1 10 00 28 42 41 54 43 00 00 00 00 00 09 4d 2c ...(BATC......M,
0010 48 56 44 43 53 00 00 00 08 00 02 01 00 00 6c 7d HVDCS.........l}
0020 65 28 a8 c0 00 00 00 00 00 00 00 00 e(..........
```
which is `DevLogin`
These addresses are decoded (script at `scripts/dec_svr.py`) from the string `SWPNPDPFLVAOLNSXPHSQPIEOPAIDENLXHXEHIFLKPGLRHUARSTLQEEEPSUIHPDLSPEAOICLOSQEMLPPALNIBIAERHZLKHXEJHYHUEIEHELEEEKEG`.
Every 8-10s
There are some other strings in the APK ending in `-$$` which decode to other ips/hostnames.
## Other stuff
These little cameras have quite some packet loss - I _tried_ to deal with it by splicing around it on the JPEG payloads, but it's probably wrong, I expected artifacts like this:
![](pics/packet_loss_good.jpg?raw=true)
but most of the time got:
![](pics/packet_loss_bad.jpg?raw=true)
which _moves_ the rest of the image, causing more visual noise.
For now, images on which there was packet loss get skipped. The algorithm to "fix" packet loss can be enabled as an option.
## Reversing
The interesting implementation is in `libvdp.so`, part of the apk bundle.
Protocol reversing was done with a combination of static analysis of the shared object with [Ghidra](https://ghidra-sre.org/) and dynamic analysis with [Frida](https://frida.re/docs/javascript-api/).
The headers reversed with Ghidra are at `types/all.h`. They are almost not used by this minimal implementation though.
The hooks used with frida are at `frida-hooks.js`, but it's mostly a playground - some useful functions got deleted once I understood the protocol.
There's also a partial Wireshark dissector at `dissector.lua`. You can install it with `make install-wireshark-dissector`.
### Take APK from emulator/sacrificial device
```
adb shell pm list packages | grep ysx
@@ -104,3 +224,8 @@ adb install-multiple *apk
[docs](https://frida.re/docs/android/)
### Start frida server
```
adb shell 'su -c nohup /data/local/tmp/frida-server-16.1.11-android-arm64 &'
```
+62
View File
@@ -0,0 +1,62 @@
<html>
<h2>${id}</h2><a href="/camera/${id}"><img src="/camera/${id}"/></a><hr/>
<script>
const alaw_to_s16_table = [
-5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736, -7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784, -2752,
-2624, -3008, -2880, -2240, -2112, -2496, -2368, -3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392, -22016,
-20992, -24064, -23040, -17920, -16896, -19968, -18944, -30208, -29184, -32256, -31232, -26112, -25088, -28160,
-27136, -11008, -10496, -12032, -11520, -8960, -8448, -9984, -9472, -15104, -14592, -16128, -15616, -13056, -12544,
-14080, -13568, -344, -328, -376, -360, -280, -264, -312, -296, -472, -456, -504, -488, -408, -392, -440, -424, -88,
-72, -120, -104, -24, -8, -56, -40, -216, -200, -248, -232, -152, -136, -184, -168, -1376, -1312, -1504, -1440, -1120,
-1056, -1248, -1184, -1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696, -688, -656, -752, -720, -560, -528, -624,
-592, -944, -912, -1008, -976, -816, -784, -880, -848, 5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736, 7552, 7296,
8064, 7808, 6528, 6272, 7040, 6784, 2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368, 3776, 3648, 4032, 3904, 3264,
3136, 3520, 3392, 22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944, 30208, 29184, 32256, 31232, 26112, 25088,
28160, 27136, 11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472, 15104, 14592, 16128, 15616, 13056, 12544, 14080,
13568, 344, 328, 376, 360, 280, 264, 312, 296, 472, 456, 504, 488, 408, 392, 440, 424, 88, 72, 120, 104, 24, 8, 56,
40, 216, 200, 248, 232, 152, 136, 184, 168, 1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184, 1888, 1824, 2016, 1952,
1632, 1568, 1760, 1696, 688, 656, 752, 720, 560, 528, 624, 592, 944, 912, 1008, 976, 816, 784, 880, 848,
];
const alaw_to_s16 = (a_val) => {
return alaw_to_s16_table[a_val];
};
const audio_context = new AudioContext();
const gain_node = audio_context.createGain(); // Declare gain node
const channels =1;
const sample_rate = 8000;
const audioBuffer = audio_context.createBuffer(channels, 960, sample_rate); // 960??
//const audioBuffer = audio_context.createBuffer(channels, decoded.length, sample_rate);
gain_node.connect(audio_context.destination); // Connect gain node to speakers
audio_context.resume();
const evtSource = new EventSource('/audio/${id}');
evtSource.onopen = (e) => {
console.log("evtsource open");
}
let endsAt = 0;
let startAt = 0;
evtSource.onmessage = (e) => {
const nowBuffering = audioBuffer.getChannelData(0);
const u8 = Uint8Array.from(atob(e.data), c => c.charCodeAt(0));
new Int16Array(u8).map(alaw_to_s16).forEach((el, i) => nowBuffering[i] = el / 0x8000 );
const source_node = audio_context.createBufferSource();
source_node.buffer = audioBuffer;
source_node.connect(gain_node);
const now = Date.now();
if(now > endsAt) { // lost packets
startAt = 0;
} else {
startAt += audioBuffer.duration;
}
source_node.start(startAt);
endsAt = now + audioBuffer.duration * 1000;
};
evtSource.onerror = (e) => {
console.log("evtsource error", e);
}
</script>
</html>
+49
View File
@@ -0,0 +1,49 @@
import { hideBin } from "yargs/helpers";
import yargs from "yargs/yargs";
import { serveHttp } from "../http_server.js";
import { opt } from "../options.js";
import { pair } from "../pair.js";
yargs(hideBin(process.argv))
.command(
"http_server",
"start http server",
(yargs) => {
return yargs
.option("debug", { default: false })
.option("ansi", { default: false })
.option("audio", { default: false })
.boolean(["debug", "ansi", "audio"])
.option("discovery_ip", { default: "192.168.1.255" })
.option("attempt_to_fix_packet_loss", { default: false })
.option("port", { describe: "HTTP Port to listen on", default: 5000 })
.number(["port"])
.strict();
},
(argv) => {
const opts: opt = argv as opt;
serveHttp(opts, argv.port, argv.audio || false);
},
)
.command(
"pair",
"configure a camera",
(yargs) => {
return yargs
.option("debug", { default: false })
.option("ansi", { default: false })
.boolean(["debug", "ansi"])
.option("discovery_ip", { default: "192.168.1.255" })
.option("attempt_to_fix_packet_loss", { default: false })
.demandOption("ssid")
.demandOption("password")
.string(["ssid", "password"]);
},
(argv) => {
const opts: opt = argv as unknown as opt;
pair({ opts, ssid: argv.ssid, password: argv.password });
},
)
.demandCommand()
.parseSync();
Binary file not shown.
+34 -2
View File
@@ -24,12 +24,44 @@ export const Commands = {
};
// Record<keyof typeof Commands,
export const CommandsByValue = Object.keys(Commands).reduce((acc, cur) => {
acc[Commands[cur]] = cur;
type t = Record<number, keyof typeof Commands>;
export const CommandsByValue: t = Object.keys(Commands).reduce((acc: t, cur) => {
let key: keyof typeof Commands = cur as keyof typeof Commands;
acc[Commands[key]] = key;
return acc;
}, {});
export const DrwStart = 0x0a11;
export const ControlCommands = {
// TODO: flip these..
ConnectUser: 0x2010,
ConnectUserAck: 0x2011,
// CloseSession: 0x3110,
// CloseSessionAck: 0x3111,
DevStatus: 0x0810, // CMD_SYSTEM_STATUS_GET
DevStatusAck: 0x0811,
WifiSettingsSet: 0x0160, // CMD_NET_WIFISETTING_SET
WifiSettings: 0x0260, // CMD_NET_WIFISETTING_GET
WifiSettingsAck: 0x0261,
ListWifi: 0x0360, // CMD_NET_WIFI_SCAN
ListWifiAck: 0x0361,
StartVideo: 0x1030, // CMD_PEER_LIVEVIDEO_START
StopVideo: 0x1130, // CMD_PEER_LIVEVIDEO_STOP
Shutdown: 0x1010, //CMD_SYSTEM_SHUTDOWN,
Reboot: 0x1110, //CMD_SYSTEM_REBOOT,
VideoParamSet: 0x1830, // CMD_PEER_VIDEOPARAM_SET
VideoParamGet: 0x1930, // CMD_PEER_VIDEOPARAM_GET
IRToggle: 0x0a30, // CMD_PEER_IRCUT_ONOFF
};
export const ccDest: Record<number, number> = {
[ControlCommands.ConnectUser]: 0xff00,
[ControlCommands.DevStatus]: 0x0000,
[ControlCommands.StartVideo]: 0x0000,
[ControlCommands.ListWifi]: 0x0000,
[ControlCommands.WifiSettings]: 0x0000,
[ControlCommands.ListWifiAck]: 0xaa55,
[ControlCommands.ConnectUserAck]: 0xaa55,
[ControlCommands.DevStatusAck]: 0xaa55,
};
+46
View File
@@ -0,0 +1,46 @@
import EventEmitter from "node:events";
import { create_LanSearch, parse_PunchPkt } from "./impl.js";
import { createSocket, RemoteInfo } from "node:dgram";
import { Commands } from "./datatypes.js";
const handleIncomingPunch = (msg: Buffer, ee: EventEmitter, rinfo: RemoteInfo, debug: boolean) => {
const ab = new Uint8Array(msg).buffer;
const dv = new DataView(ab);
const cmd_id = dv.readU16();
if (cmd_id != Commands.PunchPkt) {
return;
}
if (debug) {
console.log("Discovery got a PunchPkt");
}
ee.emit("discover", rinfo, parse_PunchPkt(dv));
};
export const discoverDevices = (debug: boolean, discovery_ip: string): EventEmitter => {
const sock = createSocket("udp4");
const SEND_PORT = 32108;
const ee = new EventEmitter();
sock.on("error", (err) => {
console.error(`sock error:\n${err.stack}`);
sock.close();
});
sock.on("message", (msg, rinfo) => handleIncomingPunch(msg, ee, rinfo, debug));
sock.on("listening", () => {
sock.setBroadcast(true);
console.log(`Searching for devices on ${discovery_ip}`);
let buf = create_LanSearch();
setInterval(() => {
console.log(".");
sock.send(new Uint8Array(buf.buffer), SEND_PORT, discovery_ip);
}, 2000);
sock.send(new Uint8Array(buf.buffer), SEND_PORT, discovery_ip);
});
sock.bind();
return ee;
};
+84 -41
View File
@@ -1,24 +1,29 @@
-- Create a new protocol for your custom packets
my_protocol = Proto("myprotocol", "My Custom Protocol")
ilnk_proto = Proto("iLnkP2P", "iLnk")
-- Define the fields you want to display in Wireshark
my_protocol.fields = {}
my_protocol.fields.type = ProtoField.string("myprotocol.type", "Type")
my_protocol.fields.payload = ProtoField.bytes("myprotocol.payload", "Data")
my_protocol.fields.len = ProtoField.uint16("myprotocol.len", "Len", base.HEX)
ilnk_proto.fields = {}
ilnk_proto.fields.type = ProtoField.string("iLnkP2P.type", "Type")
-- ilnk_proto.fields.payload = ProtoField.bytes("iLnkP2P.payload", "Payload")
ilnk_proto.fields.len = ProtoField.uint16("iLnkP2P.len", "Packet length", base.HEX)
my_protocol.fields.m_type = ProtoField.uint8("myprotocol.m_type", "Stream Type", base.HEX)
my_protocol.fields.m_stream_id = ProtoField.uint8("myprotocol.m_stream_id", "Stream ID", base.HEX)
my_protocol.fields.elem_count = ProtoField.uint16("myprotocol.elem_count", "Elem count", base.DEC)
ilnk_proto.fields.m_type = ProtoField.uint8("iLnkP2P.m_type", "Stream type", base.HEX)
ilnk_proto.fields.m_stream_id = ProtoField.uint8("iLnkP2P.m_stream_id", "Stream ID", base.HEX)
ilnk_proto.fields.pkt_seq = ProtoField.uint16("iLnkP2P.pkt_seq", "Packet ID", base.HEX)
ilnk_proto.fields.elem_count = ProtoField.uint16("iLnkP2P.elem_count", "Elem count", base.DEC)
my_protocol.fields.cmd_payload_len = ProtoField.uint16("myprotocol.cmd_payload_len", "CMD Payload Len", base.HEX)
my_protocol.fields.cmd = ProtoField.uint16("myprotocol.cmd", "CMD", base.HEX)
my_protocol.fields.start = ProtoField.uint16("myprotocol.start", "Start", base.HEX)
my_protocol.fields.cmd_dest = ProtoField.uint16("myprotocol.cmd_dest", "Dest", base.HEX)
my_protocol.fields.cmd_payload = ProtoField.bytes("myprotocol.payload", "Payload", base.DASH)
ilnk_proto.fields.cmd_payload_len = ProtoField.uint16("iLnkP2P.cmd_payload_len", "CMD Payload Len", base.HEX)
ilnk_proto.fields.cmd = ProtoField.uint16("iLnkP2P.cmd", "CMD", base.HEX)
ilnk_proto.fields.start = ProtoField.uint16("iLnkP2P.start", "Start", base.HEX)
ilnk_proto.fields.cmd_dest = ProtoField.uint16("iLnkP2P.cmd_dest", "Dest", base.HEX)
ilnk_proto.fields.auth_token = ProtoField.bytes("iLnkP2P.auth_token", "CMD auth token", base.DASH)
ilnk_proto.fields.cmd_payload = ProtoField.bytes("iLnkP2P.payload", "CMD Payload", base.DASH)
-- jpeg | audio | continuation type?
ilnk_proto.fields.data_payload = ProtoField.bytes("iLnkP2P.data_payload", "Data Payload", base.DASH)
my_protocol.fields.encrypted = ProtoField.bool("myprotocol.encrypted", "Encrypted")
my_protocol.fields.cmd_type = ProtoField.string("myprotocol.payload", "Cmd Pkt Type")
ilnk_proto.fields.encrypted = ProtoField.bool("iLnkP2P.encrypted", "Encrypted")
ilnk_proto.fields.cmd_type = ProtoField.string("iLnkP2P.cmd_type", "Cmd Pkt Type")
ilnk_proto.fields.decrypted_data = ProtoField.bytes("iLnkP2P.decrypted_data", "Decrypted data")
lut = {
[0xf1f0] = "Close",
@@ -48,45 +53,83 @@ lut = {
setmetatable(lut, lut)
-- Define a function to dissect the packets
function my_protocol.dissector(buffer, pinfo, tree)
function ilnk_proto.dissector(buffer, pinfo, tree)
local packet_length = buffer:len()
local subtree = tree:add(my_protocol, buffer(), "My Custom Protocol Data")
local subtree = tree:add(ilnk_proto, buffer(), "iLnkP2P")
-- Add the entire packet as a field
local packetname = lut[buffer(0, 2):uint()]
subtree:add(my_protocol.fields.type, packetname)
subtree:add(ilnk_proto.fields.type, packetname)
-- Set the protocol description in the packet list
pinfo.cols.protocol:set("myprotocol")
pinfo.cols.protocol:set("iLnkP2P")
if packetname == "DrwAck" then
subtree:add(my_protocol.fields.len, buffer(2, 2))
subtree:add(my_protocol.fields.m_type, buffer(4, 1))
subtree:add(my_protocol.fields.m_stream_id, buffer(5, 1))
subtree:add(my_protocol.fields.elem_count, buffer(6, 2))
subtree:add(ilnk_proto.fields.len, buffer(2, 2))
subtree:add(ilnk_proto.fields.m_type, buffer(4, 1))
subtree:add(ilnk_proto.fields.m_stream_id, buffer(5, 1))
subtree:add(ilnk_proto.fields.elem_count, buffer(6, 2))
end
if packetname == "Drw" then
subtree:add(my_protocol.fields.len, buffer(2, 2))
subtree:add(my_protocol.fields.m_type, buffer(4, 1))
subtree:add(my_protocol.fields.m_stream_id, buffer(5, 1))
subtree:add_le(my_protocol.fields.start, buffer(8, 2))
subtree:add_le(my_protocol.fields.cmd, buffer(0xa, 2))
local b_pkt_len = buffer(2, 2)
local pkt_len = b_pkt_len:uint()
local is_data_packet = buffer(5, 1):uint() == 1
subtree:add(ilnk_proto.fields.len, b_pkt_len)
subtree:add(ilnk_proto.fields.m_type, buffer(4, 1))
subtree:add(ilnk_proto.fields.m_stream_id, buffer(5, 1))
subtree:add(ilnk_proto.fields.pkt_seq, buffer(6, 2))
local b_payload_len = buffer(0xc, 2)
local payload_len = buffer(0xc, 2):le_uint()
subtree:add_le(my_protocol.fields.cmd_payload_len, buffer(0xc, 2))
subtree:add(my_protocol.fields.encrypted, payload_len >= 5):set_generated()
if buffer(0xb, 1):uint() % 2 == 1 then
cmdtype = "ack"
if not is_data_packet then
subtree:add_le(ilnk_proto.fields.start, buffer(8, 2))
subtree:add_le(ilnk_proto.fields.cmd, buffer(0xa, 2))
subtree:add_le(ilnk_proto.fields.cmd_payload_len, b_payload_len)
subtree:add_le(ilnk_proto.fields.cmd_dest, buffer(0xe, 2))
-- inline value for short-payload bytes
subtree:add(ilnk_proto.fields.auth_token, buffer(0x10, 4))
if buffer(0xb, 1):uint() % 2 == 1 then
cmdtype = "ack"
else
cmdtype = "cmd"
end
subtree:add(ilnk_proto.fields.cmd_type, cmdtype):set_generated()
subtree:add(ilnk_proto.fields.encrypted, payload_len >= 5):set_generated()
if payload_len >= 5 then
local payload = buffer(0x14, payload_len - 4)
local dec_payload = ByteArray.new()
dec_payload:set_size(payload_len - 4)
for i=4,payload_len-5 do -- inclusive upper range
local v = buffer(0x14 + i-4, 1):uint()
if (v % 2) == 0 then
v = v + 1
else
v = v - 1
end
dec_payload:set_index(i, v)
end
for i=0,3 do
local v = buffer(pkt_len+i, 1):uint()
if v % 2 == 0 then
v = v + 1
else
v = v - 1
end
dec_payload:set_index(i, v)
end
local dec_tvb = ByteArray.tvb(dec_payload, "Decrypted payload")
subtree:add(ilnk_proto.fields.decrypted_data, dec_tvb:range(0, payload_len -4) ):set_generated()
local payload_tvb = ByteArray.tvb(buffer(0x14, payload_len -4):bytes(), "CMD Payload")
subtree:add(ilnk_proto.fields.cmd_payload, payload_tvb:range(0, payload_len -4))
end
else
cmdtype = "cmd"
local payload_tvb = ByteArray.tvb(buffer(8, packet_length-8):bytes(), "Data Payload")
subtree:add(ilnk_proto.fields.data_payload, payload_tvb:range(0, packet_length-8))
end
subtree:add(my_protocol.fields.cmd_type, cmdtype):set_generated()
subtree:add_le(my_protocol.fields.cmd_dest, buffer(0xe, 2))
subtree:add(my_protocol.fields.cmd_payload, buffer(0x10, payload_len))
-- subtree:add(my_protocol.fields.cmd, buffer(0xc, 2))
end
-- AvcLIB = src/IpcSession.cpp, line 1113, CmdSndProc:BATC609531EXLVS[0:0:10] now CmdSend[start=a11,cmd=1032,len=4,dest=0]=12
-- UDP PKT SEND Drw (0xf1d0)
@@ -94,7 +137,7 @@ function my_protocol.dissector(buffer, pinfo, tree)
-- 00000000 f1 d0 00 10 d1 00 00 04 11 0a 32 10 04 00 00 00 ..........2.....
-- 00000010 50 70 77 35 Ppw5
subtree:add(my_protocol.fields.payload, buffer(2, packet_length-2))
-- subtree:add(ilnk_proto.fields.payload, buffer(2, packet_length-2))
end
udp_table = DissectorTable.get("udp.port"):add(49512, my_protocol)
udp_table = DissectorTable.get("udp.port"):add(49512, ilnk_proto)
+169 -9
View File
@@ -1,9 +1,10 @@
import { replaceFunctions, Commands, CommandsByValue } from "./func_replacements.js";
import { Commands, CommandsByValue } from "./datatypes.js";
import { replaceFunctions, XqBytesDec } from "./func_replacements.js";
import { placeholderTypes, sprintf, u16_swap } from "./utils.js";
const hook_fn = (name_in_elf, enter, leave) => {
var symbol_addr = DebugSymbol.fromName(name_in_elf).address;
console.log(`${name_in_elf} addr is: ${symbol_addr}, this is ${this}`);
console.log(`${name_in_elf} addr is: ${symbol_addr}`);
Interceptor.attach(symbol_addr, {
onEnter: enter,
onLeave: leave,
@@ -11,6 +12,18 @@ const hook_fn = (name_in_elf, enter, leave) => {
console.log(`Hooked ${name_in_elf}`);
};
const global = (name_in_elf) => {
//var symbol_addr = DebugSymbol.fromName(name_in_elf).address;
// Module.enumerateSections("libvdp.so").forEach((s) => console.log(JSON.stringify(s, null, 2)));
// "name": ".bss",
//Module.enumerateSymbols("libvdp.so").forEach((s) => console.log(JSON.stringify(s, null, 2)));
const syms = Module.enumerateSymbols("libvdp.so").filter((s) => s.name == name_in_elf);
var symbol_addr = syms[0].address;
console.log(`global ${name_in_elf} addr is: ${symbol_addr}`);
if (symbol_addr == 0x0 || symbol_addr == null) throw new Error(`can't read ${name_in_elf}`);
return new NativePointer(symbol_addr);
};
function hook_export_fn(name_in_elf, enter, leave) {
var symbol_addr = Module.findExportByName("libvdp.so", name_in_elf);
console.log(`${name_in_elf} addr is: ${symbol_addr}`);
@@ -40,6 +53,54 @@ const hook_p2p_read = () => {
);
};
const hook_Log = () => {
Java.perform(function () {
var Log = Java.use("android.util.Log");
Log.d.overload("java.lang.String", "java.lang.String", "java.lang.Throwable").implementation = function (a, b, c) {
console.log("The application reports Log.d(" + a.toString() + ", " + b.toString() + ")");
return this.d(a, b, c);
};
Log.v.overload("java.lang.String", "java.lang.String", "java.lang.Throwable").implementation = function (a, b, c) {
console.log("The application reports Log.v(" + a.toString() + ", " + b.toString() + ")");
return this.v(a, b, c);
};
Log.i.overload("java.lang.String", "java.lang.String", "java.lang.Throwable").implementation = function (a, b, c) {
console.log("The application reports Log.i(" + a.toString() + ", " + b.toString() + ")");
return this.i(a, b, c);
};
Log.e.overload("java.lang.String", "java.lang.String", "java.lang.Throwable").implementation = function (a, b, c) {
console.log("The application reports Log.e(" + a.toString() + ", " + b.toString() + ")");
return this.e(a, b, c);
};
Log.w.overload("java.lang.String", "java.lang.String", "java.lang.Throwable").implementation = function (a, b, c) {
console.log("The application reports Log.w(" + a.toString() + ", " + b.toString() + ")");
return this.w(a, b, c);
};
Log.d.overload("java.lang.String", "java.lang.String").implementation = function (a, b) {
console.log("The application reports Log.d(" + a.toString() + ", " + b.toString() + ")");
return this.d(a, b);
};
Log.v.overload("java.lang.String", "java.lang.String").implementation = function (a, b) {
console.log("The application reports Log.v(" + a.toString() + ", " + b.toString() + ")");
return this.v(a, b);
};
Log.i.overload("java.lang.String", "java.lang.String").implementation = function (a, b) {
console.log("The application reports Log.i(" + a.toString() + ", " + b.toString() + ")");
return this.i(a, b);
};
Log.e.overload("java.lang.String", "java.lang.String").implementation = function (a, b) {
console.log("The application reports Log.e(" + a.toString() + ", " + b.toString() + ")");
return this.e(a, b);
};
Log.w.overload("java.lang.String", "java.lang.String").implementation = function (a, b) {
console.log("The application reports Log.w(" + a.toString() + ", " + b.toString() + ")");
return this.w(a, b);
};
});
};
const hook___android_log_print = () => {
const sym = "__android_log_print";
hook_fn(
@@ -63,21 +124,100 @@ const hook___android_log_print = () => {
const values = types.map((t, idx) => o[t](args[idx + 3]));
const newStr = sprintf(fmt, values);
console.log(_tag, newStr);
console.log(_tag, newStr.trim());
},
() => {},
);
};
const hook_udpsend = () => {
const codeTable = global("codeTable");
/*
* WanAddrGet
139.155.68.77
P2PLIB = p2pCommon/XQPPP_Socket.c, line 846, XQ_WanAddrGet:ipv4 cAddr=139.155.68.77
WanAddrGet
119.45.114.92
P2PLIB = p2pCommon/XQPPP_Socket.c, line 846, XQ_WanAddrGet:ipv4 cAddr=119.45.114.92
WanAddrGet
162.62.63.154
P2PLIB = p2pCommon/XQPPP_Socket.c, line 846, XQ_WanAddrGet:ipv4 cAddr=162.62.63.154
WanAddrGet
3.132.215.40
*/
let x = {};
hook_fn(
"XQ_WanAddrGet",
(args) => {
console.log("WanAddrGet");
console.log(args[0].readCString());
x.ret = args[2];
},
(retval) => {
console.log("WanAddrGet RET");
console.log(x.ret.readCString());
//console.log("on wanaddrget, ret");
//console.log(hexdump(codeTable.readByteArray(0x548)));
},
);
let d = {};
hook_fn(
"XqStrDec",
(args) => {
console.log("XqStrDec param1", args[0].readCString());
console.log("codetable", codeTable.readCString());
console.log("codetable hexarr\n", hexdump(codeTable.readByteArray(0x548)));
},
(retval) => {
console.log("XqStrDec RET");
console.log(retval.readCString());
//console.log("on wanaddrget, ret");
//console.log(hexdump(codeTable.readByteArray(0x548)));
},
);
hook_fn(
"XqCodeTableInit",
(args) => {
console.log("on codetableinit, it was");
console.log(hexdump(codeTable.readByteArray(0x548)));
},
(retval) => {
console.log("on codetableinit, ret");
console.log(hexdump(codeTable.readByteArray(0x548)));
},
);
hook_fn(
"XQ_InitEncryption",
(args) => {
console.log("on initenc, it was");
console.log(hexdump(codeTable.readByteArray(0x548)));
},
(retval) => {
console.log("on initenc, ret");
console.log(hexdump(codeTable.readByteArray(0x548)));
},
);
hook_fn(
"XQ_UdpPktSend",
(args) => {
const data = args[0].readByteArray(args[1].toInt32());
const cmd = u16_swap(args[0].readU16());
const name = CommandsByValue[cmd];
console.log(`UDP PKT SEND ${name} (0x${cmd.toString(16)})`);
console.log(data);
if (name != "P2PAliveAck") {
if (name != "LanSearch" && name != "LanSearchExt") {
let cmd = "";
if (name == "Drw") {
cmd = args[0].add(0xa).readU16().toString(16);
}
let tstamp = Date.now();
console.log(`${tstamp} UDP PKT SEND ${name} (0x${cmd.toString(16)}) - CMD? ${cmd}`);
console.log(data);
}
} else {
console.log("> P2PAliveAck");
}
},
(retval) => {},
);
@@ -92,14 +232,32 @@ const hook_udpsend = () => {
(retval) => {
const data = o.buf.readByteArray(retval.toInt32());
const cmd = u16_swap(o.buf.readU16());
const len = u16_swap(o.buf.add(2).readU16());
const name = CommandsByValue[cmd];
console.log(`UDP PKT RECV, cmd=${name}, 0x${cmd.toString(16)}, ret=${retval}`);
console.log(data);
if (cmd == Commands.Drw) {
const isData = o.buf.add(5).readU8();
if (name != "P2PAlive") {
let tstamp = Date.now();
console.log(`${tstamp} UDP PKT RECV, len=${len}, cmd=${name}, 0x${cmd.toString(16)}, ret=${retval}`);
if (cmd != Commands.Drw || (cmd == Commands.Drw && !isData)) {
// dont log data payloads
console.log(data);
}
} else {
console.log("< P2PAlive");
}
if (cmd == Commands.Drw && !isData) {
if (len > 0x18) {
// pos(0xa11) == 8 + 0xc == 0x14 == 20
const under = data.unwrap().add(0x14); //, len - 0x20;
XqBytesDec(under, len - 0x10, 4);
console.log("decrypted data");
console.log(data);
}
}
},
);
/*
let s = {};
hook_fn(
"PktSeq_seqGet",
@@ -112,6 +270,7 @@ const hook_udpsend = () => {
console.log(data);
},
);
*/
/*
hook_fn(
@@ -213,10 +372,11 @@ function doHooks() {
var libnative_addr = Module.findBaseAddress("libvdp.so");
if (libnative_addr) {
hook___android_log_print();
hook_Log();
// hook_in_out_buf("create_LstReq", 0x1c, 0x1c);
//hook_in_out_buf("create_P2pRdy", 0x1c, 0x1c);
hook_udpsend();
doReplaceFunctions();
//doReplaceFunctions();
// hook_p2p_read();
// hook_pack_P2pId();
+44 -32
View File
@@ -1,5 +1,5 @@
import { swap_endianness_u32, swap_endianness_u16, u16_swap } from "./utils.js";
import { Commands, CommandsByValue } from "./datatypes.js";
import { swap_endianness_u16, swap_endianness_u32, u16_swap } from "./utils.js";
const writeCommand2 = (command, buf) => {
buf.writeByteArray([(command & 0xff00) >> 8, command & 0xff]);
@@ -12,6 +12,7 @@ export const XqBytesDec = (inoutbuf, buflen, rotate) => {
// only rotation is different
let new_buf = new Uint8Array(buflen);
new_buf.fill(0x1);
for (let i = 0; i < buflen; i++) {
let b = inoutbuf.add(i).readU8();
if ((b & 1) != 0) {
@@ -206,16 +207,26 @@ const dbg_create_Drw = (og_func) => {
};
const dbg__ZN12CPPPPChannel10CmdSndPushEiiPci = (og_func) => {
const CmdSndPush = (_this, dest, cmdtype, idk, cmdlen) => {
console.log("CmdSndPush", _this, dest.toString(16), cmdtype.toString(16), idk, cmdlen);
//CmdSndPush 0x1020 ret: 172
//CmdSndPush 0x1040 ret: 92
//CmdSndPush 0x50ff ret: 564
//CmdSndPush 0x1008 ret: 12
console.log("CmdSndPushPre", _this, dest.toString(16), cmdtype.toString(16), idk, cmdlen);
// CmdSndPush 0x1020 ret: 172
// CmdSndPush 0x1040 ret: 92
// CmdSndPush 0x50ff ret: 564
// CmdSndPush 0x1008 ret: 12
//
//if (cmdtype == 0x1020) return 172; // mask
if (cmdtype == 0x1040) return 92; // mask
if (cmdtype == 0x50ff) return 564; // mask
if (cmdtype == 0x1008) return 12; // mask
//if (cmdtype == 0x1020) return 172; // login
// maybe these brick it
//if (cmdtype == 0x1040) return 92; // mask
//if (cmdtype == 0x50ff) return 564; // mask
//if (cmdtype == 0x1008) return 12; // battery + status online?
//// new
////if (cmdtype == 0x6003) return 12; // wifilist
//if (cmdtype == 0x6002) return 12; // wifisettings
//if (cmdtype == 0x1031) return 44; // "open settings panel" ??
//if (cmdtype == 0x1032) return 12; //idk
//// mb reboot
//if (cmdtype == 0x2005) return 20; //idk
let ret = og_func(_this, dest, cmdtype, idk, cmdlen);
console.log(`CmdSndPush 0x${cmdtype.toString(16)} ret: ${ret}`);
@@ -224,38 +235,36 @@ const dbg__ZN12CPPPPChannel10CmdSndPushEiiPci = (og_func) => {
return CmdSndPush;
};
const dbg__Z6NetCmdP7_JNIEnvPciiP8_jobject = (og_func) => {
// "pointer", "pointer", "uint32", "uint32", "pointer"
const NetCmd = (java_class, p2pid, sit, cmd, java_param) => {
console.log("NetCmd", java_class, p2pid.readCString(), sit, cmd.toString(16), java_param);
// if (cmd == 0x0000) return 0;
let ret = og_func(java_class, p2pid, sit, cmd, java_param);
console.log(`NetCmd 0x${cmd.toString(16)} ret: ${ret}`);
return ret;
};
return NetCmd;
};
const dbg__Z9SystemCmdP7_JNIEnvPciiP8_jobject = (og_func) => {
// "pointer", "pointer", "uint32", "uint32", "pointer"
const AvCmd = (java_class, p2pid, sit, cmd, java_param) => {
const SystemCmd = (java_class, p2pid, sit, cmd, java_param) => {
console.log("SystemCmd", java_class, p2pid.readCString(), sit, cmd.toString(16), java_param);
if (cmd == 0x3018) return 20; // mask
if (cmd == 0x3019) return 12; // mask
if (cmd == 0x3005) return 12; // mask
if (cmd == 0x3026) return 0; // mask
if (cmd == 0x3001) return 12;
if (cmd == 0x3003) return 12;
//if (cmd == 0x3011) return 272; // this is STOP !!
//if (cmd == 0x3010) return 272; // borks
let ret = og_func(java_class, p2pid, sit, cmd, java_param);
console.log(`SystemCmd 0x${cmd.toString(16)} ret: ${ret}`);
return ret;
};
return AvCmd;
return SystemCmd;
};
const dbg__Z5AvCmdP7_JNIEnvPciiP8_jobject = (og_func) => {
// "pointer", "pointer", "uint32", "uint32", "pointer"
const AvCmd = (java_class, p2pid, sit, cmd, java_param) => {
console.log("AvCmd", java_class, p2pid.readCString(), sit, cmd.toString(16), java_param);
if (cmd == 0x3018) return 20; // mask
if (cmd == 0x3019) return 12; // mask
if (cmd == 0x3005) return 12; // mask
if (cmd == 0x3026) return 0; // mask
if (cmd == 0x3001) return 12;
if (cmd == 0x3003) return 12;
//if (cmd == 0x3011) return 272; // this is STOP !!
//if (cmd == 0x3010) return 272; // borks
// if (cmd == 0x3011) return 272; // this is STOP !!
// if (cmd == 0x3010) return 272; // borks
let ret = og_func(java_class, p2pid, sit, cmd, java_param);
console.log(`AvCmd 0x${cmd.toString(16)} ret: ${ret}`);
return ret;
@@ -284,10 +293,10 @@ const dbg_pack_ClntPkt = (og_func) => {
},
};
/*
P2PAlive: 0xf1e0,
P2PAliveAck: 0xf1e1,
P2pRdy: 0xf142, // idk??
*/
P2PAlive: 0xf1e0,
P2PAliveAck: 0xf1e1,
P2pRdy: 0xf142, // idk??
*/
const fn = packFn[cmd];
if (fn == undefined) {
@@ -378,6 +387,7 @@ const replace_func = (stub, ret, args) => {
// CSession_CtrlPkt_Proc(struct, *cmd) == control?
export const replaceFunctions = () => {
const replacements = [
/*
[create_P2pAlive, "uint8", ["pointer"]],
[create_P2pAliveAck, "uint8", ["pointer"]],
[create_LanSearch, "uint8", ["pointer"]],
@@ -397,6 +407,8 @@ export const replaceFunctions = () => {
[dbg_pack_ClntPkt, "uint32", ["uint32", "pointer", "pointer"]],
[dbg__Z5AvCmdP7_JNIEnvPciiP8_jobject, "uint32", ["pointer", "pointer", "uint32", "uint32", "pointer"]],
[dbg__Z9SystemCmdP7_JNIEnvPciiP8_jobject, "uint32", ["pointer", "pointer", "uint32", "uint32", "pointer"]],
*/
[dbg__Z6NetCmdP7_JNIEnvPciiP8_jobject, "uint32", ["pointer", "pointer", "uint32", "uint32", "pointer"]],
[dbg__ZN12CPPPPChannel10CmdSndPushEiiPci, "uint64", ["pointer", "uint32", "uint32", "pointer", "uint32"]],
];
+158 -36
View File
@@ -1,10 +1,9 @@
import { Commands, CommandsByValue, ControlCommands } from "./datatypes.js";
import { create_P2pRdy, SendStartVideo, SendUsrChk } from "./impl.js";
import { Session } from "./server.js";
import { u16_swap } from "./utils.js";
import { XqBytesDec } from "./func_replacements.js";
import { hexdump } from "./hexdump.js";
let curImage = null;
import { create_P2pRdy, SendListWifi, SendUsrChk, DevSerial } from "./impl.js";
import { Session } from "./session.js";
import { u16_swap, u32_swap } from "./utils.js";
export const notImpl = (_: Session, dv: DataView) => {
const raw = dv.readU16();
@@ -24,35 +23,103 @@ export const handle_P2PAlive = (session: Session, _: DataView) => {
const b = create_P2pAliveAck();
session.send(b);
};
export const handle_PunchPkt = (session: Session, dv: DataView) => {
const punchCmd = dv.readU16();
const len = dv.add(2).readU16();
const prefix = dv.add(4).readString(4);
const serial = dv.add(8).readU64().toString();
const suffix = dv.add(16).readString(4);
// f141 20 BATC 609531 EXLV
session.eventEmitter.emit("connect", prefix.toString() + serial + suffix.toString());
session.send(create_P2pRdy(dv.add(4).readByteArray(len)));
export const handle_P2PRdy = (session: Session, _: DataView) => {
const b = SendUsrChk(session, "admin", "admin");
session.send(b);
};
export const createResponseForControlCommand = (session: Session, dv: DataView): DataView | null => {
export const makePunchPkt = (dev: DevSerial): DataView => {
const len = dev.prefix.length + dev.suffix.length + 8;
const outbuf = new DataView(new Uint8Array(0x14).buffer); // 8 = serial u64
outbuf.add(0).writeString(dev.prefix);
outbuf.add(4).writeU64(dev.serialU64);
outbuf.add(8 + dev.prefix.length).writeString(dev.suffix);
return create_P2pRdy(outbuf);
};
export const createResponseForControlCommand = (session: Session, dv: DataView): DataView[] => {
const start_type = dv.add(8).readU16(); // 0xa11 on control; data starts here on DATA pkt
const cmd_id = dv.add(10).readU16(); // 0x1120
const payload_len = u16_swap(dv.add(0xc).readU16());
if (start_type != 0x110a) {
console.error(`Expected start_type to be 0xa11, got 0x${start_type.toString(16)}`);
return;
return [];
}
const rotate_chr = 4;
if (payload_len > rotate_chr) {
// 20 = 16 (header) + 4 (??)
XqBytesDec(dv.add(20), payload_len - 4, rotate_chr);
console.log("Decrypted");
console.log(hexdump(dv));
}
if (cmd_id == ControlCommands.ConnectUserAck) {
let c = new Uint8Array(dv.add(0x14).readByteArray(4).buffer);
session.ticket[0] = c[0] % 2 == 0 ? c[0] + 1 : c[0] - 1;
session.ticket[1] = c[1] % 2 == 0 ? c[1] + 1 : c[1] - 1;
session.ticket[2] = c[2] % 2 == 0 ? c[2] + 1 : c[2] - 1;
session.ticket[3] = c[3] % 2 == 0 ? c[3] + 1 : c[3] - 1;
const buf = SendStartVideo(session);
return buf;
let c = new Uint8Array(dv.add(0x18).readByteArray(4).buffer);
session.ticket = [...c];
session.eventEmitter.emit("login");
return [];
}
if (cmd_id == ControlCommands.DevStatusAck) {
// ParseDevStatus -> offset relevant?
let charging = u32_swap(dv.add(0x28).readU32()) & 1; // 0x14000101 v 0x14000100
let power = u16_swap(dv.add(0x18).readU16()); // '3730' or '3765', milliVolts?
let dbm = dv.add(0x24).readU8() - 0x100; // 0xbf - 0x100 = -65dbm .. constant??
// > -50 = excellent, -50 to -60 good, -60 to -70 fair, <-70 weak
console.log(`charging? ${charging}, batlevel? ${power}, wifi dbm: ${dbm}`);
}
if (cmd_id == ControlCommands.WifiSettingsAck) {
const wifiSettings = {
enable: dv.add(0x14).readU32(),
status: dv.add(0x18).readU32(),
mode: dv.add(0x1c).readU32LE(),
channel: dv.add(0x20).readU32(),
authtype: dv.add(0x24).readU32(),
dhcp: dv.add(0x28).readU32(),
ssid: dv.add(0x2c).readString(0x20),
psk: dv.add(0x4c).readString(0x80),
ip: dv.add(0xcc).readString(0x10),
mask: dv.add(0xdc).readString(0x10),
gw: dv.add(0xec).readString(0x10),
dns1: dv.add(0xfc).readString(0x10),
dns2: dv.add(0x10c).readString(0x10),
};
const buf = SendListWifi(session);
console.log(`Current wifi settings: ${JSON.stringify(wifiSettings, null, 2)}`);
return [buf];
}
if (cmd_id == ControlCommands.ListWifiAck) {
let startat = 0x10;
let msg_len = 91;
console.log("payload len", payload_len);
let msg_count = (payload_len - 9) / msg_len;
let remote_msg_count = dv.add(startat).readU32LE();
console.log("should get messages:", msg_count, "in payload: ", remote_msg_count);
startat += 4;
let items = [];
for (let i = 0; i < msg_count; i++) {
const wifiListItem = {
// startat = msg_len * i + 0x14;
ssid: dv.add(startat).readString(0x40),
mac: dv.add(startat + 0x40).readByteArray(8),
security: dv.add(startat + 0x48).readU32LE(),
dbm0: dv.add(startat + 0x4c).readU32LE(),
dbm1: dv.add(startat + 0x50).readU32LE(),
mode: dv.add(startat + 0x54).readU32LE(),
channel: dv.add(startat + 0x58).readU32LE(),
};
console.log(`Wifi Item: ${JSON.stringify(wifiListItem, null, 2)}`);
startat += msg_len;
console.log("ended at", startat);
items.push(wifiListItem);
}
}
return [];
};
const deal_with_data = (session: Session, dv: DataView) => {
@@ -63,6 +130,7 @@ const deal_with_data = (session: Session, dv: DataView) => {
const m_hdr = dv.add(8).readByteArray(4);
let is_new_image = true;
let audio = true;
const pkt_id = dv.add(6).readU16();
for (let i = 0; i < 4; i++) {
is_new_image = is_new_image && m_hdr.add(i).readU8() == JPEG_HEADER[i];
audio = audio && m_hdr.add(i).readU8() == AUDIO_HEADER[i];
@@ -72,24 +140,85 @@ const deal_with_data = (session: Session, dv: DataView) => {
// "stream_head_t->type == 0x06" per pdf
if (dv.add(12).readU8() == 0x06) {
const audio_len = u16_swap(dv.add(8 + 16).readU16());
// may have received the next 'data' packet id as an audio frame -- jpeg was fine
if (pkt_id == session.rcvSeqId + 1) {
session.rcvSeqId = session.rcvSeqId + 1;
}
const audio_buf = dv.add(32 + 8).readByteArray(audio_len).buffer; // 8 for pkt header, 32 for `stream_head_t`
session.eventEmitter.emit("audio", Buffer.from(audio_buf));
session.eventEmitter.emit("audio", { gap: false, data: Buffer.from(audio_buf) });
} else {
// not sure what these are for, there's one per frame. maybe alignment?
}
} else {
const data = dv.add(8).readByteArray(pkt_len - 4);
if (is_new_image) {
if (curImage != null) {
session.eventEmitter.emit("frame", curImage);
if (session.curImage.length > 0 && !session.frame_is_bad) {
session.eventEmitter.emit("frame");
}
curImage = Buffer.from(data.buffer);
session.frame_was_fixed = false;
session.frame_is_bad = false;
session.curImage = [Buffer.from(data.buffer)];
session.rcvSeqId = pkt_id;
} else {
curImage = Buffer.concat([curImage, Buffer.from(data.buffer)]);
if (pkt_id <= session.rcvSeqId) {
// retransmit
return;
}
let b = Buffer.from(data.buffer);
if (pkt_id > session.rcvSeqId + 1) {
session.frame_is_bad = true;
// this should always be enabled but currently it seems to cause more visual distortion
// than just missing some frames
if (!session.options.attempt_to_fix_packet_loss) {
return;
}
if (session.curImage.length == 1) return; // header does not have markers
let lastFrameSlice = session.curImage[session.curImage.length - 1];
const lastResetMarker = findAllResetMarkers(lastFrameSlice).pop();
if (lastResetMarker == undefined) {
// not storing rcvSeqId as this frame did not put us back in track
return;
}
const firstResetMarker = findAllResetMarkers(b).shift();
if (firstResetMarker == undefined) {
// not storing rcvSeqId as this frame did not put us back in track
return;
}
session.curImage[session.curImage.length - 1] = Buffer.from(lastFrameSlice.subarray(0, lastResetMarker));
b = Buffer.from(b.subarray(firstResetMarker));
session.frame_is_bad = false;
session.frame_was_fixed = true;
}
session.rcvSeqId = pkt_id;
if (session.curImage != null) {
session.curImage.push(b);
}
}
}
};
const findAllResetMarkers = (b: Buffer): number[] => {
// a reset marker is a byte 0xff followed by a byte 0xd0-0xd7
let ret = [];
for (let i = 0; i < b.length - 1; i++) {
if (b[i] == 0xff) {
const nb = b[i + 1];
if (nb >= 0xd0 && nb <= 0xd7) {
ret.push(i);
}
}
}
return ret;
};
const makeDrwAck = (dv: DataView): DataView => {
const pkt_id = dv.add(6).readU16();
const m_stream = dv.add(5).readU8(); // data = 1, control = 0
@@ -115,13 +244,6 @@ export const handle_Drw = (session: Session, dv: DataView) => {
deal_with_data(session, dv);
} else {
const b = createResponseForControlCommand(session, dv);
if (b != null) {
session.send(b);
}
b.forEach(session.send);
}
};
export const handle_P2PRdy = (session: Session, _: DataView) => {
const b = SendUsrChk("admin", "admin", session.outgoingCommandId);
session.send(b);
};
+5 -2
View File
@@ -1,5 +1,5 @@
// hacked hexdump from frida to work on normal ArrayBuffers
import "./shim.ts";
import "./shim.js";
export const hexdump = (target, options) => {
options = options || {};
@@ -10,12 +10,15 @@ export const hexdump = (target, options) => {
const ansiColor = options.hasOwnProperty("ansiColor") ? options.ansiColor : 0;
let buffer;
if (target instanceof DataView) {
target = target.buffer;
}
if (target instanceof ArrayBuffer) {
if (length === undefined) length = target.byteLength;
else length = Math.min(length, target.byteLength);
buffer = target;
} else {
throw "Gimme array buffer";
throw "Gimme array buffer or DataView";
}
const startAddress = 0;
+127
View File
@@ -0,0 +1,127 @@
import { RemoteInfo } from "dgram";
import { readFileSync } from "node:fs";
import http from "node:http";
import { opt } from "./options.js";
import { discoverDevices } from "./discovery.js";
import { DevSerial } from "./impl.js";
import { Handlers, makeSession, Session, startVideoStream } from "./session.js";
let BOUNDARY = "a very good boundary line";
let responses: Record<string, http.ServerResponse[]> = {};
let audioResponses: Record<string, http.ServerResponse[]> = {};
let sessions: Record<string, Session> = {};
export const serveHttp = (opts: opt, port: number, with_audio: boolean) => {
const server = http.createServer((req, res) => {
if (req.url.startsWith("/ui/")) {
let devId = req.url.split("/")[2];
let s = sessions[devId];
if (s === undefined) {
res.writeHead(400);
res.end("invalid ID");
return;
}
if (!s.connected) {
res.writeHead(400);
res.end("Nothing online");
return;
}
const ui = readFileSync("asd.html").toString();
res.end(ui.replace(/\${id}/g, devId));
return;
}
if (req.url.startsWith("/audio/")) {
let devId = req.url.split("/")[2];
let s = sessions[devId];
if (s === undefined) {
res.writeHead(400);
res.end("invalid ID");
return;
}
if (!s.connected) {
res.writeHead(400);
res.end("Nothing online");
return;
}
res.setHeader("Content-Type", `text/event-stream`);
audioResponses[devId].push(res);
return;
}
if (req.url.startsWith("/camera/")) {
let devId = req.url.split("/")[2];
console.log("requested for", devId);
let s = sessions[devId];
if (s === undefined) {
res.writeHead(400);
res.end("invalid ID");
return;
}
if (!s.connected) {
res.writeHead(400);
res.end("Nothing online");
return;
}
res.setHeader("Content-Type", `multipart/x-mixed-replace; boundary="${BOUNDARY}"`);
responses[devId].push(res);
res.on("close", () => {
responses[devId] = responses[devId].filter((r) => r !== res);
console.log("Conn closed, kicked");
});
} else {
res.write(`<html>`);
Object.keys(sessions).forEach((id) =>
res.write(`<h2>${id}</h2><a href="/ui/${id}"><img src="/camera/${id}"/></a><hr/>`),
);
res.write(`</html>`);
res.end();
}
});
let devEv = discoverDevices(opts.debug, opts.discovery_ip);
devEv.on("discover", (rinfo: RemoteInfo, dev: DevSerial) => {
if (dev.devId in sessions) {
console.log(`ignoring ${dev.devId} - ${rinfo.address}`);
return;
}
console.log(`discovered ${dev.devId} - ${rinfo.address}`);
responses[dev.devId] = [];
audioResponses[dev.devId] = [];
const s = makeSession(Handlers, dev, rinfo, startVideoStream, opts);
const header = Buffer.from(`--${BOUNDARY}\r\nContent-Type: image/jpeg\r\n\r\n`);
s.eventEmitter.on("frame", () => {
const assembled = Buffer.concat(s.curImage);
responses[dev.devId].forEach((res) => {
res.write(header);
res.write(assembled);
});
});
s.eventEmitter.on("disconnect", () => {
console.log("deleting from sessions");
delete sessions[dev.devId];
});
if (with_audio) {
s.eventEmitter.on("audio", ({ gap, data }) => {
// ew, maybe WS?
var b64encoded = Buffer.from(data).toString("base64");
audioResponses[dev.devId].forEach((res) => {
res.write("data: ");
res.write(b64encoded);
res.write("\n\n");
});
});
}
sessions[dev.devId] = s;
});
console.log(`Starting HTTP server on port ${port}`);
server.listen(port);
};
+168 -69
View File
@@ -1,93 +1,165 @@
import "./shim.ts";
import "./shim.js";
import { Commands } from "./datatypes.js";
import { Session } from "./server.js";
import { ccDest, Commands, ControlCommands } from "./datatypes.js";
import { XqBytesEnc } from "./func_replacements.js";
import { hexdump } from "./hexdump.js";
import { Session } from "./session.js";
import { u16_swap } from "./utils.js";
const str2byte = (s: string): number[] => {
return Array.from(s).map((_, i) => s.charCodeAt(i));
};
const CmdSndProcHdr = (start: number, cmd: number, len: number, dest: number): DataView => {
len = len + 4; // hdr size?
let cmdHeader = new DataView(new Uint8Array(8).buffer);
cmdHeader.writeU16(u16_swap(start));
cmdHeader.add(2).writeU16(u16_swap(cmd));
cmdHeader.add(4).writeU16(u16_swap(len));
cmdHeader.add(6).writeU16(u16_swap(dest));
return cmdHeader;
const makeDataReadWrite = (session: Session, command: number, data: DataView | null): DataView => {
const DRW_HEADER_LEN = 0x10;
const TOKEN_LEN = 0x4;
const CHANNEL = 0;
const START_CMD = 0x110a;
let pkt_len = DRW_HEADER_LEN + TOKEN_LEN;
let payload_len = TOKEN_LEN;
let bufCopy: Uint8Array | null = null;
if (data && data.byteLength > 4) {
bufCopy = new Uint8Array(data.buffer);
const bufDV = new DataView(bufCopy.buffer);
// this mutates the buffer, don't want to mutate the caller
XqBytesEnc(bufDV, bufDV.byteLength, 4);
pkt_len += bufDV.byteLength;
payload_len += bufDV.byteLength;
}
const ret = new DataView(new Uint8Array(pkt_len).buffer);
ret.add(0).writeU16(Commands.Drw);
ret.add(2).writeU16(pkt_len - 4); // -4 as we ignore the [0xf1, 0xd0, len, len]
ret.add(4).writeU8(0xd1); // ?
ret.add(5).writeU8(CHANNEL);
ret.add(6).writeU16(session.outgoingCommandId);
ret.add(8).writeU16(START_CMD);
ret.add(10).writeU16(command);
ret.add(12).writeU16(u16_swap(payload_len));
ret.add(14).writeU16(ccDest[command]);
ret.add(16).writeByteArray(session.ticket);
if (data && data.byteLength > 4) {
ret.add(20).writeByteArray(bufCopy);
}
session.outgoingCommandId++;
return ret;
};
const DrwHdr = (cmd: number, len: number, d1_or_d2: 0xd1 | 0xd2, m_chan: number, pkt_id: number): DataView => {
let retret = new DataView(new Uint8Array(len + 4).buffer);
retret.writeU16(cmd);
retret.add(2).writeU16(len); // buflen -4?
retret.add(4).writeU8(d1_or_d2);
retret.add(5).writeU8(m_chan); // chan? hardcoded
retret.add(6).writeU16(pkt_id);
return retret;
export const SendIRToggle = (session: Session): DataView => {
return makeDataReadWrite(session, ControlCommands.IRToggle, null);
};
export const SendDevStatus = (session: Session): DataView => {
return makeDataReadWrite(session, ControlCommands.DevStatus, null);
};
export const SendWifiSettings = (session: Session): DataView => {
return makeDataReadWrite(session, ControlCommands.WifiSettings, null);
};
export const SendListWifi = (session: Session): DataView => {
return makeDataReadWrite(session, ControlCommands.ListWifi, null);
};
export const SendStopVideo = (session: Session): DataView => {
return makeDataReadWrite(session, ControlCommands.StopVideo, null);
};
export const SendStartVideo = (session: Session): DataView => {
// TODO: extract SendUsrChk
let buf = new DataView(new Uint8Array(0x18).buffer);
// console.log(hexdump(DrwHdr(0xf1d0, 0x0114, 0xd1, 0, pkt_id).buffer));
let bytes = [
0xf1,
0xd0,
0x01, // len? lower values= no response, larger values = 1 frame then kicked
0x14, // len
0xd1, // ?
0x00, // chan
session.outgoingCommandId >> 8,
session.outgoingCommandId,
0x11,
0x0a,
0x10,
0x30,
0x08,
0x01,
0x00,
0x00,
session.ticket[0],
session.ticket[1],
session.ticket[2],
session.ticket[3],
0x01,
0x01,
0x01,
0x01,
];
buf.writeByteArray(bytes);
return buf;
return makeDataReadWrite(session, ControlCommands.StartVideo, null);
};
export const SendUsrChk = (username: string, password: string, pkt_id: number): DataView => {
// type is char account[0x20]; char password[0x80];
export const getVideoKey = (session: Session): void => {
// this is not useful at all
for (let i = 0; i < 12; i++) {
// payload len??
const payload = [0x0, i]; //, 0x0, 0x0, 0x0, 0x0];
const dv = new DataView(new Uint8Array(payload).buffer);
session.send(makeDataReadWrite(session, ControlCommands.VideoParamGet, dv));
}
};
export const SendVideoResolution = (session: Session, resol: 1 | 2 | 3 | 4): DataView[] => {
// seems like 0x1 = resolution, and is specified by ID not by size
// unclear what 0x2-0xf achieve - they report back as '0' always -- ignored?
const pairs = {
1: [
// 320 x 240
[0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0],
//[0x7, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0],
],
2: [
// 640x480
[0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0],
//[0x7, 0x0, 0x0, 0x0, 0x50, 0x0, 0x0, 0x0],
],
3: [
// also 640x480 on the X5 -- hwat now?
[0x1, 0x0, 0x0, 0x0, 0x3, 0x0, 0x0, 0x0],
//[0x7, 0x0, 0x0, 0x0, 0x78, 0x0, 0x0, 0x0],
],
4: [
// also 640x480 on the X5 -- hwat now?
[0x1, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0],
//[0x7, 0x0, 0x0, 0x0, 0xa0, 0x0, 0x0, 0x0],
],
// maybe the 0x7 = bitrate??
};
return pairs[resol].map((payload: number[]) => {
const dv = new DataView(new Uint8Array(payload).buffer);
return makeDataReadWrite(session, ControlCommands.VideoParamSet, dv);
});
};
export const SendReboot = (session: Session): DataView => {
let dv = null;
return makeDataReadWrite(session, ControlCommands.Reboot, dv);
};
export const SendWifiDetails = (session: Session, ssid: string, password: string, dhcp: boolean): DataView => {
if (!dhcp) {
throw new Error("only DHCP is supported");
}
let buf = new Uint8Array(0x108).fill(0);
let cmd_payload = new DataView(buf.buffer);
let mask_reversed = "0.255.255.255";
// unclear which is which ))
let m_ip = "0.0.0.0";
let m_gw = "0.0.0.0";
let m_dns1 = "0.0.0.0";
let m_dns2 = "0.0.0.0";
cmd_payload.add(0x14).writeU8(1); // DHCP ?
cmd_payload.add(0x18).writeByteArray(str2byte(ssid));
cmd_payload.add(0x38).writeByteArray(str2byte(password));
cmd_payload.add(0xb8).writeByteArray(str2byte(mask_reversed));
cmd_payload.add(0xc8).writeByteArray(str2byte(m_ip));
cmd_payload.add(0xd8).writeByteArray(str2byte(m_gw));
cmd_payload.add(0xe8).writeByteArray(str2byte(m_dns1));
cmd_payload.add(0xf8).writeByteArray(str2byte(m_dns2));
const ret = makeDataReadWrite(session, ControlCommands.WifiSettingsSet, cmd_payload);
return ret;
};
export const SendUsrChk = (session: Session, username: string, password: string): DataView => {
let buf = new Uint8Array(0x20 + 0x80);
buf.fill(0);
let cmd_payload = new DataView(buf.buffer);
// type is char account[0x20]; char password[0x80];
cmd_payload.writeByteArray(str2byte(username));
cmd_payload.add(0x20).writeByteArray(str2byte(password));
return makeDataReadWrite(session, ControlCommands.ConnectUser, cmd_payload);
};
const start = 0xa11;
const dest = 0xff;
const cmd = 0x1020;
const len = buf.byteLength;
let cmdHeader = CmdSndProcHdr(start, cmd, len, dest);
let ret = new DataView(new Uint8Array(12 + len).buffer);
ret.writeByteArray(new Uint8Array(cmdHeader.buffer));
XqBytesEnc(cmd_payload, 0x20 + 0x80, 4);
ret.add(12).writeByteArray(new Uint8Array(cmd_payload.buffer));
// need to encapsulate this into create_Drw(outbuf, 0xd1, param4?, svar1?,
// copy_len, inbuf); seems like param4/svar1 are overflowing == maybe '0xa'
// and '0x2010'??
let retret = DrwHdr(0xf1d0, 8 + 12 + len - 4, 0xd1, 0, pkt_id);
retret.add(8).writeByteArray(new Uint8Array(ret.buffer));
return retret;
export const create_LanSearch = (): DataView => {
const outbuf = new DataView(new Uint8Array(4).buffer);
outbuf.writeU16(Commands.LanSearch);
outbuf.add(2).writeU16(0x0);
return outbuf;
};
export const create_P2pRdy = (inbuf: DataView): DataView => {
@@ -98,3 +170,30 @@ export const create_P2pRdy = (inbuf: DataView): DataView => {
outbuf.add(4).writeByteArray(new Uint8Array(inbuf.readByteArray(P2PRDY_SIZE).buffer));
return outbuf;
};
export const create_P2pAlive = (): DataView => {
const outbuf = new DataView(new Uint8Array(4).buffer);
outbuf.writeU16(Commands.P2PAlive);
outbuf.add(2).writeU16(0);
return outbuf;
};
export const create_P2pClose = (): DataView => {
const outbuf = new DataView(new Uint8Array(4).buffer);
outbuf.writeU16(Commands.Close);
outbuf.add(2).writeU16(0);
return outbuf;
};
export type DevSerial = { prefix: string; serial: string; suffix: string; serialU64: bigint; devId: string };
export const parse_PunchPkt = (dv: DataView): DevSerial => {
const punchCmd = dv.readU16();
const len = dv.add(2).readU16();
const prefix = dv.add(4).readString(4);
const serialU64 = dv.add(8).readU64();
const serial = serialU64.toString();
const suffix = dv.add(16).readString(len - 16 + 4); // 16 = offset, +4 header
const devId = prefix + serial + suffix;
return { prefix, serial, suffix, serialU64, devId };
};
+6
View File
@@ -0,0 +1,6 @@
export type opt = {
debug: boolean;
ansi: boolean;
discovery_ip: string;
attempt_to_fix_packet_loss: boolean;
};
+455 -28
View File
@@ -4,7 +4,12 @@
"requires": true,
"packages": {
"": {
"dependencies": {
"esbuild": "^0.20.2",
"yargs": "^17.7.2"
},
"devDependencies": {
"@types/yargs": "^17.0.32",
"mocha": "^10.2.0",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
@@ -22,6 +27,351 @@
"node": ">=12"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
"integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
"cpu": [
"ppc64"
],
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
"integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
"integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
"integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
"integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
"integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
"integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
"integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
"integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
"integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
"integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
"integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
"cpu": [
"loong64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
"integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
"cpu": [
"mips64el"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
"integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
"cpu": [
"ppc64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
"integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
"cpu": [
"riscv64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
"integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
"cpu": [
"s390x"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
"integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
"integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
"integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
"integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
"integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
"integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
"integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
@@ -81,6 +431,21 @@
"undici-types": "~5.26.4"
}
},
"node_modules/@types/yargs": {
"version": "17.0.32",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz",
"integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==",
"dev": true,
"dependencies": {
"@types/yargs-parser": "*"
}
},
"node_modules/@types/yargs-parser": {
"version": "21.0.3",
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
"integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
"dev": true
},
"node_modules/acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
@@ -115,7 +480,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -124,7 +488,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
@@ -270,21 +633,22 @@
}
},
"node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"dev": true,
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
@@ -295,8 +659,7 @@
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/concat-map": {
"version": "0.0.1",
@@ -357,14 +720,49 @@
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/esbuild": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.20.2",
"@esbuild/android-arm": "0.20.2",
"@esbuild/android-arm64": "0.20.2",
"@esbuild/android-x64": "0.20.2",
"@esbuild/darwin-arm64": "0.20.2",
"@esbuild/darwin-x64": "0.20.2",
"@esbuild/freebsd-arm64": "0.20.2",
"@esbuild/freebsd-x64": "0.20.2",
"@esbuild/linux-arm": "0.20.2",
"@esbuild/linux-arm64": "0.20.2",
"@esbuild/linux-ia32": "0.20.2",
"@esbuild/linux-loong64": "0.20.2",
"@esbuild/linux-mips64el": "0.20.2",
"@esbuild/linux-ppc64": "0.20.2",
"@esbuild/linux-riscv64": "0.20.2",
"@esbuild/linux-s390x": "0.20.2",
"@esbuild/linux-x64": "0.20.2",
"@esbuild/netbsd-x64": "0.20.2",
"@esbuild/openbsd-x64": "0.20.2",
"@esbuild/sunos-x64": "0.20.2",
"@esbuild/win32-arm64": "0.20.2",
"@esbuild/win32-ia32": "0.20.2",
"@esbuild/win32-x64": "0.20.2"
}
},
"node_modules/escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
"dev": true,
"engines": {
"node": ">=6"
}
@@ -442,7 +840,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
@@ -560,7 +957,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"engines": {
"node": ">=8"
}
@@ -708,6 +1104,35 @@
"url": "https://opencollective.com/mochajs"
}
},
"node_modules/mocha/node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"dev": true,
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"node_modules/mocha/node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dev": true,
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -829,7 +1254,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -867,7 +1291,6 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -881,7 +1304,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -1016,7 +1438,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -1039,27 +1460,25 @@
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/yargs": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dev": true,
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dependencies": {
"cliui": "^7.0.2",
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.0",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=10"
"node": ">=12"
}
},
"node_modules/yargs-parser": {
@@ -1086,6 +1505,14 @@
"node": ">=10"
}
},
"node_modules/yargs/node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"engines": {
"node": ">=12"
}
},
"node_modules/yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+8 -1
View File
@@ -1,11 +1,18 @@
{
"type": "module",
"scripts": {
"test": "mocha tests"
"test": "mocha tests",
"tsc": "tsc",
"build": "esbuild cmd/bin.ts --bundle --platform=node --outfile=dist/bin.cjs --target=node12"
},
"devDependencies": {
"@types/yargs": "^17.0.32",
"mocha": "^10.2.0",
"ts-node": "^10.9.2",
"esbuild": "^0.20.2",
"typescript": "^5.3.3"
},
"dependencies": {
"yargs": "^17.7.2"
}
}
+33
View File
@@ -0,0 +1,33 @@
import { RemoteInfo } from "dgram";
import { opt } from "./options.js";
import { discoverDevices } from "./discovery.js";
import { DevSerial } from "./impl.js";
import { Handlers, makeSession, Session, configureWifi } from "./session.js";
export const pair = ({ opts, ssid, password }: { opts: opt; ssid: string; password: string }) => {
console.log(`Will configure any devices found to join ${ssid}`);
let sessions: Record<string, Session> = {};
let devEv = discoverDevices(opts.debug, opts.discovery_ip);
if (password == "") {
throw new Error("You must set a non-zero-length password");
}
const onLogin = configureWifi(ssid, password);
devEv.on("discover", (rinfo: RemoteInfo, dev: DevSerial) => {
if (dev.devId in sessions) {
console.log(`ignoring ${dev.devId} - ${rinfo.address}`);
return;
}
console.log(`discovered ${dev.devId} - ${rinfo.address}`);
const s = makeSession(Handlers, dev, rinfo, onLogin, opts);
s.eventEmitter.on("disconnect", () => {
console.log("deleting from sessions");
delete sessions[dev.devId];
});
sessions[dev.devId] = s;
});
};
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 939 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

+63
View File
@@ -273,3 +273,66 @@ AvcLIB = src/object_jni.cpp, line 653, SystemCmd:[p2pID=BATC609531EXLVS]SystemCm
AvcLIB = src/object_jni.cpp, line 653, SystemCmd:[p2pID=BATC609531EXLVS]SystemCmd=0x1040
SystemCmd 0x1008 ret: 12
SystemCmd 0x1040 ret: 92
battery max = 3650 while transmitting
battery min = 3200 (powered off)
bat 3480 -> 3200 while transmitting via wifi = 106 seconds
----
XqStrDec param1 SWPNPDPFLVAOLNSXPHSQPIEOPAIDENLXHXEHIFLKPGLRHUARSTLQEEEPSUIHPDLSPEAOICLOSQEMLPPALNIBIAERHZLKHXEJHYHUEIEHELEEEKEG => strlen'd => 112
=> 4;139.155.68.77;119.45.114.92;162.62.63.154;3.132.215.40
112 /2 = 56
buf = 57
res = 56 char + \0
codetable = AAABACADAEAFAGAHAIAJAKALAMANAOAPAQARASATAUAVAWAXAYAZBABBBCBDBEBFBGBHBIBJBKBLBMBNBOBPBQBRBSBTBUBVBWBXBYBZCACBCCCDCECFCGCHCICJCKCLCMCNCOCPCQCRCSCTCUCVCWCXCYCZDADBDCDDDEDFDGDHDIDJDKDLDMDNDODPDQDRDSDTDUDVDWDXDYDZEAEBECEDEEEFEGEHEIEJEKELEMENEOEPEQERESETEUEVEWEXEYEZFAFBFCFDFEFFFGFHFIFJFKFLFMFNFOFPFQFRFSFTFUFVFWFXFYFZGAGBGCGDGEGFGGGHGIGJGKGLGMGNGOGPGQGRGSGTGUGVGWGXGYGZHAHBHCHDHEHFHGHHHIHJHKHLHMHNHOHPHQHRHSHTHUHVHWHXHYHZIAIBICIDIEIFIGIHIIIJIKILIMINIOIPIQIRISITIUIVIWIXIYIZJAJBJCJDJEJFJGJHJIJJJKJLJMJNJOJPJQJRJSJTJUJVJWJXJYJZKAKBKCKDKEKFKGKHKIKJKKKLKMKNKOKPKQKRKSKTKUKVKWKXKYKZLALBLCLDLELFLGLHLILJLKLLLMLNLOLPLQLRLSLTLULVLWLXLYLZMAMBMCMDMEMFMGMHMIMJMKMLMMMNMOMPMQMRMSMTMUMVMWMXMYMZNANBNCNDNENFNGNHNINJNKNLNMNNNONPNQNRNSNTNUNVNWNXNYNZOAOBOCODOEOFOGOHOIOJOKOLOMONOOOPOQOROSOTOUOVOWOXOYOZPAPBPCPDPEPFPGPHPIPJPKPLPMPNPOPPPQPRPSPTPUPVPWPXPYPZQAQBQCQDQEQFQGQHQIQJQKQLQMQNQOQPQQQRQSQTQUQVQWQXQYQZRARBRCRDRERFRGRHRIRJRKRLRMRNRORPRQRRRSRTRURVRWRXRYRZSASBSCSDSESFSGSHSISJSKSLSMSNSOSPSQSRSSSTSUSVSWSXSYSZTATBTCTDTETFTGTHTITJTKTLTMTNTOTPTQTRTSTTTUTVTWTXTYTZUAUBUCUDUEUFUGUHUIUJUKULUMUNUOUPUQURUSUTUUUVUWUXUYUZVAVBVCVDVEVFVGVHVIVJVKVLVMVNVOVPVQVRVSVTVUVVVWVXVYVZWAWBWCWDWEWFWGWHWIWJWKWLWMWNWOWPWQWRWSWTWUWVWWWXWYWZXAXBXCXDXEXFXGXHXIXJXKXLXMXNXOXPXQXRXSXTXUXVXWXXXYXZYAYBYCYDYEYFYGYHYIYJYKYLYMYNYOYPYQYRYSYTYUYVYWYXYYYZZAZBZCZDZEZFZGZHZIZJZKZLZMZNZOZPZQZRZSZTZUZVZWZXZYZZ
=
for i in string.ascii_uppercase:
for j in string.ascii_uppercase:
print(f'{i}{j}', end='')
what is this??
0000 24 02 00 00 01 0a 12 00 02 1c ff ff 00 00 00 00 $...............
0010 01 00 00 00 65 64 61 31 38 34 34 64 30 30 34 64 ....eda1844d004d
0020 33 36 34 33 61 62 66 64 66 62 36 63 38 62 34 32 3643abfdfb6c8b42
0030 35 36 30 33 00 00 00 00 00 00 00 00 00 00 00 00 5603............
0040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0050 00 00 00 00 01 00 00 00 09 00 00 00 78 00 00 00 ............x...
0060 61 36 65 34 36 34 37 38 34 35 33 63 39 61 65 61 a6e46478453c9aea
0070 63 61 35 61 66 36 32 34 00 00 00 00 00 00 00 00 ca5af624........
0080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00a0 31 61 64 37 35 61 65 37 32 65 30 35 64 63 66 34 1ad75ae72e05dcf4
00b0 64 61 64 62 64 31 37 61 00 00 00 00 00 00 00 00 dadbd17a........
00c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00e0 42 41 54 43 36 30 39 35 38 30 48 56 44 43 53 00 BATC609580HVDCS.
00f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0100 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0120 02 00 00 00 41 49 7a 61 53 79 42 2d 62 6f 78 4f ....AIzaSyB-boxO
0130 47 35 6e 36 41 62 4b 4d 4c 41 4f 77 6d 6d 31 50 G5n6AbKMLAOwmm1P
0140 5a 7a 71 50 6b 79 5a 6a 4d 77 63 00 00 00 00 00 ZzqPkyZjMwc.....
0150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0160 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0170 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0180 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0190 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01b0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01c0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01d0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01e0 00 00 00 00 00 00 00 00 41 49 7a 61 53 79 42 2d ........AIzaSyB-
01f0 62 6f 78 4f 47 35 6e 36 41 62 4b 4d 4c 41 4f 77 boxOG5n6AbKMLAOw
0200 6d 6d 31 50 5a 7a 71 50 6b 79 5a 6a 4d 77 63 00 mm1PZzqPkyZjMwc.
0210 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0220 00 00 00 00 00 00 00 00 ........
-- this looks like google keys: AIzaSyB
+65
View File
@@ -0,0 +1,65 @@
import string
data = [
0xf1, 0xd0, 0x01, 0x18, 0xd1, 0x00, 0x00, 0x02, 0x11, 0x0a, 0x01, 0x60, 0x0c, 0x01, 0x00, 0x00,
0x4c, 0x31, 0x67, 0x4e, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01, 0x72, 0x6a, 0x78, 0x6f, 0x64, 0x75, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x72, 0x74, 0x71, 0x64, 0x73, 0x62, 0x73, 0x60,
0x71, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x31, 0x2f, 0x33, 0x34, 0x34, 0x2f, 0x33, 0x34,
0x34, 0x2f, 0x33, 0x34, 0x34, 0x01, 0x01, 0x01, 0x31, 0x2f, 0x31, 0x2f, 0x31, 0x2f, 0x31, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x31, 0x2f, 0x31, 0x2f, 0x31, 0x2f, 0x31, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x31, 0x2f, 0x31, 0x2f, 0x31, 0x2f, 0x31, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x31, 0x2f, 0x31, 0x2f, 0x31, 0x2f, 0x31, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
]
def dec(d):
ret = []
for b in d[0x14:]:
if b % 2:
ret.append(b - 1)
else:
ret.append(b + 1)
ret = d[:0x14] + ret[-4:] + ret[:-4]
return ret
def hexdump(dump):
print(" " * 10, end="")
for i in range(0, 0xF+1):
print(f"{i:2X}", end=" ")
print(" ", end="")
for i in range(0, 0xF+1):
print(f"{i:X}", end="")
print()
for i in range(0, len(dump), 0x10):
line = dump[i:i+0x10]
print(f"{i:08x}", end=" ")
for b in line:
print(f"{b:02x}", end=" ")
if len(line) < 16:
print(" " * (16 - len(line)), end="")
print(" ", end="")
for b in line:
rep = "."
if chr(b) in string.digits or chr(b) in string.ascii_letters:
rep = chr(b)
print(rep, end="")
print("")
_dec = dec(data)
hexdump(_dec[0x14:])
#print([hex(n) for n in _dec])
print("for test values, DEC")
print("".join([f"{n:02x}" for n in _dec]))
print("for test values, enc")
print("".join([f"{n:02x}" for n in data]))
+20
View File
@@ -0,0 +1,20 @@
import string
ct = []
for i in string.ascii_uppercase:
for j in string.ascii_uppercase:
ct.append(f'{i}{j}')
insec = 'SWPNPDPFLVAOLNSXPHSQPIEOPAIDENLXHXEHIFLKPGLRHUARSTLQEEEPSUIHPDLSPEAOICLOSQEMLPPALNIBIAERHZLKHXEJHYHUEIEHELEEEKEG'
insec = 'PFLXLSTBLKHYLPLRHUEHIEEGEEARLQPLIHIAEKAOSTLVEOSQPDHZLNPAICIFEREJLKEMENHUHXIBEPEEEHEIEL'
insec = 'EKTDROREHXHURHRKRMCXEEKPRNKKUZPNLXNYNOHYAONRNUNWRJSQGZNXGUNTIHKIJYEHPAKBHTKEKGKDLKJVKHKFERGSGIEIHUGLEDGOGQGNEEGFGRGP'
out = '4;139.155.68.77;119.45.114.92;162.62.63.154;3.132.215.40'
dec = ''
for i in range(0, len(insec), 2):
cur = insec[i:i+2]
idx = ct.index(cur)
mod_cnt = idx // 0x5e
_sum = (idx + (mod_cnt * -0x5e) + 0x20) & 0x7f
dec += chr(_sum)
print(dec)
-136
View File
@@ -1,136 +0,0 @@
import dgram from "node:dgram";
import { createWriteStream } from "node:fs";
import { create_LanSearch } from "./func_replacements.js";
import { Commands, CommandsByValue } from "./datatypes.js";
import { handle_P2PAlive, handle_PunchPkt, handle_P2PRdy, handle_Drw, notImpl, noop } from "./handlers.js";
import { hexdump } from "./hexdump.js";
import EventEmitter from "node:events";
export type Session = {
send: (msg: DataView) => void;
broadcast: (msg: DataView) => void;
outgoingCommandId: number;
ticket: number[];
eventEmitter: EventEmitter;
};
export type PacketHandler = (session: Session, dv: DataView) => void;
type opt = {
debug: boolean;
ansi: boolean;
};
type msgCb = (session: Session, msg: Buffer, rinfo: any, options: opt) => void;
type connCb = (session: Session) => void;
const makeSession = (cb: msgCb, connCb: connCb, options?: opt): Session => {
const sock = dgram.createSocket("udp4");
sock.on("error", (err) => {
console.error(`sock error:\n${err.stack}`);
sock.close();
});
sock.on("message", (msg, rinfo) => cb(session, msg, rinfo, options));
sock.on("listening", () => {
const address = sock.address();
console.log(`sock listening ${address.address}:${address.port}`);
sock.setBroadcast(true);
connCb(session);
});
const RECV_PORT = 49512; // important?
const DST_IP = "192.168.1.1";
const BCAST_IP = "192.168.1.255";
const SEND_PORT = 32108;
sock.bind(RECV_PORT);
const session: Session = {
outgoingCommandId: 0,
ticket: [0, 0, 0, 0],
eventEmitter: new EventEmitter(),
send: (msg: DataView) => {
const raw = msg.readU16();
const cmd = CommandsByValue[raw];
if (options.debug) {
console.log(`>> ${cmd}`);
console.log(hexdump(msg.buffer, { ansi: options.ansi, ansiColor: 0 }));
}
if (raw == Commands.Drw) {
// not sure why cmd == Commands.Drw does not work
session.outgoingCommandId++;
}
sock.send(new Uint8Array(msg.buffer), SEND_PORT, DST_IP);
},
broadcast: (msg: DataView) => sock.send(new Uint8Array(msg.buffer), SEND_PORT, BCAST_IP),
};
return session;
};
const Handlers: Record<keyof typeof Commands, PacketHandler> = {
PunchPkt: handle_PunchPkt,
Close: notImpl,
LanSearchExt: notImpl,
LanSearch: notImpl,
P2PAlive: handle_P2PAlive,
P2PAliveAck: notImpl,
Hello: notImpl,
P2pRdy: handle_P2PRdy,
P2pReq: notImpl,
LstReq: notImpl,
DrwAck: noop,
Drw: handle_Drw,
// From CSession_CtrlPkt_Proc, incomplete
PunchTo: notImpl,
HelloAck: notImpl,
RlyTo: notImpl,
DevLgnAck: notImpl,
P2PReqAck: notImpl,
ListenReqAck: notImpl,
RlyHelloAck: notImpl, // always
RlyHelloAck2: notImpl, // if len >1??
};
const s = makeSession(
(session, msg, _, options) => {
const ab = new Uint8Array(msg).buffer;
const dv = new DataView(ab);
const cmd = CommandsByValue[dv.readU16()];
if (options.debug) {
console.log(`<< ${cmd}`);
console.log(hexdump(msg.buffer, { ansi: options.ansi, ansiColor: 1 }));
}
Handlers[cmd](session, dv);
},
(session) => {
const int = setInterval(() => {
let buf = new DataView(new Uint8Array(4).buffer);
create_LanSearch(buf);
session.broadcast(buf);
}, 1000);
},
{ debug: false, ansi: false },
);
let cur_image_index = 0;
const audioFd = createWriteStream(`captures/audio.pcm`);
s.eventEmitter.on("frame", (frame: Buffer) => {
const fname = `captures/${cur_image_index.toString().padStart(4, "0")}.jpg`;
let cur_image = createWriteStream(fname);
cur_image_index++;
cur_image.write(frame);
cur_image.close();
console.log("got an entire frame", frame.length);
});
s.eventEmitter.on("audio", (frame: Buffer) => {
audioFd.write(frame);
});
s.eventEmitter.on("connect", (name: string) => {
console.log(`Connected to ${name}`);
});
+159
View File
@@ -0,0 +1,159 @@
import { createSocket, RemoteInfo } from "node:dgram";
import EventEmitter from "node:events";
import { Commands, CommandsByValue } from "./datatypes.js";
import { handle_Drw, handle_P2PAlive, handle_P2PRdy, makePunchPkt, noop, notImpl } from "./handlers.js";
import { hexdump } from "./hexdump.js";
import { create_P2pAlive, DevSerial, SendStartVideo, SendVideoResolution, SendWifiDetails } from "./impl.js";
import { opt } from "./options.js";
export type Session = {
send: (msg: DataView) => void;
outgoingCommandId: number;
ticket: number[];
eventEmitter: EventEmitter;
dst_ip: string;
lastReceivedPacket: number;
connected: boolean;
devName: string;
timers: ReturnType<typeof setInterval>[];
curImage: Buffer[];
rcvSeqId: number;
frame_is_bad: boolean;
frame_was_fixed: boolean;
options: opt;
};
export type PacketHandler = (session: Session, dv: DataView, rinfo: RemoteInfo) => void;
type msgCb = (
session: Session,
handlers: Record<keyof typeof Commands, PacketHandler>,
msg: Buffer,
rinfo: RemoteInfo,
) => void;
const handleIncoming: msgCb = (session, handlers, msg, rinfo) => {
const ab = new Uint8Array(msg).buffer;
const dv = new DataView(ab);
const cmd = CommandsByValue[dv.readU16()];
if (session.options.debug) {
console.log(`<< ${cmd}`);
console.log(hexdump(msg.buffer, { ansi: session.options.ansi, ansiColor: 1 }));
}
handlers[cmd](session, dv, rinfo);
session.lastReceivedPacket = Date.now();
};
export const makeSession = (
handlers: Record<keyof typeof Commands, PacketHandler>,
dev: DevSerial,
ra: RemoteInfo,
onLogin: (s: Session) => void,
options: opt,
): Session => {
const sock = createSocket("udp4");
sock.on("error", (err) => {
console.error(`sock error:\n${err.stack}`);
sock.close();
});
sock.on("message", (msg, rinfo) => handleIncoming(session, handlers, msg, rinfo));
sock.on("listening", () => {
const buf = makePunchPkt(dev);
session.send(buf);
});
const SEND_PORT = 32108;
sock.bind();
const sessTimer = setInterval(() => {
const delta = Date.now() - session.lastReceivedPacket;
if (delta > 600) {
let buf = create_P2pAlive();
session.send(buf);
}
if (delta > 8000) {
session.eventEmitter.emit("disconnect");
}
}, 400);
const session: Session = {
outgoingCommandId: 0,
ticket: [0, 0, 0, 0],
lastReceivedPacket: 0,
eventEmitter: new EventEmitter(),
connected: true,
timers: [sessTimer],
devName: dev.devId,
send: (msg: DataView) => {
const raw = msg.readU16();
const cmd = CommandsByValue[raw];
if (options.debug) {
console.log(`>> ${cmd}`);
if (raw != Commands.P2PAlive) {
console.log(hexdump(msg.buffer, { ansi: options.ansi, ansiColor: 0 }));
}
}
sock.send(new Uint8Array(msg.buffer), SEND_PORT, session.dst_ip);
},
dst_ip: ra.address,
curImage: [],
rcvSeqId: 0,
frame_is_bad: false,
frame_was_fixed: false,
options: options,
};
session.eventEmitter.on("disconnect", () => {
console.log(`Disconnected from ${session.devName} - ${session.dst_ip}`);
session.dst_ip = "0.0.0.0";
session.connected = false;
session.timers.forEach((x) => clearInterval(x));
session.timers = [];
});
session.eventEmitter.on("login", () => {
console.log(`Logged in - ${session.devName}`);
onLogin(session);
});
return session;
};
export const configureWifi = (ssid: string, password: string) => {
return (s: Session) => {
[SendWifiDetails(s, ssid, password, true)].forEach(s.send);
};
};
export const startVideoStream = (s: Session) => {
[
...SendVideoResolution(s, 2), // 640x480
SendStartVideo(s),
].forEach(s.send);
};
export const Handlers: Record<keyof typeof Commands, PacketHandler> = {
PunchPkt: notImpl,
P2PAlive: handle_P2PAlive,
P2pRdy: handle_P2PRdy,
DrwAck: noop,
Drw: handle_Drw,
Close: notImpl,
LanSearchExt: notImpl,
LanSearch: notImpl,
P2PAliveAck: notImpl,
Hello: notImpl,
P2pReq: notImpl,
LstReq: notImpl,
PunchTo: notImpl,
HelloAck: notImpl,
RlyTo: notImpl,
DevLgnAck: notImpl,
P2PReqAck: notImpl,
ListenReqAck: notImpl,
RlyHelloAck: notImpl, // always
RlyHelloAck2: notImpl, // if len >1??
};
+24 -8
View File
@@ -1,5 +1,5 @@
DataView.prototype.add = function (offset) {
return new DataView(this.buffer, offset);
return new DataView(this.buffer, offset + this.byteOffset);
};
DataView.prototype.writeU8 = function (val) {
return this.setUint8(0, val);
@@ -19,9 +19,15 @@ DataView.prototype.readU8 = function () {
DataView.prototype.readU16 = function () {
return this.getUint16(0);
};
DataView.prototype.readU16LE = function () {
return this.getUint16(0, true);
};
DataView.prototype.readU32 = function () {
return this.getUint32(0);
};
DataView.prototype.readU32LE = function () {
return this.getUint32(0, true);
};
DataView.prototype.readU64 = function () {
return this.getBigUint64(0);
};
@@ -44,23 +50,33 @@ DataView.prototype.readByteArray = function (len) {
};
DataView.prototype.readString = function (len) {
const ba = this.readByteArray(len);
return String.fromCharCode.apply(null, new Uint8Array(ba.buffer));
const s = String.fromCharCode.apply(null, new Uint8Array(ba.buffer));
const nullByte = s.indexOf("\0");
if (nullByte !== -1) return s.substring(0, nullByte);
return s;
};
DataView.prototype.writeString = function (str) {
const bytes = [...str].map((_, i) => str.charCodeAt(i));
return this.writeByteArray(bytes);
};
declare global {
interface DataView {
add(offset: number): DataView;
readByteArray(len: number): DataView;
writeString(str: string): void;
readString(len: number): string;
readU16(): number;
readU16LE(): number;
readU32(): number;
readU64(): number;
readU32LE(): number;
readU64(): bigint;
readU8(): number;
writeByteArray(arr: Uint8Array | number[]): undefined;
writeU16(n: number): undefined;
writeU32(n: number): undefined;
writeU64(n: number): undefined;
writeU8(n: number): undefined;
writeByteArray(arr: Uint8Array | number[]): void;
writeU16(n: number): void;
writeU32(n: number): void;
writeU64(n: bigint): void;
writeU8(n: number): void;
}
}
export default global;
+35 -9
View File
@@ -5,7 +5,7 @@ import assert from "assert";
import { XqBytesDec, XqBytesEnc } from "../func_replacements.js";
import { createResponseForControlCommand } from "../handlers.js";
import { hexdump } from "../hexdump.js";
import { SendUsrChk } from "../impl.ts";
import { SendStartVideo, SendDevStatus, SendUsrChk, SendWifiDetails } from "../impl.ts";
import { placeholderTypes, sprintf } from "../utils.js";
describe("debug_tools", () => {
@@ -106,6 +106,13 @@ describe("module", () => {
XqBytesEnc(in_buf, long_dec_bytes.byteLength, 4); // this mutates in_buf
assert.deepEqual(new Uint8Array(in_buf.buffer), long_enc_bytes);
});
/* TODO
it("decrypts offset dataviews", () => {
const in_buf = new DataView(simple_enc_bytes.buffer.slice(0));
XqBytesDec(in_buf, simple_enc_bytes.byteLength, 4); // this mutates in_buf
assert.deepEqual(new Uint8Array(in_buf.buffer), simple_dec_bytes);
});
*/
it("reverts Enc with Dec", () => {
const in_buf = new DataView(long_dec_bytes.buffer.slice(0));
XqBytesEnc(in_buf, long_dec_bytes.byteLength, 4); // this mutates in_buf
@@ -116,23 +123,42 @@ describe("module", () => {
});
const hstrToBA = (hs) => new Uint8Array(hs.match(/../g).map((h) => parseInt(h, 16))).buffer;
describe("events", () => {
it("emits login event upon logging in", () => {
// TODO
});
});
describe("make packet", () => {
it("builds a good SendUsrChk", () => {
const expected_str =
"f1d000b0d1000000110a2010a400ff00000000006f01010101010101010101010101010101010101010101010101010160656c686f01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010160656c68";
const expected = hstrToBA(expected_str);
assert.deepEqual(SendUsrChk("admin", "admin").buffer, expected);
const sess = { outgoingCommandId: 0, ticket: [0, 0, 0, 0] };
assert.deepEqual(SendUsrChk(sess, "admin", "admin").buffer, expected);
});
it("builds a good SendStartVideo", () => {
const input_pkt_str = "f1d00018d1000000110a20110c00ff000000000064504737fe010101";
// token-in = 0x64 0x50 0x47 0x37
const _expected_str = "f1d00114d1000000110a1030080100006551463601010101";
// output is 0x3010; 'start video'; hardcoded but shouldnt
const _expected_str = "f1d00010d1000000110a10300400000001020304";
const expected = hstrToBA(_expected_str);
const sess = { outgoingCommandId: 0, ticket: [0, 0, 0, 0] };
const got = createResponseForControlCommand(sess, new DataView(hstrToBA(input_pkt_str)));
const sess = { outgoingCommandId: 0, ticket: [1, 2, 3, 4] };
const got = SendStartVideo(sess);
assert.deepEqual(got.buffer, expected);
});
it("builds a good SendDevStatus", () => {
const sess = { outgoingCommandId: 0, ticket: [1, 2, 3, 4] };
const _expected_str = "f1d00010d1000000110a08100400000001020304";
const expected = hstrToBA(_expected_str);
const got = SendDevStatus(sess);
assert.deepEqual(got.buffer, expected);
});
it("builds a good WifiSettingsSet", () => {
const sess = { outgoingCommandId: 2, ticket: [1, 2, 3, 4] };
const _expected_str =
"f1d00118d1000002110a01600c010000010203040101010101010101010101010101010100010101726a786f647501010101010101010101010101010101010101010101010101017274716473627360710101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101312f3334342f3334342f333434010101312f312f312f31010101010101010101312f312f312f31010101010101010101312f312f312f31010101010101010101312f312f312f3101010101010101010101010101";
const expected = hstrToBA(_expected_str);
const got = SendWifiDetails(sess, "skynet", "supercrap", true);
assert.deepEqual(got.buffer, expected);
assert.deepEqual(sess.ticket, [0x65, 0x51, 0x46, 0x36]);
});
});
+8 -2
View File
@@ -1,10 +1,16 @@
{
"include": ["*.ts"],
"compilerOptions": {
"target": "es6",
"module": "esnext",
"module": "es6",
"moduleResolution": "node",
"esModuleInterop": true,
"moduleResolution": "node"
"preserveConstEnums": true,
"outDir": "./build/",
"rootDir": "./",
"strict": false,
"sourceMap": false
}
}
+2
View File
@@ -24,6 +24,8 @@ export const sprintf = (str, values) => {
.join("");
return s + str.slice(lastScanned);
};
export const u32_swap = (x) =>
((x & 0xff000000) >> 24) | ((x & 0xff0000) >> 8) | ((x & 0xff00) << 8) | ((x & 0xff) << 24);
export const u16_swap = (x) => ((x & 0xff00) >> 8) | ((x & 0x00ff) << 8);
export const swap_endianness_u16 = (ptr) => {
const bytes = ptr.readU16();