diff --git a/src/Makefile.am b/src/Makefile.am index 5796bcc0..e58b7016 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -47,6 +47,7 @@ noinst_HEADERS = \ event_exec.h \ event_url.h \ acl.h auth.h \ + metadata_xiph.h \ format.h \ format_ogg.h \ format_mp3.h \ @@ -95,6 +96,7 @@ icecast_SOURCES = \ listensocket.c \ fastevent.c \ navigation.c \ + metadata_xiph.c \ format.c \ format_ogg.c \ format_mp3.c \ diff --git a/src/format_flac.c b/src/format_flac.c index d804463d..e8205420 100644 --- a/src/format_flac.c +++ b/src/format_flac.c @@ -8,7 +8,7 @@ * oddsock , * Karl Heyes * and others (see AUTHORS for details). - * Copyright 2014-2018, Philipp "ph3-der-loewe" Schafft , + * Copyright 2014-2022, Philipp "ph3-der-loewe" Schafft , */ @@ -19,10 +19,13 @@ #endif #include +#include +#include #include #include #include "refbuf.h" +#include "metadata_xiph.h" #include "format_ogg.h" #include "client.h" #include "stats.h" @@ -30,51 +33,192 @@ #define CATMODULE "format-flac" #include "logging.h" +typedef enum { + FLAC_BLOCK_TYPE__ERROR = -1, + FLAC_BLOCK_TYPE_STREAMINFO = 0, + FLAC_BLOCK_TYPE_PADDING = 1, + FLAC_BLOCK_TYPE_APPLICATION = 2, + FLAC_BLOCK_TYPE_SEEKTABLE = 3, + FLAC_BLOCK_TYPE_VORBIS_COMMENT = 4, + FLAC_BLOCK_TYPE_CUESHEET = 5, + FLAC_BLOCK_TYPE_PICTURE = 6 +} flac_block_type_t; + +typedef struct { + flac_block_type_t type; + bool last; + size_t len; + const void *data; +} flac_block_t; + +/* returns true if parse was ok, false on error */ +static bool flac_parse_block(flac_block_t *block, const ogg_packet * packet, size_t offset) +{ + uint8_t type; + uint32_t len; + + /* check header length */ + if ((size_t)packet->bytes <= (4 + offset)) + return false; + + type = packet->packet[offset]; + + /* 0xFF is the sync code for a FRAME not a block */ + if (type == 0xFF) + return false; + + memset(block, 0, sizeof(*block)); + + if (type & 0x80) { + block->last = true; + type &= 0x7F; + } + + if (type > FLAC_BLOCK_TYPE_PICTURE) + return false; + + block->type = type; + + len = (unsigned char)packet->packet[1 + offset]; + len <<= 8; + len |= (unsigned char)packet->packet[2 + offset]; + len <<= 8; + len |= (unsigned char)packet->packet[3 + offset]; + + /* check Ogg packet size vs. self-sync size */ + if ((size_t)packet->bytes != (len + 4 + offset)) + return false; + + block->len = len; + block->data = packet->packet + 4 + offset; + + return true; +} + +static const char * flac_block_type_to_name(flac_block_type_t type) +{ + switch (type) { + case FLAC_BLOCK_TYPE__ERROR: + return ""; + break; + case FLAC_BLOCK_TYPE_STREAMINFO: + return "STREAMINFO"; + break; + case FLAC_BLOCK_TYPE_PADDING: + return "PADDING"; + break; + case FLAC_BLOCK_TYPE_APPLICATION: + return "APPLICATION"; + break; + case FLAC_BLOCK_TYPE_SEEKTABLE: + return "SEEKTABLE"; + break; + case FLAC_BLOCK_TYPE_VORBIS_COMMENT: + return "VORBIS_COMMENT"; + break; + case FLAC_BLOCK_TYPE_CUESHEET: + return "CUESHEET"; + break; + case FLAC_BLOCK_TYPE_PICTURE: + return "PICTURE"; + break; + } + + return ""; +} + +static void flac_handle_block_streaminfo(format_plugin_t *plugin, ogg_state_t *ogg_info, ogg_codec_t *codec, const flac_block_t *block) +{ + uint32_t raw; + uint32_t sample_rate; + uint32_t channels; + uint32_t bits; + + if (block->len != 34) { + ICECAST_LOG_ERROR("Can not parse FLAC header block STREAMINFO"); + return; + } + + raw = metadata_xiph_read_u32be_unaligned(block->data + 10); + sample_rate = ((raw >> 12) & 0xfffff) + 0; + channels = ((raw >> 9) & 0x7 ) + 1; + bits = ((raw >> 4) & 0x1F ) + 1; + + stats_event_args(ogg_info->mount, "audio_samplerate", "%ld", (long int)sample_rate); + stats_event_args(ogg_info->mount, "audio_channels", "%ld", (long int)channels); + stats_event_args(ogg_info->mount, "audio_bits", "%ld", (long int)bits); +} + +static void flac_handle_block(format_plugin_t *plugin, ogg_state_t *ogg_info, ogg_codec_t *codec, const flac_block_t *block) +{ + ICECAST_LOG_DEBUG("Found header of type %s%s with %zu bytes of data", flac_block_type_to_name(block->type), block->last ? "(last)" : "", block->len); + + switch (block->type) { + case FLAC_BLOCK_TYPE_STREAMINFO: + flac_handle_block_streaminfo(plugin, ogg_info, codec, block); + break; + case FLAC_BLOCK_TYPE_VORBIS_COMMENT: + vorbis_comment_clear(&plugin->vc); + vorbis_comment_init(&plugin->vc); + if (!metadata_xiph_read_vorbis_comments(&plugin->vc, block->data, block->len)) { + ICECAST_LOG_ERROR("Can not parse FLAC header block VORBIS_COMMENT"); + } + ogg_info->log_metadata = 1; + break; + default: + /* no-op */ + break; + } +} static void flac_codec_free (ogg_state_t *ogg_info, ogg_codec_t *codec) { ICECAST_LOG_DEBUG("freeing FLAC codec"); - stats_event (ogg_info->mount, "FLAC_version", NULL); - ogg_stream_clear (&codec->os); - free (codec); + stats_event(ogg_info->mount, "FLAC_version", NULL); + ogg_stream_clear(&codec->os); + free(codec); } - /* Here, we just verify the page is ok and then add it to the queue */ static refbuf_t *process_flac_page (ogg_state_t *ogg_info, ogg_codec_t *codec, ogg_page *page, format_plugin_t *plugin) { refbuf_t * refbuf; - if (codec->headers) - { + if (codec->headers) { ogg_packet packet; - if (ogg_stream_pagein (&codec->os, page) < 0) - { + + if (ogg_stream_pagein(&codec->os, page) < 0) { ogg_info->error = 1; return NULL; } - while (ogg_stream_packetout (&codec->os, &packet)) - { - int type = packet.packet[0]; - if (type == 0xFF) - { - codec->headers = 0; - break; + + while (ogg_stream_packetout(&codec->os, &packet)) { + flac_block_t block; + + if (flac_parse_block(&block, &packet, 0)) { + flac_handle_block(plugin, ogg_info, codec, &block); + + if (block.last) { + codec->headers = 0; + break; + } + + continue; } - if (type >= 1 && type <= 0x7E) - continue; - if (type >= 0x81 && type <= 0xFE) - continue; + ogg_info->error = 1; + return NULL; } - if (codec->headers) - { - format_ogg_attach_header (ogg_info, page); + + if (codec->headers) { + format_ogg_attach_header(ogg_info, page); return NULL; } } - refbuf = make_refbuf_with_page (page); + + refbuf = make_refbuf_with_page(page); + return refbuf; } @@ -84,18 +228,19 @@ static refbuf_t *process_flac_page (ogg_state_t *ogg_info, ogg_codec_t *codec, o ogg_codec_t *initial_flac_page (format_plugin_t *plugin, ogg_page *page) { ogg_state_t *ogg_info = plugin->_state; - ogg_codec_t *codec = calloc (1, sizeof (ogg_codec_t)); + ogg_codec_t *codec = calloc(1, sizeof(ogg_codec_t)); ogg_packet packet; - ogg_stream_init (&codec->os, ogg_page_serialno (page)); - ogg_stream_pagein (&codec->os, page); + ogg_stream_init(&codec->os, ogg_page_serialno(page)); + ogg_stream_pagein(&codec->os, page); - ogg_stream_packetout (&codec->os, &packet); + ogg_stream_packetout(&codec->os, &packet); ICECAST_LOG_DEBUG("checking for FLAC codec"); do { unsigned char *parse = packet.packet; + flac_block_t block; if (page->header_len + page->body_len != 79) break; @@ -108,12 +253,15 @@ ogg_codec_t *initial_flac_page (format_plugin_t *plugin, ogg_page *page) ICECAST_LOG_INFO("seen initial FLAC header"); parse += 4; - stats_event_args (ogg_info->mount, "FLAC_version", "%d.%d", parse[0], parse[1]); + stats_event_args(ogg_info->mount, "FLAC_version", "%d.%d", parse[0], parse[1]); codec->process_page = process_flac_page; codec->codec_free = flac_codec_free; codec->headers = 1; codec->name = "FLAC"; + if (flac_parse_block(&block, &packet, 13)) + flac_handle_block(plugin, ogg_info, codec, &block); + format_ogg_attach_header(ogg_info, page); return codec; } while (0); diff --git a/src/format_opus.c b/src/format_opus.c index a1d3d27a..94c7e0a6 100644 --- a/src/format_opus.c +++ b/src/format_opus.c @@ -7,7 +7,7 @@ * * Copyright 2012, David Richards, Mozilla Foundation, * and others (see AUTHORS for details). - * Copyright 2014-2018, Philipp "ph3-der-loewe" Schafft , + * Copyright 2014-2022, Philipp "ph3-der-loewe" Schafft , */ @@ -21,6 +21,7 @@ #include #include +#include "metadata_xiph.h" #include "format_opus.h" #include "stats.h" #include "refbuf.h" @@ -37,19 +38,6 @@ static void opus_codec_free (ogg_state_t *ogg_info, ogg_codec_t *codec) free(codec); } -static uint32_t __read_header_u32be_unaligned(const unsigned char *in) -{ - uint32_t ret = 0; - ret += in[3]; - ret <<= 8; - ret += in[2]; - ret <<= 8; - ret += in[1]; - ret <<= 8; - ret += in[0]; - return ret; -} - static void __handle_header_opushead(ogg_state_t *ogg_info, ogg_packet *packet) { if (packet->bytes < 19) { @@ -63,17 +51,12 @@ static void __handle_header_opushead(ogg_state_t *ogg_info, ogg_packet *packet) } stats_event_args(ogg_info->mount, "audio_channels", "%ld", (long int)packet->packet[9]); - stats_event_args(ogg_info->mount, "audio_samplerate", "%ld", (long int)__read_header_u32be_unaligned(packet->packet+12)); + stats_event_args(ogg_info->mount, "audio_samplerate", "%ld", (long int)metadata_xiph_read_u32le_unaligned(packet->packet+12)); } static void __handle_header_opustags(ogg_state_t *ogg_info, ogg_packet *packet, format_plugin_t *plugin) { - size_t comments; - size_t next; size_t left = packet->bytes; - size_t buflen = 0; - char *buf = NULL; - char *buf_new; const void *p = packet->packet; if (packet->bytes < 16) { @@ -85,77 +68,11 @@ static void __handle_header_opustags(ogg_state_t *ogg_info, ogg_packet *packet, p += 8; left -= 8; - /* Now the vendor string follows. We just skip it. */ - next = __read_header_u32be_unaligned(p); - p += 4; - left -= 4; - - if (left < (next + 4)) { - ICECAST_LOG_WARN("Bad Opus header: corrupted OpusTags header."); - return; - } - p += next; - left -= next; - - /* Next is the comment counter. */ - comments = __read_header_u32be_unaligned(p); - p += 4; - left -= 4; - - /* Ok, next (comments) blocks follows, each composed of 4 byte length followed by the data */ - if (left < (comments * 4)) { - ICECAST_LOG_WARN("Bad Opus header: corrupted OpusTags header."); - return; - } - vorbis_comment_clear(&plugin->vc); vorbis_comment_init(&plugin->vc); - - while (comments) { - next = __read_header_u32be_unaligned(p); - p += 4; - left -= 4; - - if (left < next) { - if (buf) - free(buf); - vorbis_comment_clear(&plugin->vc); - vorbis_comment_init(&plugin->vc); - ICECAST_LOG_WARN("Bad Opus header: corrupted OpusTags header."); - return; - } - - if ((next + 1) > buflen) { - buf_new = realloc(buf, next + 1); - if (buf_new) { - buf = buf_new; - buflen = next + 1; - } - } - - if (buflen >= (next + 1)) { - memcpy(buf, p, next); - buf[next] = 0; - vorbis_comment_add(&plugin->vc, buf); - } - - p += next; - left -= next; - - comments--; - if (comments && left < 4) { - if (buf) - free(buf); - vorbis_comment_clear(&plugin->vc); - vorbis_comment_init(&plugin->vc); - ICECAST_LOG_WARN("Bad Opus header: corrupted OpusTags header."); - return; - } + if (!metadata_xiph_read_vorbis_comments(&plugin->vc, p, left)) { + ICECAST_LOG_WARN("Bad Opus header: corrupted OpusTags header."); } - - if (buf) - free(buf); - ogg_info->log_metadata = 1; } diff --git a/src/metadata_xiph.c b/src/metadata_xiph.c new file mode 100644 index 00000000..395f6542 --- /dev/null +++ b/src/metadata_xiph.c @@ -0,0 +1,115 @@ +/* Icecast + * + * This program is distributed under the GNU General Public License, version 2. + * A copy of this license is included with this source. + * + * Copyright 2022, Philipp "ph3-der-loewe" Schafft + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include "metadata_xiph.h" + +#include "logging.h" +#define CATMODULE "metadata-xiph" + +uint32_t metadata_xiph_read_u32be_unaligned(const unsigned char *in) +{ + uint32_t ret = 0; + ret += in[0]; + ret <<= 8; + ret += in[1]; + ret <<= 8; + ret += in[2]; + ret <<= 8; + ret += in[3]; + return ret; +} + +uint32_t metadata_xiph_read_u32le_unaligned(const unsigned char *in) +{ + uint32_t ret = 0; + ret += in[3]; + ret <<= 8; + ret += in[2]; + ret <<= 8; + ret += in[1]; + ret <<= 8; + ret += in[0]; + return ret; +} + +bool metadata_xiph_read_vorbis_comments(vorbis_comment *vc, const void *buffer, size_t len) +{ + bool ret = true; + size_t expected_len = 8; + uint32_t vendor_len; + uint32_t count; + uint32_t i; + char *out_buffer = NULL; + size_t out_buffer_len = 0; + + if (!vc || !buffer || len < expected_len) + return false; + + /* reading vendor tag and discarding it */ + vendor_len = metadata_xiph_read_u32le_unaligned(buffer); + expected_len += vendor_len; + + if (len < expected_len) + return false; + + buffer += 4 + vendor_len; + + count = metadata_xiph_read_u32le_unaligned(buffer); + + expected_len += count * 4; + + if (len < expected_len) + return false; + + buffer += 4; + + for (i = 0; i < count; i++) { + uint32_t comment_len = metadata_xiph_read_u32le_unaligned(buffer); + + buffer += 4; + + expected_len += comment_len; + + if (len < expected_len) { + ret = false; + break; + } + + if (out_buffer_len < comment_len || !out_buffer) { + char *n_out_buffer = realloc(out_buffer, comment_len + 1); + if (!n_out_buffer) { + ret = false; + break; + } + + out_buffer = n_out_buffer; + out_buffer_len = comment_len; + } + + memcpy(out_buffer, buffer, comment_len); + out_buffer[comment_len] = 0; + buffer += comment_len; + + vorbis_comment_add(vc, out_buffer); + } + + if (!ret) { + free(out_buffer); + vorbis_comment_clear(vc); + vorbis_comment_init(vc); + } + + return ret; +} diff --git a/src/metadata_xiph.h b/src/metadata_xiph.h new file mode 100644 index 00000000..7127fdca --- /dev/null +++ b/src/metadata_xiph.h @@ -0,0 +1,24 @@ +/* Icecast + * + * This program is distributed under the GNU General Public License, version 2. + * A copy of this license is included with this source. + * + * Copyright 2022, Philipp "ph3-der-loewe" Schafft + */ + +#ifndef __METADATA_XIPH_H__ +#define __METADATA_XIPH_H__ + +#include +#include +#include /* for size_t */ + +#include + +uint32_t metadata_xiph_read_u32be_unaligned(const unsigned char *in); +uint32_t metadata_xiph_read_u32le_unaligned(const unsigned char *in); + +/* returns true if parsing was successful, *vc must be in inited state before and will be in inited state after (even when false is returned) */ +bool metadata_xiph_read_vorbis_comments(vorbis_comment *vc, const void *buffer, size_t len); + +#endif