mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-14 08:15:02 -05:00
[airplay] Add WWW authentication for Airplay 2
Some speakers (e.g. Homepod) that are configured to require password want that this is used both for pairing authentication and classic WWW Authentication. This commit adds the WWW Authentication that is also used for Airplay 1. Closes #1545.
This commit is contained in:
parent
62c72d72d6
commit
e437e41870
@ -650,11 +650,220 @@ chacha_encrypt(uint8_t *cipher, uint8_t *plain, size_t plain_len, const void *ad
|
||||
|
||||
/* --------------------- Helpers for sending RTSP requests ------------------ */
|
||||
|
||||
static int
|
||||
auth_header_add(struct evrtsp_request *req, const char *password, const char *realm, const char *nonce, const char *method, const char *uri)
|
||||
{
|
||||
const char *hash_fmt = "%02x";
|
||||
const char *username = "";
|
||||
char ha1[33] = { 0 };
|
||||
char ha2[33] = { 0 };
|
||||
char ebuf[64];
|
||||
char auth[256];
|
||||
uint8_t *hash_bytes;
|
||||
size_t hashlen;
|
||||
gcry_md_hd_t hd;
|
||||
gpg_error_t gc_err;
|
||||
int i;
|
||||
int ret;
|
||||
|
||||
if (!password)
|
||||
password = "";
|
||||
|
||||
gc_err = gcry_md_open(&hd, GCRY_MD_MD5, 0);
|
||||
if (gc_err != GPG_ERR_NO_ERROR)
|
||||
{
|
||||
gpg_strerror_r(gc_err, ebuf, sizeof(ebuf));
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Could not open MD5: %s\n", ebuf);
|
||||
return -1;
|
||||
}
|
||||
|
||||
hashlen = gcry_md_get_algo_dlen(GCRY_MD_MD5);
|
||||
|
||||
/* HA 1 */
|
||||
gcry_md_write(hd, username, strlen(username));
|
||||
gcry_md_write(hd, ":", 1);
|
||||
gcry_md_write(hd, realm, strlen(realm));
|
||||
gcry_md_write(hd, ":", 1);
|
||||
gcry_md_write(hd, password, strlen(password));
|
||||
|
||||
hash_bytes = gcry_md_read(hd, GCRY_MD_MD5);
|
||||
if (!hash_bytes)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Could not read MD5 hash\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (i = 0; i < hashlen; i++)
|
||||
sprintf(ha1 + (2 * i), hash_fmt, hash_bytes[i]);
|
||||
|
||||
/* RESET */
|
||||
gcry_md_reset(hd);
|
||||
|
||||
/* HA 2 */
|
||||
gcry_md_write(hd, method, strlen(method));
|
||||
gcry_md_write(hd, ":", 1);
|
||||
gcry_md_write(hd, uri, strlen(uri));
|
||||
|
||||
hash_bytes = gcry_md_read(hd, GCRY_MD_MD5);
|
||||
if (!hash_bytes)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Could not read MD5 hash\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (i = 0; i < hashlen; i++)
|
||||
sprintf(ha2 + (2 * i), hash_fmt, hash_bytes[i]);
|
||||
|
||||
/* RESET */
|
||||
gcry_md_reset(hd);
|
||||
|
||||
/* Final value */
|
||||
gcry_md_write(hd, ha1, 32);
|
||||
gcry_md_write(hd, ":", 1);
|
||||
gcry_md_write(hd, nonce, strlen(nonce));
|
||||
gcry_md_write(hd, ":", 1);
|
||||
gcry_md_write(hd, ha2, 32);
|
||||
|
||||
hash_bytes = gcry_md_read(hd, GCRY_MD_MD5);
|
||||
if (!hash_bytes)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Could not read MD5 hash\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (i = 0; i < hashlen; i++)
|
||||
sprintf(ha1 + (2 * i), hash_fmt, hash_bytes[i]);
|
||||
|
||||
gcry_md_close(hd);
|
||||
|
||||
/* Build header */
|
||||
ret = snprintf(auth, sizeof(auth), "Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\"",
|
||||
username, realm, nonce, uri, ha1);
|
||||
if ((ret < 0) || (ret >= sizeof(auth)))
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Authorization value header exceeds buffer size\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
evrtsp_add_header(req->output_headers, "Authorization", auth);
|
||||
|
||||
DPRINTF(E_SPAM, L_AIRPLAY, "Authorization header: %s\n", auth);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
auth_header_parse(struct airplay_session *rs, struct evrtsp_request *req)
|
||||
{
|
||||
const char *param;
|
||||
char *auth = NULL;
|
||||
char *token;
|
||||
char *ptr;
|
||||
|
||||
if (rs->realm)
|
||||
{
|
||||
free(rs->realm);
|
||||
rs->realm = NULL;
|
||||
}
|
||||
|
||||
if (rs->nonce)
|
||||
{
|
||||
free(rs->nonce);
|
||||
rs->nonce = NULL;
|
||||
}
|
||||
|
||||
param = evrtsp_find_header(req->input_headers, "WWW-Authenticate");
|
||||
if (!param)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "WWW-Authenticate header not found\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
DPRINTF(E_DBG, L_AIRPLAY, "WWW-Authenticate: %s\n", param);
|
||||
|
||||
if (strncmp(param, "Digest ", strlen("Digest ")) != 0)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Unsupported authentication method: %s\n", param);
|
||||
goto error;
|
||||
}
|
||||
|
||||
auth = strdup(param);
|
||||
if (!auth)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Out of memory for WWW-Authenticate header copy\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
token = strchr(auth, ' ');
|
||||
if (!token)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Unexpected WWW-Authenticate auth\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
token++;
|
||||
|
||||
token = strtok_r(token, " =", &ptr);
|
||||
while (token)
|
||||
{
|
||||
if (strcmp(token, "realm") == 0)
|
||||
{
|
||||
token = strtok_r(NULL, "=\"", &ptr);
|
||||
if (!token)
|
||||
break;
|
||||
|
||||
rs->realm = strdup(token);
|
||||
}
|
||||
else if (strcmp(token, "nonce") == 0)
|
||||
{
|
||||
token = strtok_r(NULL, "=\"", &ptr);
|
||||
if (!token)
|
||||
break;
|
||||
|
||||
rs->nonce = strdup(token);
|
||||
}
|
||||
|
||||
token = strtok_r(NULL, " =", &ptr);
|
||||
}
|
||||
|
||||
if (!rs->realm || !rs->nonce)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Could not find realm/nonce in WWW-Authenticate header\n");
|
||||
|
||||
if (rs->realm)
|
||||
{
|
||||
free(rs->realm);
|
||||
rs->realm = NULL;
|
||||
}
|
||||
|
||||
if (rs->nonce)
|
||||
{
|
||||
free(rs->nonce);
|
||||
rs->nonce = NULL;
|
||||
}
|
||||
|
||||
goto error;
|
||||
}
|
||||
|
||||
DPRINTF(E_SPAM, L_AIRPLAY, "Found realm: [%s], nonce: [%s]\n", rs->realm, rs->nonce);
|
||||
|
||||
free(auth);
|
||||
return 0;
|
||||
|
||||
error:
|
||||
free(auth);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
request_headers_add(struct evrtsp_request *req, struct airplay_session *rs, enum evrtsp_cmd_type req_method)
|
||||
{
|
||||
char buf[64];
|
||||
const char *user_agent;
|
||||
const char *method;
|
||||
const char *url;
|
||||
int ret;
|
||||
|
||||
snprintf(buf, sizeof(buf), "%d", rs->cseq);
|
||||
evrtsp_add_header(req->output_headers, "CSeq", buf);
|
||||
@ -664,6 +873,20 @@ request_headers_add(struct evrtsp_request *req, struct airplay_session *rs, enum
|
||||
user_agent = cfg_getstr(cfg_getsec(cfg, "general"), "user_agent");
|
||||
evrtsp_add_header(req->output_headers, "User-Agent", user_agent);
|
||||
|
||||
// If we have a realm + nonce it means that the device told us in the reply to
|
||||
// SETUP that www authentication with password is required
|
||||
if (rs->realm && rs->nonce)
|
||||
{
|
||||
method = evrtsp_method(req_method);
|
||||
url = (req_method == EVRTSP_REQ_OPTIONS) ? "*" : rs->session_url;
|
||||
|
||||
ret = auth_header_add(req, rs->password, rs->realm, rs->nonce, method, url);
|
||||
if (ret < 0)
|
||||
return -1;
|
||||
|
||||
rs->req_has_auth = 1;
|
||||
}
|
||||
|
||||
snprintf(buf, sizeof(buf), "%" PRIX64, libhash);
|
||||
evrtsp_add_header(req->output_headers, "Client-Instance", buf);
|
||||
evrtsp_add_header(req->output_headers, "DACP-ID", buf);
|
||||
@ -2714,6 +2937,28 @@ response_handler_setup_session(struct evrtsp_request *req, struct airplay_sessio
|
||||
uint64_t uintval;
|
||||
int ret;
|
||||
|
||||
if (req->response_code == RTSP_UNAUTHORIZED)
|
||||
{
|
||||
if (rs->req_has_auth)
|
||||
{
|
||||
DPRINTF(E_LOG, L_AIRPLAY, "Bad or missing password for device '%s' (%s)\n", rs->devname, rs->address);
|
||||
return AIRPLAY_SEQ_ABORT;
|
||||
}
|
||||
|
||||
// We haven't tried authenticating yet, so save realm and nonce from the
|
||||
// received WWW-Authenticate header and trigger a re-run with auth header
|
||||
ret = auth_header_parse(rs, req);
|
||||
if (ret < 0)
|
||||
return AIRPLAY_SEQ_ABORT;
|
||||
|
||||
return AIRPLAY_SEQ_START_PLAYBACK;
|
||||
}
|
||||
else if (req->response_code != RTSP_OK)
|
||||
{
|
||||
DPRINTF(E_WARN, L_AIRPLAY, "Unexpected reply to SETUP (session) from '%s'\n", rs->devname);
|
||||
return AIRPLAY_SEQ_ABORT;
|
||||
}
|
||||
|
||||
ret = wplist_from_evbuf(&response, req->input_buffer);
|
||||
if (ret < 0)
|
||||
{
|
||||
@ -3147,7 +3392,9 @@ static struct airplay_seq_request airplay_seq_request[][7] =
|
||||
#if AIRPLAY_USE_AUTH_SETUP
|
||||
{ AIRPLAY_SEQ_START_PLAYBACK, "auth-setup", EVRTSP_REQ_POST, payload_make_auth_setup, NULL, "application/octet-stream", "/auth-setup", true },
|
||||
#endif
|
||||
{ AIRPLAY_SEQ_START_PLAYBACK, "SETUP (session)", EVRTSP_REQ_SETUP, payload_make_setup_session, response_handler_setup_session, "application/x-apple-binary-plist", NULL, false },
|
||||
// proceed_on_rtsp_not_ok is true because a device may reply with 401 Unauthorized
|
||||
// and a WWW-Authenticate header, and then we may need re-run with password auth
|
||||
{ AIRPLAY_SEQ_START_PLAYBACK, "SETUP (session)", EVRTSP_REQ_SETUP, payload_make_setup_session, response_handler_setup_session, "application/x-apple-binary-plist", NULL, true },
|
||||
{ AIRPLAY_SEQ_START_PLAYBACK, "SETPEERS", EVRTSP_REQ_SETPEERS, payload_make_setpeers, NULL, "/peer-list-changed", NULL, false },
|
||||
{ AIRPLAY_SEQ_START_PLAYBACK, "SETUP (stream)", EVRTSP_REQ_SETUP, payload_make_setup_stream, response_handler_setup_stream, "application/x-apple-binary-plist", NULL, false },
|
||||
{ AIRPLAY_SEQ_START_PLAYBACK, "RECORD", EVRTSP_REQ_RECORD, payload_make_record, response_handler_record, NULL, NULL, false },
|
||||
|
Loading…
Reference in New Issue
Block a user