GET /info

This commit is contained in:
Christian Glombek 2023-08-05 06:37:12 +02:00
parent cf2aac7039
commit b4b3a53384
4 changed files with 428 additions and 35 deletions

View file

@ -275,6 +275,7 @@ summary({'dbus (Bluetooth, rt, portal, pw-reserve)': dbus_dep.found()}, bool_yn:
cdata.set('HAVE_DBUS', dbus_dep.found()) cdata.set('HAVE_DBUS', dbus_dep.found())
sdl_dep = dependency('sdl2', required : get_option('sdl2')) sdl_dep = dependency('sdl2', required : get_option('sdl2'))
summary({'SDL2 (video examples)': sdl_dep.found()}, bool_yn: true, section: 'Misc dependencies') summary({'SDL2 (video examples)': sdl_dep.found()}, bool_yn: true, section: 'Misc dependencies')
plist_lib = dependency('libplist-2.0', required: get_option('raop'))
drm_dep = dependency('libdrm', required : false) drm_dep = dependency('libdrm', required : false)
readline_dep = dependency('readline', required : get_option('readline')) readline_dep = dependency('readline', required : get_option('readline'))

View file

@ -612,7 +612,7 @@ if build_module_raop
install : true, install : true,
install_dir : modules_install_dir, install_dir : modules_install_dir,
install_rpath: modules_install_dir, install_rpath: modules_install_dir,
dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, openssl_lib], dependencies : [mathlib, dl_lib, rt_lib, pipewire_dep, openssl_lib, plist_lib],
) )
endif endif
summary({'raop-sink (requires OpenSSL)': build_module_raop}, bool_yn: true, section: 'Optional Modules') summary({'raop-sink (requires OpenSSL)': build_module_raop}, bool_yn: true, section: 'Optional Modules')

View file

