Skip to content

Add Support For Password-Protected Receivers - #26

Merged
omarroth merged 4 commits into
omarroth:mainfrom
jamescoman:push-yzzksuzywvmy
Aug 6, 2026
Merged

Add Support For Password-Protected Receivers#26
omarroth merged 4 commits into
omarroth:mainfrom
jamescoman:push-yzzksuzywvmy

Conversation

@jamescoman

@jamescoman jamescoman commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Adds support for receivers with Require Password enabled (on an Apple TV:
Settings → AirPlay and HomeKit). Such a receiver challenges the mirroring
SETUP with HTTP Digest auth; nothing answered it, so mirroring failed with
HTTP 401 and there was no way past it.

DOUBLETAKE_CODE='...' doubletake -target 192.168.1.77

Commits

  1. Add AppleTV14,1 without Homepod to tested devices — the configuration
    this was developed against.
  2. Fix PIN prompt for receivers that use a fixed password — the prompt
    assumed an onscreen 4-digit code, and fmt.Scanln truncated anything with a
    space in it.
  3. airplay: log the SETUP plist under -debug — dumps the SETUP descriptor
    in a stable key order so two runs can be diffed. This is what made the rest
    tractable.
  4. airplay: answer HTTP Digest challenges from password-protected receivers
    — the fix itself.

Each builds and tests independently.

Notes on the implementation

The challenge carries no qop or algorithm, so this is RFC 2069 digest:
MD5(HA1:nonce:HA2). The retry lives inside rtspRequest/httpRequest rather
than at the ~24 call sites, so every request is covered uniformly and the
mirroring code is untouched by this PR. The nonce is cached on first sight, so
only the first request on a connection pays for a retry — a receiver that
challenges one request challenges them all.

The Digest username is fixed at AirPlay, matching the realm="airplay" that
mirroring receivers advertise. It is deliberately not configurable: the
challenge never carries a username and it is folded into HA1, so a wrong guess
is indistinguishable from a wrong password, and an option there would only
offer a second way to produce the same 401.

-pin is renamed to -code rather than adding a second flag beside it. The
two carry the same thing from the user's point of view — whatever the receiver
is asking for, an onscreen PIN or a configured password — and the receiver, not
the user, decides which it wants. This PR adds no net new flags.
$DOUBLETAKE_CODE is preferred and takes precedence, since a command line is
visible to other users in ps and lands in shell history.

One behaviour change worth flagging: a code no longer forces a full re-pair the
way -pin did. A password is needed on every session, so re-pairing each run
would discard working credentials. -pair still requests one explicitly.

Verification

End to end against a password-protected AppleTV14,1 / tvOS 26.5
(AirTunes/950.7.1): full mirroring session, 13× 200, a single 401 (the expected
first challenge), no warnings. realm="airplay", username AirPlay, uri as
the absolute request URI — a path-only uri is rejected with another 401.

Unit tests cover challenge parsing (including a real receiver nonce), the
digest computation against an independently-derived vector, the retry decision
table, and end-to-end cases against a fake receiver: a challenged request is
retried with credentials, a request with no password configured is not retried
at all, and later requests reuse the cached challenge instead of being
challenged again.

Dropped after review

An earlier revision also advertised timingProtocol=PTP for pair-verified
sessions, on the theory that receivers reject the legacy NTP clock with a bare
400. @omarroth found the resulting stream stalled after the first frame, and
on re-testing I could not reproduce the 400 at all — the same receiver now
completes SETUP on NTP in 133 ms and streams cleanly. That commit is gone and
timingProtocol is back to plain NTP.

Receivers with "Require Password" enabled use a password you set rather than
a rotating onscreen code, so nothing appears on the TV and the old prompt sent
you looking for one. A failed pair-pin-start is likewise normal for those
receivers, so warn instead of exiting.

Also read the whole line: fmt.Scanln stops at the first space and silently
truncated any password containing one.
A receiver that rejects SETUP answers with a bare status and no body, so
there is nothing to go on without seeing what was actually sent. Dumps both
the setup plist and the audio stream descriptor in a stable key order, with
byte slices summarised, so two runs can be diffed.
@omarroth

omarroth commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Thanks for this! Password option appears to work great.

It doesn't appear the PTP path is implemented correctly. Testing locally I'm seeing the stream stop after the first correct broadcast frame.

Couple other minor things:

  • I would prefer to reduce the number of new options being added. Since -pin and -password are functionally identical to the user I would prefer to instead have a single -code or similar option instead of specifying both.
  • I don't think I understand the purpose of -auth-user, is this for streaming audio only to HomePods, etc? In that case there are already other tools for doing that on Linux and I would prefer the option be removed in favor of realm="airplay".

A receiver with "Require Password" enabled challenges SETUP with 401 and a
WWW-Authenticate Digest header, which nothing answered, so mirroring to it was
impossible.

The challenge carries no qop or algorithm, so this is RFC 2069 digest:
MD5(HA1:nonce:HA2). The retry lives inside rtspRequest/httpRequest rather than
at the 24 call sites, so every request is covered uniformly and the mirroring
code is untouched. The nonce is cached on first sight, so only the first
request on a connection pays for a retry: a receiver that challenges one
request challenges them all.

The Digest username is fixed at "AirPlay", matching the realm="airplay" that
mirroring receivers advertise. It is deliberately not configurable: the
challenge never carries a username and it is folded into HA1, so a wrong guess
is indistinguishable from a wrong password -- an option there would only offer
a second way to produce the same 401.

Renames -pin to -code rather than adding a second flag beside it. The two carry
the same thing from the user's point of view -- whatever the receiver is asking
for, an onscreen PIN or a configured password -- and the receiver, not the
user, decides which one it wants. $DOUBLETAKE_CODE is preferred and takes
precedence, since a command line is visible to other users in ps and lands in
shell history.

A code no longer implies pairing. -pin forced a full pair-setup, which is wrong
for a password: that is needed on every session, so re-pairing each run would
discard working credentials. -pair still asks for one explicitly.

Verified against AppleTV14,1 / tvOS 26.5 (AirTunes/950.7.1): realm="airplay",
username "AirPlay", uri as the absolute request URI. Full password-protected
mirroring session, 14x 200.
@jamescoman jamescoman changed the title Add Support For Password-Protected Receivers, Supporting Bug Fixes Add Support For Password-Protected Receivers Aug 5, 2026
@jamescoman

Copy link
Copy Markdown
Contributor Author

Thanks for taking a look!

I thought the PTP issue was a result of differences in our testing hardware,
but I couldn't reproduce it locally anymore. I think I got the AppleTV in a
weird state with a lot of testing while drafting this initially. PTP is gone
now.

Now that this is back to just adding password support, there's no net change in
flags:

  • -pin and -password are merged to -code. Only -pair forces a re-pair every
    session though. DOUBLETAKE_CODE takes precedence over the flag.
  • -auth-user is gone. I was never clear on where a good line was for faking PTP
    for old devices, but that's gone and this can leave with it. Apparently older
    devices used a different user and that would have been relevant for PTP.
  • timing-protocol is gone with PTP

@omarroth
omarroth merged commit d2e40af into omarroth:main Aug 6, 2026
3 checks passed
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.

2 participants