err4o4 / spotify-car-thing-reverse-engineering

394 stars 5 forks source link

Custom HTML using the stock, out-of-the-box firmware #24

Open vitorio opened 1 year ago

vitorio commented 1 year ago

It seemed like extra hassle to have to update the firmware just to run a custom webapp, so here's the minimum amount of JavaScript to initialize the stock, OOTB Superbird so you can see your own HTML on it.

Unlike the Chromium in later firmware, the stock Superbird is running QtWebEngine 5.12.7, equivalent to Chromium 69.0.3497.113 (although its User-Agent reports 69.0.3497.128). The webapp is built into the qt-superbird-app, embedded as Qt resources. It can be extracted from the binary using Ghidra and the scripts described in @gipi's article, Reversing C++, Qt based applications using Ghidra, available in their repo at https://github.com/gipi/ghidra_scripts.

There's no "Control WebSocket" in the stock app when it first launches, it instead initially uses a QWebChannel, and there appear to be a few initialization commands sent back from the webapp to qt-superbird-app before it will start rendering HTML and provide messages over the QWebChannel. Not all the initialization commands are necessary to start rendering HTML, and the status messages that come in regularly over the QWebChannel can be ignored.

QtWebEngine heavily caches all the files it accesses, and manually emptying the cache(s) between runs will spare a lot of headaches when testing.

Assuming you've followed in the instructions in @frederic's https://github.com/frederic/superbird-bulkcmd repo to enable ADB (or equivalent), you can do something like this to stop the existing processes, upload an example.html, clear the caches, and launch qt-superbird-app manually using the same configuration as specified for supervisord plus a Chrome remote debugging port:

$ adb shell supervisorctl stop swupdate
swupdate: stopped
$ adb shell supervisorctl stop superbird
superbird: stopped
$ adb shell mkdir /tmp/webapp
$ adb push example.html /tmp/webapp/
example.html: 1 file pushed, 0 skipped. 0.8 MB/s (1188 bytes in 0.001s)
$ adb forward tcp:9222 tcp:9222
9222
$ adb shell rm -rf /var/cache/QtWebEngineCache/*
$ adb shell rm -rf /var/cache/qt-superbird-app/*
$ adb shell 'QT_LOGGING_RULES="" QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --ignore-gpu-blacklist --touch-events=enabled" QT_QPA_EGLFS_INTEGRATION="eglfs_mali" QMLSCENE_DEVICE="" QT_QUICK_BACKEND="" QT_QPA_EGLFS_PHYSICAL_WIDTH="51" QT_QPA_EGLFS_PHYSICAL_HEIGHT="86" QT_QPA_EGLFS_ROTATION="-90" QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS="/dev/input/event3:rotate=270" QT_QPA_EGLFS_NO_LIBINPUT="1" HOME="/home/superbird" XDG_CACHE_HOME="/var/cache" XDG_RUNTIME_DIR="/var/cache/qt-superbird-app" QTWEBENGINE_REMOTE_DEBUGGING=9222 qt-superbird-app --config=/etc/qt-superbird-app/superbird_target.ini --url file:///tmp/webapp/example.html'
2015-01-01T03:44:04.870Z [D] main.cpp:66 BOOTMARK: main(): 0ms
2015-01-01T03:44:04.872Z [D] main.cpp:73 BOOTMARK: Config Parsed: 3ms
...

You can also run your webapp on your local machine and reverse the port to the Superbird using ADB, replacing --url file:///tmp/webapp/example.html above with --url http://localhost:8000/example.html:

$ adb reverse tcp:8000 tcp:8000
$ python3 -m http.server
Serving HTTP on port 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
...

Here's the contents of example.html, the meta elements are from the stock webapp, qwebchannel.js is built-in, the handleResponse function overloads the stock one to not delete the callbacks once they're executed to avoid some JS warnings, and the signalEmitted function is where you'd go to see the status messages coming in over the channel:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=800,height=480,initial-scale=1,maximum-scale=1,user-scalable=no">
<meta name="HandheldFriendly" content="true"/>
<title>Superbird</title>
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script>
var _superbirdchannel = new QWebChannel(qt.webChannelTransport, function(channel){});
_superbirdchannel.handleResponse = function(message) {
  if (!message.hasOwnProperty("id")) {
    console.error("Invalid response message received: ", JSON.stringify(message));
    return;
  };
  this.execCallbacks[message.id](message.data);
};
_superbirdchannel.execCallbacks[0] = function(data){};
_superbirdchannel.execCallbacks[1] = function(data){};
_superbirdchannel.objects = {};
_superbirdchannel.objects.superbird = {};
_superbirdchannel.objects.superbird.signalEmitted = function(signal, args){};
_superbirdchannel.send({type: 3, id: 0});
_superbirdchannel.send({type: 7, object: 'superbird', signal: 5});
_superbirdchannel.send({type: 6, object: 'superbird', method: 9, args: [], id: 1});
</script>
</head>
<body style="background-color:#0f0;">
<h1>superbird OOTB</h1>
</body>
</html>

When run, it looks like this:

superbirdootb

chaseadam commented 8 months ago

I ended up with a version of qt-superbird-app which does not appear to have support for --url argument:

The following arguments were not expected: file:///tmp/webapp/example.html --url

--help output confirms (assuming it would be listed):

✗ adb shell 'QT_LOGGING_RULES="" QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --ignore-gpu-blacklist --touch-event
s=enabled" QT_QPA_EGLFS_INTEGRATION="eglfs_mali" QMLSCENE_DEVICE="" QT_QUICK_BACKEND="" QT_QPA_EGLFS_PHYSICAL_WIDTH="51" QT_QPA_EGLF
S_PHYSICAL_HEIGHT="86" QT_QPA_EGLFS_ROTATION="-90" QT_QPA_EVDEV_TOUCHSCREEN_PARAMETERS="/dev/input/event3:rotate=270" QT_QPA_EGLFS_N
O_LIBINPUT="1" HOME="/home/superbird" XDG_CACHE_HOME="/var/cache" XDG_RUNTIME_DIR="/var/cache/qt-superbird-app" QTWEBENGINE_REMOTE_D
EBUGGING=9222 qt-superbird-app --config=/etc/qt-superbird-app/superbird_target.ini --help'
2015-01-01T00:28:33.042Z [D] main.cpp:65 BOOTMARK: main(): 0ms
Spotify Superbird Application: HEAD-v0.30.27-g2ec606be
Usage: qt-superbird-app [OPTIONS]

Options:
  -h,--help                   Print this help message and exit
  --config=superbird.ini      Read an ini file
  -v,--version                Print short version and exit
  -d,--data-path TEXT         Path of directory in which persistent app data is stored
  -w,--webapp-path TEXT       Path of directory in which webapp files are stored
  -t,--phone-host TEXT        Host address of the phone to connect to
  -b,--bt-host TEXT           Bluetooth MAC of the phone to connect to
  -p,--phone-port UINT        TCP Port on the phone to connect to
  -n,--serial-number-file TEXT
                              Path to a file containing a device serial number
  -a,--playback-device TEXT   ALSA playback device
  -r,--record-device TEXT     ALSA record device
  -C,--record-channels INT    Number of channels to record
  -B,--record-bits INT        Record bits
  -I,--input-channels INT     Number of actual input channels
  -f,--wakeword-file TEXT     Wakeword file #1
  -s,--wakeword-sensitivity INT
                              Wakeword #1 operating point
  --wakeword-frequency INT    Wakeword #1 HPF frequency
  --wakeword-windlevel INT    Wakeword #1 windlevel
  --wakeword-2-file TEXT      Wakeword file #2
  --wakeword-2-sensitivity INT
                              Wakeword #2 operating point
  --wakeword-2-frequency INT  Wakeword #2 HPF frequency
  --wakeword-2-windlevel INT  Wakeword #2 windlevel
  --wakeword-3-file TEXT      Wakeword file #3
  --wakeword-3-sensitivity INT
                              Wakeword #3 operating point
  --wakeword-3-frequency INT  Wakeword #3 HPF frequency
  --wakeword-3-windlevel INT  Wakeword #3 windlevel
  -e,--local-command-file TEXT
                              Local command file
  -m,--multimodel-file TEXT   Multimodel wakeword file (optional)
  -M,--multimodel-sensitivity INT
                              Multimodel wakeword operation point
  --pryon-wakeword-file TEXT  Pryon wakeword file (optional)
  --pryon-fingerprint-file TEXT
                              Pryon fingerprint list file (optional)
  --pryon-watermarking-file TEXT
                              Pryon watermarking file (optional)
  --pryon-sensitivity INT     Pryon wakeword operation point
  --afe-type TEXT             AFE type
  --afe-file TEXT             AFE model file
  -q,--root-cert-file TEXT    Root certificate file
  -S,--local-command-sensitivity INT:INT in [1 - 25]
                              Local commands operating point (1-25)
  -o,--opus-bitrate INT:INT in [0 - 64000]
                              Opus bitrate (6000-64000). 0 to bypass.
  -l,--log-file TEXT          Path to log file (default = stdout)
  --log-level INT             Global log level (0 -- 5)
  --hide-cursor BOOLEAN       Whether to hide the mouse cursor
  --send-wakeword             Send the wakeword in voice requests
  --record-requests-dir TEXT  Save audio from voice requests in the given directory. Empty string means don't record anything.
  --dump-raw-mic-audio BOOLEAN
                              Save raw microphone audio in the record-requests-dir directory.
  --playback-raw-mic-audio BOOLEAN
                              Playback raw microphone audio from the record-requests-dir directory.
  --playback-raw-mic-audio-options TEXT
                              Raw microphone audio playback options
  --ab-metadata-path TEXT     Path from which to load A/B partition data
  --ota-path TEXT             Path to store OTA packages
  --cache-path TEXT           Path to store cached data
  --image-cache-size INT:INT in [-1 - 2147483647]
                              Image cache size (-1 = Disable, 0 = Default, <value> = cache size in kb)
  --headless-mode BOOLEAN     Headless mode (no UI updates)
vitorio commented 8 months ago

Are you sure you're still running the stock, out-of-the-box firmware? I believe stock is version 6.3.29, whereas the final versions were 8.2.5 or 8.3.6, maybe? If there's a /usr/bin/chromium-browser then you're on a newer one, and this won't work for you, and you should instead try e.g. https://github.com/pajowu/superbird-custom-webapp