Skip to content

Harden OpenStream controls and polish Android UX#5

Closed
YashasVM wants to merge 3 commits into
mainfrom
codex/harden-openstream
Closed

Harden OpenStream controls and polish Android UX#5
YashasVM wants to merge 3 commits into
mainfrom
codex/harden-openstream

Conversation

@YashasVM

@YashasVM YashasVM commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • Secure the phone control channel with per-source pairing tokens, bounded request handling, and robust OBS control I/O.
  • Make public Android releases signed and checksum-verified; add pytest to CI.
  • Polish Android controls for accessibility, clarity, and safer live operation.
  • Align protocol and release documentation with the implemented behavior.

Validation

  • python -m pytest -q (30 passed)
  • :app:compileDebugKotlin with Android Studio JBR 21
  • git diff --check

@codex Please review the control-token pairing flow, release-update verification, and Android UI/lifecycle integration.

Summary by CodeRabbit

  • New Features

    • Added Android update integrity checks with checksum verification before installation.
    • Introduced authenticated camera control pairing using a shared token.
    • Added a confirmation dialog before stopping a stream.
    • Improved accessibility and touch targets across the Android app.
  • Bug Fixes

    • Android releases now consistently use signed release builds and include checksum assets.
    • The main screen now resizes properly when the keyboard opens.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7029eef3-a4d0-486e-9289-ddfcb191f407

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/harden-openstream

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f70aa26791

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +170 to +171
val expectedToken = authTokenProvider()?.takeIf(::isValidAuthToken) ?: return false
val supplied = suppliedToken?.takeIf(::isValidAuthToken) ?: return false

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow reserve before requiring a cached token

When OBS chooses a discovered phone from the source properties, the phone has only advertised itself and has not opened an OBS pairing URL, so MainActivity.controlAuthToken is still null. This early return false makes the very first authenticated /reserve return 401, which breaks the documented OBS-selected-phone pairing path unless the user first taps the slot on the phone.

Useful? React with 👍 / 👎.