@ -6,6 +6,7 @@
#include <assert.h> #include <assert.h>
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
#include <stdbool.h>
#include <errno.h> #include <errno.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/socket.h> #include <sys/socket.h>
@ -18,6 +19,7 @@
#include <math.h> #include <math.h>
#include <arpa/inet.h> #include <arpa/inet.h>
#include <netinet/in.h> #include <netinet/in.h>
#include <plist/plist.h>
#include <openssl/err.h> #include <openssl/err.h>
#if OPENSSL_API_LEVEL >= 30000 #if OPENSSL_API_LEVEL >= 30000
@ -29,6 +31,7 @@
#include <openssl/aes.h> #include <openssl/aes.h>
#include <openssl/md5.h> #include <openssl/md5.h>
#include <openssl/evp.h> #include <openssl/evp.h>
#include <openssl/kdf.h>
#include "config.h" #include "config.h"
@ -145,12 +148,18 @@ PW_LOG_TOPIC_STATIC(mod_topic, "mod." NAME);
#define SHA512_DIGEST_LENGTH 64 #define SHA512_DIGEST_LENGTH 64
#endif #endif
#define DEFAULT_USER_AGENT "iTunes/11.0.4 (Windows; N)" #define DEFAULT_USER_NAME "PipeWire"
#define DEFAULT_USER_NAME "iTunes"
#define AP_USER_AGENT "AirPlay/381.13"
#define AP_USER_NAME "AirPlay"
#define AP_REQUEST_BUFSIZE 4096
#define AP_SRP_USER_NAME "Pair-Setup" #define AP_SRP_USER_NAME "Pair-Setup"
#define AP_CONTROL_SALT "Control-Salt"
#define AP_CONTROL_ENC_INFO "Control-Write-Encryption-Key"
#define AP_CONTROL_DEC_INFO "Control-Read-Encryption-Key"
#define AP_NONCE_LENGTH 12
#define AP_AUTHTAG_LENGTH 16
#define AP_ENCRYPTED_BLOCK_LENGTH_MAX 1024
#define AP_REQUEST_BUFSIZE 4096
// For transient pairing the key_len will be 64 bytes, but only 32 are used for
// audio payload encryption. For normal pairing the key is 32 bytes.
#define AP_AUDIO_KEY_LEN 32
#define MAX_PORT_RETRY 128 #define MAX_PORT_RETRY 128
@ -261,6 +270,8 @@ struct impl {
uint8_t shared_secret[64]; uint8_t shared_secret[64];
size_t shared_secret_len; // Will be 32 (normal) or 64 (transient) size_t shared_secret_len; // Will be 32 (normal) or 64 (transient)
//struct pw_rtsp_cipher_context *control_cipher_ctx;
uint16_t control_port; uint16_t control_port;
int control_fd; int control_fd;
struct spa_source *control_source; struct spa_source *control_source;
@ -445,6 +456,9 @@ static int flush_to_udp_packet(struct impl *impl)
} }
if (impl->encryption == CRYPTO_RSA) if (impl->encryption == CRYPTO_RSA)
aes_encrypt(impl, dst, len); aes_encrypt(impl, dst, len);
// TODO lorbus
//else if (impl->encryption == CRYPTO_PAIR_TRANSIENT)
// rtp_ap_encrypt(impl, dst, len, impl->shared_secret, AP_AUDIO_KEY_LEN, NULL);
impl->rtptime += n_frames; impl->rtptime += n_frames;
impl->seq = (impl->seq + 1) & 0xffff; impl->seq = (impl->seq + 1) & 0xffff;
@ -1087,9 +1101,273 @@ static int rtsp_setup_reply(void *data, int status, const struct spa_dict *heade
return 0; return 0;
} }
static void wplist_dict_add_uint(plist_t node, const char *key, uint64_t val)
{
plist_t add = plist_new_uint(val);
plist_dict_set_item(node, key, add);
}
static void wplist_dict_add_string(plist_t node, const char *key, const char *val)
{
plist_t add = plist_new_string(val);
plist_dict_set_item(node, key, add);
}
static void wplist_dict_add_bool(plist_t node, const char *key, bool val)
{
plist_t add = plist_new_bool(val);
plist_dict_set_item(node, key, add);
}
static void wplist_dict_add_data(plist_t node, const char *key, uint8_t *data, size_t len)
{
plist_t add = plist_new_data((const char *)data, len);
plist_dict_set_item(node, key, add);
}
static int
wplist_to_bin(uint8_t **data, size_t *len, plist_t node)
{
char *out = NULL;
uint32_t out_len = 0;
plist_to_bin(node, &out, &out_len);
if (!out)
return -1;
*data = (uint8_t *)out;
*len = out_len;
return 0;
}
static int
wplist_from_bin(plist_t *node, const struct pw_array *buf)
{
plist_t out = NULL;
plist_from_bin((char *)buf->data, (uint32_t)buf->size, &out);
if (!out)
return -1;
*node = out;
return 0;
}
// Executes SHA512 RFC 5869 extract + expand, writing a derived key to okm
static int
hkdf_extract_expand(uint8_t *okm, size_t okm_len, const uint8_t *ikm, size_t ikm_len, const char *salt, const char *info)
{
EVP_PKEY_CTX *pctx;
if (okm_len > SHA512_DIGEST_LENGTH)
return -1;
if (! (pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL)))
return -1;
if (EVP_PKEY_derive_init(pctx) <= 0)
goto error;
if (EVP_PKEY_CTX_set_hkdf_md(pctx, EVP_sha512()) <= 0)
goto error;
if (EVP_PKEY_CTX_set1_hkdf_salt(pctx, (const unsigned char *)salt, strlen(salt)) <= 0)
goto error;
if (EVP_PKEY_CTX_set1_hkdf_key(pctx, ikm, ikm_len) <= 0)
goto error;
if (EVP_PKEY_CTX_add1_hkdf_info(pctx, (const unsigned char *)info, strlen(info)) <= 0)
goto error;
if (EVP_PKEY_derive(pctx, okm, &okm_len) <= 0)
goto error;
EVP_PKEY_CTX_free(pctx);
return 0;
error:
EVP_PKEY_CTX_free(pctx);
return -1;
}
/*
static pw_rtsp_cipher_context *ap_cipher_context_new(const uint8_t *shared_secret, size_t shared_secret_len, const char *enc_salt, const char *enc_info, const char *dec_salt, const char *dec_info)
{
struct pw_rtsp_cipher_context *cctx;
int ret;
cctx = calloc(1, sizeof(struct pw_rtsp_cipher_context));
if (!cctx)
goto error;
ret = hkdf_extract_expand(cctx->encryption_key, sizeof(cctx->encryption_key), shared_secret, shared_secret_len, enc_salt, enc_info);
if (ret < 0)
goto error;
ret = hkdf_extract_expand(cctx->decryption_key, sizeof(cctx->decryption_key), shared_secret, shared_secret_len, dec_salt, dec_info);
if (ret < 0)
goto error;
return cctx;
error:
free(cctx);
return NULL;
}
static int chacha20_poly1305_encrypt(uint8_t *cipher, const uint8_t *plain, size_t plain_len,
const uint8_t *key, size_t key_len, const void *ad, size_t ad_len,
uint8_t *tag, size_t tag_len, const uint8_t nonce[AP_NONCE_LENGTH]) {
EVP_CIPHER_CTX *ctx;
int len;
if (!(ctx = EVP_CIPHER_CTX_new()))
return -1;
if (EVP_EncryptInit_ex(ctx, EVP_chacha20_poly1305(), NULL, key, nonce) != 1)
goto error;
if (EVP_CIPHER_CTX_set_padding(ctx, 0) != 1) // Maybe not necessary
goto error;
if (ad_len > 0 && EVP_EncryptUpdate(ctx, NULL, &len, ad, ad_len) != 1)
goto error;
if (EVP_EncryptUpdate(ctx, cipher, &len, plain, plain_len) != 1)
goto error;
assert((size_t)len == plain_len);
if (EVP_EncryptFinal_ex(ctx, NULL, &len) != 1)
goto error;
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, tag_len, tag) != 1)
goto error;
EVP_CIPHER_CTX_free(ctx);
return 0;
error:
EVP_CIPHER_CTX_free(ctx);
return -1;
}
static ssize_t ap_encrypt(uint8_t **ciphertext, size_t *ciphertext_len,
const uint8_t *plaintext, size_t plaintext_len,
struct pw_rtsp_cipher_context *cctx)
{
uint8_t nonce[AP_NONCE_LENGTH] = { 0 };
uint8_t tag[AP_AUTHTAG_LENGTH];
const uint8_t *plain_block;
uint8_t *cipher_block;
uint16_t block_len;
int nblocks;
int ret;
int i;
if (plaintext_len == 0 || !plaintext)
return -1;
// Encryption is done in blocks, where each block consists of a short, the
// encrypted data and an auth tag. The short is the size of the encrypted
// data. The encrypted data in the block cannot exceed AP_ENCRYPTED_BLOCK_LENGTH_MAX == 1024.
nblocks = 1 + ((plaintext_len - 1) / AP_ENCRYPTED_BLOCK_LENGTH_MAX); // Ceiling of division
*ciphertext_len = nblocks * (sizeof(block_len) + AP_AUTHTAG_LENGTH) + plaintext_len;
*ciphertext = malloc(*ciphertext_len);
cctx->encryption_counter_prev = cctx->encryption_counter;
for (i = 0, plain_block = plaintext, cipher_block = *ciphertext; i < nblocks; i++) {
// If it is the last block we will encrypt only the remaining data
block_len = (i + 1 == nblocks) ? (plaintext + plaintext_len - plain_block) : AP_ENCRYPTED_BLOCK_LENGTH_MAX;
memcpy(nonce + 4, &(cctx->encryption_counter), sizeof(cctx->encryption_counter));// TODO BE or LE?
// Write the ciphered block
memcpy(cipher_block, &block_len, sizeof(block_len)); // TODO BE or LE?
ret = chacha20_poly1305_encrypt(cipher_block + sizeof(block_len),
plain_block, block_len, cctx->encryption_key, sizeof(cctx->encryption_key),
&block_len, sizeof(block_len), tag, sizeof(tag), nonce);
if (ret < 0) {
pw_log_error("Encryption with chacha poly1305 failed");
cctx->encryption_counter = cctx->encryption_counter_prev;
free(*ciphertext);
return -1;
}
memcpy(cipher_block + sizeof(block_len) + block_len, tag, AP_AUTHTAG_LENGTH);
plain_block += block_len;
cipher_block += block_len + sizeof(block_len) + AP_AUTHTAG_LENGTH;
cctx->encryption_counter++;
}
return plain_block - plaintext;
}
static int chacha20_poly1305_decrypt(uint8_t *plain, const uint8_t *cipher, size_t cipher_len,
const uint8_t *key, size_t key_len, const void *ad, size_t ad_len,
uint8_t *tag, size_t tag_len, const uint8_t nonce[AP_NONCE_LENGTH])
{
EVP_CIPHER_CTX *ctx;
int len;
if (! (ctx = EVP_CIPHER_CTX_new()))
return -1;
if (EVP_DecryptInit_ex(ctx, EVP_chacha20_poly1305(), NULL, key, nonce) != 1)
goto error;
if (EVP_CIPHER_CTX_set_padding(ctx, 0) != 1) // Maybe not necessary
goto error;
if (EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, tag_len, tag) != 1)
goto error;
if (ad_len > 0 && EVP_DecryptUpdate(ctx, NULL, &len, ad, ad_len) != 1)
goto error;
if (EVP_DecryptUpdate(ctx, plain, &len, cipher, cipher_len) != 1)
goto error;
if (EVP_DecryptFinal_ex(ctx, NULL, &len) != 1)
goto error;
EVP_CIPHER_CTX_free(ctx);
return 0;
error:
EVP_CIPHER_CTX_free(ctx);
return -1;
}
static ssize_t ap_decrypt(uint8_t **plaintext, size_t *plaintext_len,
const uint8_t *ciphertext, size_t ciphertext_len,
struct pw_rtsp_cipher_context *cctx)
{
uint8_t nonce[AP_NONCE_LENGTH] = { 0 };
uint8_t tag[AP_AUTHTAG_LENGTH];
uint8_t *plain_block;
const uint8_t *cipher_block;
uint16_t block_len;
int ret;
if (ciphertext_len < sizeof(block_len) || !ciphertext)
return -1;
// This will allocate more than we need. Since we don't know the number of
// blocks in the ciphertext yet we can't calculate the exact required length.
*plaintext = malloc(ciphertext_len);
cctx->decryption_counter_prev = cctx->decryption_counter;
for (plain_block = *plaintext, cipher_block = ciphertext; cipher_block < ciphertext + ciphertext_len; ) {
memcpy(&block_len, cipher_block, sizeof(block_len)); // TODO BE or LE?
if (cipher_block + block_len + sizeof(block_len) + AP_AUTHTAG_LENGTH > ciphertext + ciphertext_len) {
// The remaining ciphertext doesn't contain an entire block, so stop
break;
}
memcpy(tag, cipher_block + sizeof(block_len) + block_len, sizeof(tag));
memcpy(nonce + 4, &(cctx->decryption_counter), sizeof(cctx->decryption_counter));// TODO BE or LE?
ret = chacha20_poly1305_decrypt(plain_block, cipher_block + sizeof(block_len), block_len, cctx->decryption_key, sizeof(cctx->decryption_key), &block_len, sizeof(block_len), tag, sizeof(tag), nonce);
if (ret < 0) {
pw_log_error("Decryption with chacha poly1305 failed");
cctx->decryption_counter = cctx->decryption_counter_prev;
free(*plaintext);
return -1;
}
plain_block += block_len;
cipher_block += block_len + sizeof(block_len) + AP_AUTHTAG_LENGTH;
cctx->decryption_counter++;
}
*plaintext_len = plain_block - *plaintext;
return cipher_block - ciphertext;
}
*/
static int rtsp_do_setup(struct impl *impl) static int rtsp_do_setup(struct impl *impl)
{ {
int res; int res;
uint8_t *content = NULL;
size_t content_len = 0;
plist_t root_plist = NULL;
switch (impl->protocol) { switch (impl->protocol) {
case PROTO_TCP: case PROTO_TCP:
@ -1109,10 +1387,59 @@ static int rtsp_do_setup(struct impl *impl)
impl->timing_source = pw_loop_add_io(impl->loop, impl->timing_fd, impl->timing_source = pw_loop_add_io(impl->loop, impl->timing_fd,
SPA_IO_IN, false, on_timing_source_io, impl); SPA_IO_IN, false, on_timing_source_io, impl);
pw_properties_setf(impl->headers, "Transport", switch (impl->protocol) {
"RTP/AVP/UDP;unicast;interleaved=0-1;mode=record;" case CRYPTO_PAIR_TRANSIENT:
"control_port=%u;timing_port=%u", // Encryption/decryption of control channel
impl->control_port, impl->timing_port); const char *control_salt = AP_CONTROL_SALT;
const char *enc_info = AP_CONTROL_ENC_INFO;
const char *dec_info = AP_CONTROL_DEC_INFO;
plist_t stream = plist_new_dict();
wplist_dict_add_uint(stream, "audioFormat", 0x40000); // 0x40000 ALAC/44100/16/2
wplist_dict_add_string(stream, "audioMode", "default");
wplist_dict_add_uint(stream, "controlPort", impl->control_port);
wplist_dict_add_uint(stream, "ct", impl->codec); // Compression type, 1 LPCM, 2 ALAC, 3 AAC, 4 AAC ELD, 32 OPUS
wplist_dict_add_bool(stream, "isMedia", true); // ?
//wplist_dict_add_uint(stream, "latencyMax", 88200); // TODO how do these latencys work?
wplist_dict_add_uint(stream, "latencyMin", RAOP_LATENCY_MIN);
wplist_dict_add_data(stream, "shk", impl->shared_secret, impl->shared_secret_len);
wplist_dict_add_uint(stream, "spf", FRAMES_PER_UDP_PACKET);
wplist_dict_add_uint(stream, "sr", impl->info.rate);
wplist_dict_add_uint(stream, "type", 0x60); // RTP type: 0x60 = 96 realtime, 103 buffered
wplist_dict_add_bool(stream, "supportsDynamicStreamID", false);
wplist_dict_add_uint(stream, "streamConnectionID", (uint64_t)impl->session_id);
plist_t streams = plist_new_array();
plist_array_append_item(streams, stream);
root_plist = plist_new_dict();
plist_dict_set_item(root_plist, "streams", streams);
content = malloc(content_len);
wplist_to_bin(&content, &content_len, root_plist);
const char *url = pw_rtsp_client_get_url(impl->rtsp);
//impl->control_cipher_ctx = ap_cipher_context_new(impl->shared_secret, impl->shared_secret_len, control_salt, enc_info, control_salt, dec_info);
//if (!impl->control_cipher_ctx) {
// pw_log_error("Could not create control cipher");
// goto error;
//}
//struct pw_rtsp_cipher *cipher = ap_cipher_new(ap_decrypt, ap_encrypt);
res = pw_rtsp_client_url_send(impl->rtsp, url, "SETUP", &impl->headers->dict,
"application/x-apple-binary-plist", content, content_len,
rtsp_setup_reply, impl);
plist_free(root_plist);
free(content);
return res;
default:
pw_properties_setf(impl->headers, "Transport",
"RTP/AVP/UDP;unicast;interleaved=0-1;mode=record;"
"control_port=%u;timing_port=%u",
impl->control_port, impl->timing_port);
}
break; break;
default: default:
@ -1413,7 +1740,7 @@ static int rtsp_do_raop_auth(struct impl *impl, const struct spa_dict *headers)
return rtsp_send(impl, "OPTIONS", NULL, NULL, rtsp_raop_auth_reply); return rtsp_send(impl, "OPTIONS", NULL, NULL, rtsp_raop_auth_reply);
} }
static tlv_values_t * tlv_message_process(const uint8_t *data, size_t data_len) static tlv_values_t *tlv_message_process(const uint8_t *data, size_t data_len)
{ {
tlv_values_t *response; tlv_values_t *response;
tlv_t *error; tlv_t *error;
@ -1718,53 +2045,117 @@ static int rtsp_raop_options_reply(void *data, int status, const struct spa_dict
return res; return res;
} }
static void rtsp_do_raop_options(void *data)
{
struct impl *impl = data;
uint8_t rac[16];
char sac[16*4];
if (pw_getrandom(rac, sizeof(rac), 0) < 0) {
pw_log_error("error generating random data: %m");
return;
}
base64_encode(rac, sizeof(rac), sac, '\0');
pw_properties_set(impl->headers, "Apple-Challenge", sac);
pw_rtsp_client_send(impl->rtsp, "OPTIONS", &impl->headers->dict,
NULL, NULL, rtsp_raop_options_reply, impl);
return;
}
static int rtsp_get_info_reply(void *data, int status, const struct spa_dict *headers, const struct pw_array *content)
{
struct impl *impl = data;
int res;
pw_log_info("info status: %d", status);
plist_t info;
res = wplist_from_bin(&info, content);
if (res < 0)
return res;
plist_t features_plist = plist_dict_get_item(info, "features");
if (!features_plist)
return -1;
uint64_t features;
plist_get_uint_val(features_plist, &features);
pw_log_info("features value: 0x%" PRIx64 "\n", features);
plist_t status_flags_plist = plist_dict_get_item(info, "statusFlags");
if (!status_flags_plist)
return -1;
uint64_t status_flags;
plist_get_uint_val(status_flags_plist, &status_flags);
pw_log_info("statusFlags value: 0x%" PRIx64 "\n", status_flags);
switch (impl->encryption) {
case CRYPTO_PAIR_TRANSIENT:
rtsp_do_ap2_pair_setup1(impl);
break;
default:
rtsp_do_raop_options(impl);
break;
}
return 0;
}
static void rtsp_connected(void *data) static void rtsp_connected(void *data)
{ {
struct impl *impl = data; struct impl *impl = data;
uint32_t sci[2]; uint32_t sci[2];
//plist_t txt, qualifier = plist_new_array(), root_plist = plist_new_dict();
pw_log_info("connected"); pw_log_info("connected");
impl->connected = true; impl->connected = true;
// TODO: Do a GET /info first
if (pw_getrandom(sci, sizeof(sci), 0) < 0) { if (pw_getrandom(sci, sizeof(sci), 0) < 0) {
pw_log_error("error generating random data: %m"); pw_log_error("error generating random data: %m");
return; return;
} }
pw_properties_setf(impl->headers, "Client-Instance",
"%08X%08X", sci[0], sci[1]);
pw_properties_setf(impl->headers, "DACP-ID",
"%08X%08X", sci[0], sci[1]);
switch (impl->encryption) { switch (impl->encryption) {
case CRYPTO_PAIR_TRANSIENT: case CRYPTO_PAIR_TRANSIENT:
pw_properties_set(impl->headers, "User-Agent", AP_USER_AGENT); pw_properties_setf(impl->headers, "DACP-ID",
"%08X%08X", sci[0], sci[1]);
pw_properties_setf(impl->headers, "User-Agent", "PipeWire/%s", pw_get_headers_version());
//txt = plist_new_string("txtAirPlay");
rtsp_do_ap2_pair_setup1(impl);
break; break;
default: default:
uint8_t rac[16];
char sac[16*4];
if (pw_getrandom(rac, sizeof(rac), 0) < 0) { pw_properties_setf(impl->headers, "Client-Instance",
pw_log_error("error generating random data: %m"); "%08X%08X", sci[0], sci[1]);
return;
}
base64_encode(rac, sizeof(rac), sac, '\0'); //txt = plist_new_string("txtRAOP");
pw_properties_set(impl->headers, "Apple-Challenge", sac);
pw_properties_set(impl->headers, "User-Agent", DEFAULT_USER_AGENT);
pw_rtsp_client_send(impl->rtsp, "OPTIONS", &impl->headers->dict,
NULL, NULL, rtsp_raop_options_reply, impl);
break; break;
} }
/*
plist_array_append_item(qualifier, txt);
plist_dict_set_item(root_plist, "qualifier", qualifier);
size_t content_len = 0;
uint8_t *content = malloc(content_len);
wplist_to_bin(&content, &content_len, root_plist);
pw_rtsp_client_url_send(impl->rtsp, "/info", "GET", &impl->headers->dict,
"application/x-apple-binary-plist", content, content_len,
rtsp_get_info_reply, impl);
plist_free(root_plist);
free(content);
*/
pw_properties_setf(impl->headers, "User-Agent", "PipeWire/%s", pw_get_headers_version());
pw_rtsp_client_url_send(impl->rtsp, "/info", "GET", &impl->headers->dict,
NULL, NULL, 0,
rtsp_get_info_reply, impl);
return; return;
} }

View file

@ -243,7 +243,7 @@ static int process_status(struct pw_rtsp_client *client, char *buf)
const char *state = NULL, *s; const char *state = NULL, *s;
size_t len; size_t len;
pw_log_info("status: %s", buf); pw_log_info("processing status: %s", buf);
s = pw_split_walk(buf, " ", &len, &state); s = pw_split_walk(buf, " ", &len, &state);
if (!spa_strstartswith(s, "RTSP/")) if (!spa_strstartswith(s, "RTSP/"))
@ -355,6 +355,7 @@ static int process_content(struct pw_rtsp_client *client)
spa_assert((size_t) res <= client->content_length); spa_assert((size_t) res <= client->content_length);
client->content_length -= res; client->content_length -= res;
pw_log_info("processing content (%ld bytes):\n%s", client->content.size, client->content.data);
} }
if (client->content_length == 0) if (client->content_length == 0)