Comment thread obs-plugin/src/openstream-source.cpp Outdated
Comment on lines +846 to +850
while (response.size() < 8192) {
const int received = recv(sock, buffer, sizeof(buffer), 0);
if (received <= 0) break;
response.append(buffer, static_cast<size_t>(received));
if (response.find("\r\n\r\n") != std::string::npos) break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Read the response body before trusting reserve success

For a busy phone, /reserve returns HTTP 200 with {"ok":false} in the body, but this loop stops as soon as it sees the header terminator. If TCP delivers headers separately from the body, send_control_command() never sees "ok":false and OBS treats the rejected reservation as successful, then tries to connect to a phone that did not accept the slot.

Useful? React with 👍 / 👎.

Comment on lines +62 to +64
fun dispose() {
unregister()
executor.shutdownNow()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cancel delayed update checks before shutting down

MainActivity posts a delayed appUpdater.checkForUpdates() during onCreate and never removes it. If the activity is destroyed within that delay, dispose() shuts the executor down here; when the pending UI-thread runnable later calls checkForUpdates(), executor.execute throws RejectedExecutionException, so quickly leaving the app can crash it.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
android/app/src/main/res/layout/activity_main.xml (1)

193-203: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider using @dimen/os_control_btn_size for port button height.

btnPortDown and btnPortUp use hardcoded 48dp for both width and height, while all other converted buttons use @dimen/os_control_btn_size for height. Using the dimension resource for the height keeps touch-target sizing centralized if the value changes in the future.

♻️ Optional refactor for port button height
             <Button
                 android:id="@+id/btnPortDown"
                 android:layout_width="48dp"
-                android:layout_height="48dp"
+                android:layout_height="`@dimen/os_control_btn_size`"
                 android:gravity="center"
             <Button
                 android:id="@+id/btnPortUp"
                 android:layout_width="48dp"
-                android:layout_height="48dp"
+                android:layout_height="`@dimen/os_control_btn_size`"
                 android:gravity="center"

Also applies to: 219-229

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/app/src/main/res/layout/activity_main.xml` around lines 193 - 203,
Update the port control buttons in the activity_main layout so their height uses
the shared dimension resource instead of a hardcoded value. In the btnPortDown
and btnPortUp button definitions, replace the fixed 48dp height with
`@dimen/os_control_btn_size` to keep sizing consistent with the other converted
controls. Keep the existing width and other attributes unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/android.yml:
- Around line 18-19: The checkout step in the Android workflow should disable
persisted Git credentials to match the hardened setup used elsewhere. Update the
actions/checkout usage in the test job to set persist-credentials to false,
keeping the job’s pytest-only behavior intact while preventing unnecessary token
persistence.

In @.github/workflows/release.yml:
- Around line 77-89: The Android APK normalization step is vulnerable because
VERSION_NAME is interpolated directly into the shell script from
inputs.tag/github.ref_name, allowing command substitution. Move the tag/ref
value into the step’s env block and read it from a shell variable inside the
script, keeping the JSON generation logic in the Normalize Android APK artifact
step unchanged apart from sourcing VERSION_NAME safely.

In `@android/app/src/main/java/dev/openstream/app/update/AppUpdater.kt`:
- Around line 206-223: `hasExpectedApkDigest()` is doing full APK SHA-256 work
synchronously on the main thread, which can block UI during
`installDownloadedApk()`. Move the digest computation off the UI thread by
running the `MessageDigest`/stream read logic on `executor`, then post the
result back to the activity thread before continuing the install or failure
path. Keep the fix localized around `hasExpectedApkDigest()`,
`installDownloadedApk()`, `downloadReceiver.onReceive()`, and
`resumePendingInstallIfAllowed()`.

In `@docs/protocol.md`:
- Around line 24-26: The fenced protocol examples in the docs are missing
language tags, which triggers markdownlint warnings and inconsistent rendering.
Update the unlabeled code fences in the protocol examples to use a plain text
fence, and make the same change for each matching example in the protocol
documentation so the snippets remain readable without implying a programming
language.

---

Nitpick comments:
In `@android/app/src/main/res/layout/activity_main.xml`:
- Around line 193-203: Update the port control buttons in the activity_main
layout so their height uses the shared dimension resource instead of a hardcoded
value. In the btnPortDown and btnPortUp button definitions, replace the fixed
48dp height with `@dimen/os_control_btn_size` to keep sizing consistent with the
other converted controls. Keep the existing width and other attributes
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e0a96c5-3f6e-4516-b96e-b06e4fc09d0f

📥 Commits

Reviewing files that changed from the base of the PR and between 60711f8 and f70aa26.

📒 Files selected for processing (16)
  • .github/workflows/android.yml
  • .github/workflows/release.yml
  • android/app/src/main/AndroidManifest.xml
  • android/app/src/main/java/dev/openstream/app/MainActivity.kt
  • android/app/src/main/java/dev/openstream/app/control/CameraControlServer.kt
  • android/app/src/main/java/dev/openstream/app/update/AppUpdater.kt
  • android/app/src/main/res/layout/activity_main.xml
  • android/app/src/main/res/values/colors.xml
  • android/app/src/main/res/values/dimens.xml
  • android/app/src/main/res/values/strings.xml
  • docs/protocol.md
  • docs/release-notes-template.md
  • docs/release.md
  • obs-plugin/src/openstream-source.cpp
  • tests/test_production_contracts.py
  • tests/test_repo_contract.py

Comment on lines +18 to +19
- name: Check out repository
uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Set persist-credentials: false on this checkout.

The publish-android-update job already hardens its checkout (Line 99), but this test job leaves the default credential persistence enabled, which zizmor flags (artipacked). Since the job only runs pytest, disabling credential persistence is safe.

🔒 Proposed hardening
       - name: Check out repository
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Check out repository
uses: actions/checkout@v4
- name: Check out repository
uses: actions/checkout@v4
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 18-19: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/android.yml around lines 18 - 19, The checkout step in the
Android workflow should disable persisted Git credentials to match the hardened
setup used elsewhere. Update the actions/checkout usage in the test job to set
persist-credentials to false, keeping the job’s pytest-only behavior intact
while preventing unnecessary token persistence.

Source: Linters/SAST tools

Comment on lines 77 to 89
- name: Normalize Android APK artifact
run: |
mkdir -p dist
if [ -f android/app/build/outputs/apk/release/app-release.apk ]; then
cp android/app/build/outputs/apk/release/app-release.apk dist/openstream-android.apk
printf 'signed-release\n' > dist/android-signing.txt
else
cp android/app/build/outputs/apk/debug/app-debug.apk dist/openstream-android.apk
printf 'debug-signed-beta\n' > dist/android-signing.txt
fi
cp android/app/build/outputs/apk/release/app-release.apk dist/openstream-android.apk
sha256sum dist/openstream-android.apk > dist/openstream-android.apk.sha256
VERSION_NAME="${{ inputs.tag || github.ref_name }}"
VERSION_NAME="${VERSION_NAME#v}"
printf '{\n "versionName": "%s",\n "versionCode": %s,\n "apkAsset": "openstream-android.apk"\n}\n' \
APK_SHA256="$(cut -d ' ' -f 1 dist/openstream-android.apk.sha256)"
printf '{\n "versionName": "%s",\n "versionCode": %s,\n "apkAsset": "openstream-android.apk",\n "apkSha256": "%s"\n}\n' \
"$VERSION_NAME" \
"${{ steps.version.outputs.code }}" \
"$APK_SHA256" \
> dist/openstream-android-update.json

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant workflow section with line numbers.
file=".github/workflows/release.yml"
if [ -f "$file" ]; then
  nl -ba "$file" | sed -n '70,95p'
else
  echo "Missing file: $file"
fi

# Show surrounding references to inputs.tag and github.ref_name in the workflow.
rg -n 'inputs\.tag|github\.ref_name|steps\.version\.outputs\.code' .github/workflows/release.yml

Repository: YashasVM/OpenStream

Length of output: 197


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=".github/workflows/release.yml"

echo '--- lines 70-95 ---'
sed -n '70,95p' "$file" | cat -n

echo
echo '--- occurrences ---'
rg -n 'inputs\.tag|github\.ref_name|steps\.version\.outputs\.code' "$file"

Repository: YashasVM/OpenStream

Length of output: 1975


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=".github/workflows/release.yml"

echo '--- lines 1-80 ---'
sed -n '1,80p' "$file" | cat -n

echo
echo '--- workflow triggers and inputs ---'
rg -n 'on:|workflow_dispatch|workflow_call|inputs:|tag:' "$file"

Repository: YashasVM/OpenStream

Length of output: 3911


Template-injection risk in VERSION_NAME.
inputs.tag is a workflow_dispatch string input, and github.ref_name is expanded into the shell script before execution. A value containing $(...) would execute here. Pass it through env: and read it from a shell variable instead.

🧰 Tools
🪛 zizmor (1.26.1)

[error] 82-82: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[error] 82-82: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)


[info] 87-87: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 77 - 89, The Android APK
normalization step is vulnerable because VERSION_NAME is interpolated directly
into the shell script from inputs.tag/github.ref_name, allowing command
substitution. Move the tag/ref value into the step’s env block and read it from
a shell variable inside the script, keeping the JSON generation logic in the
Normalize Android APK artifact step unchanged apart from sourcing VERSION_NAME
safely.

Source: Linters/SAST tools

Comment on lines +206 to +223
private fun hasExpectedApkDigest(): Boolean {
val expected = pendingRelease?.apkSha256 ?: return false
val apkUri = downloadManager.getUriForDownloadedFile(pendingDownloadId) ?: return false
val actual = runCatching {
activity.contentResolver.openInputStream(apkUri)?.use { input ->
val digest = MessageDigest.getInstance("SHA-256")
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
while (true) {
val read = input.read(buffer)
if (read < 0) break
digest.update(buffer, 0, read)
}
digest.digest().joinToString("") { byte -> "%02x".format(byte) }
}
}.getOrNull()
return actual != null && actual.equals(expected, ignoreCase = true)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm installDownloadedApk call sites all run on the main thread.
rg -nP -C3 'installDownloadedApk\s*\(' android/app/src/main/java/dev/openstream/app/update/AppUpdater.kt

Repository: YashasVM/OpenStream

Length of output: 851


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=android/app/src/main/java/dev/openstream/app/update/AppUpdater.kt

# Map the file structure first, then inspect the relevant sections.
ast-grep outline "$file" --view expanded

printf '\n--- relevant lines ---\n'
sed -n '1,260p' "$file" | nl -ba | sed -n '1,260p'

printf '\n--- executor / threading references ---\n'
rg -n 'executor|Handler|Looper|runOnUiThread|Coroutine|Dispatchers|onReceive|onResume|registerReceiver|installDownloadedApk|hasExpectedApkDigest' "$file"

Repository: YashasVM/OpenStream

Length of output: 1530


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=android/app/src/main/java/dev/openstream/app/update/AppUpdater.kt

printf '%s\n' '--- install/download flow ---'
sed -n '31,110p' "$file" | cat -n

printf '%s\n' '--- install + digest implementation ---'
sed -n '146,230p' "$file" | cat -n

printf '%s\n' '--- registration / receiver setup ---'
sed -n '1,70p' "$file" | cat -n

Repository: YashasVM/OpenStream

Length of output: 10985


Move APK digest hashing off the UI thread. installDownloadedApk() is reached from downloadReceiver.onReceive() and resumePendingInstallIfAllowed(), both on the main thread here. Hashing a full APK synchronously in hasExpectedApkDigest() can block the UI long enough to trigger an ANR; compute the digest on executor and post the install/failure path back to the activity thread.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/app/src/main/java/dev/openstream/app/update/AppUpdater.kt` around
lines 206 - 223, `hasExpectedApkDigest()` is doing full APK SHA-256 work
synchronously on the main thread, which can block UI during
`installDownloadedApk()`. Move the digest computation off the UI thread by
running the `MessageDigest`/stream read logic on `executor`, then post the
result back to the activity thread before continuing the install or failure
path. Keep the fix localized around `hasExpectedApkDigest()`,
`installDownloadedApk()`, `downloadReceiver.onReceive()`, and
`resumePendingInstallIfAllowed()`.

Comment thread docs/protocol.md Outdated
Comment on lines 24 to 26
```
srt://<obs-pc-ip>:9000?mode=caller&latency=120
srt://<phone-ip>:9000?mode=caller&latency=120
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language tag to these fenced examples.

The unlabeled fences trigger the markdownlint warning and render less consistently. text is enough here.

Suggested fix
-```
+```text
 srt://<phone-ip>:9000?mode=caller&latency=120

@@
- +text
srt://0.0.0.0:9000?mode=listener&latency=120

@@
-```
+```text
openstream://connect?slotId=<slot-id>&slotLabel=<slot-label>&sourceInstanceId=<source-id>&controlToken=<64-hex-token>&host=<obs-ip>&port=<port>&latency=<ms>&name=...
</details>


Also applies to: 30-32, 136-138

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.22.1)</summary>

[warning] 24-24: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @docs/protocol.md around lines 24 - 26, The fenced protocol examples in the
docs are missing language tags, which triggers markdownlint warnings and
inconsistent rendering. Update the unlabeled code fences in the protocol
examples to use a plain text fence, and make the same change for each matching
example in the protocol documentation so the snippets remain readable without
implying a programming language.


</details>

<!-- fingerprinting:phantom:triton:quartz -->

<!-- cr-indicator-types:potential_issue -->

<!-- cr-comment:v1:e6428657747f766e1faae8a1 -->

_Source: Linters/SAST tools_

<!-- This is an auto-generated comment by CodeRabbit -->

YashasVM added 2 commits July 10, 2026 01:42
# Conflicts:
#	android/app/src/main/java/dev/openstream/app/MainActivity.kt
#	android/app/src/main/res/layout/activity_main.xml
#	android/app/src/main/res/values/colors.xml
#	android/app/src/main/res/values/dimens.xml
#	android/app/src/main/res/values/strings.xml
@YashasVM

Copy link
Copy Markdown
Owner Author

Closing as superseded by merged PR #6. PR #5 is a draft, has a dirty merge state, and its Android build check failed; its intended stabilization/release work is covered by #6.

@YashasVM YashasVM closed this Jul 10, 2026
@YashasVM YashasVM deleted the codex/harden-openstream branch July 10, 2026 09:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant