#ifdef _WIN32 #include #endif #include #include "protocol_shoutcast2Client.h" #include "ripList.h" #include "stats.h" #include "streamData.h" #include "w3cLog.h" #include "metadata.h" #include "uvox2Common.h" #include "global.h" #include "bandwidth.h" #include "MP3Header.h" #include "ADTSHeader.h" #include "file/fileUtils.h" #include "services/stdServiceImpl.h" using namespace std; using namespace uniString; using namespace stringUtil; #define LOGNAME "DST" #define DEBUG_LOG(...) do { if (gOptions.shoutcast2ClientDebug()) DLOG(__VA_ARGS__); } while(0) #define AD_DEBUG_LOG(...) do { if (gOptions.adMetricsDebug()) DLOG(__VA_ARGS__); } while(0) protocol_shoutcast2Client::protocol_shoutcast2Client (protocol_HTTPStyle &hs, const streamData::streamID_t streamID, const uniString::utf8 &hostName, const uniString::utf8 &addr,const uniString::utf8 &XFF, const bool cdnSlave) : protocol_shoutcastClient (hs, streamID, hostName, addr, XFF, streamData::SHOUTCAST2), m_cdnSlave(cdnSlave) { DEBUG_LOG(m_clientLogString + __FUNCTION__, LOGNAME, streamID); m_state = &protocol_shoutcast2Client::state_AttachToStream; m_nextState = NULL; } protocol_shoutcast2Client::~protocol_shoutcast2Client() throw() { cleanup("Shoutcast 2", gOptions.shoutcast2ClientDebug(), true); } ///////////////////////////////////// W3C Logging ////////////////////////////////////////////// // create W3C entry. Entries describe the duration a client has listened to a specific // title. the entry is generated on a title change, or when the client disconnects void protocol_shoutcast2Client::logW3C() throw() { if (gOptions.w3cEnable()) { vector md; if (!m_lastMetadata.empty()) { // loop through packets and reassemble. Since we put this stuff in there, we don't need to do integrity checks size_t total_data_size = m_lastMetadata.size(); for (size_t x = 0; x < total_data_size;) { if ((UV2X_OVERHEAD + UV2X_META_HDR_SIZE) > (total_data_size - x)) { break; // out of data } const uv2xHdr *voxHdr = (const uv2xHdr*)(&(m_lastMetadata[x])); const __uint8 *contents = (const __uint8 *)((&(m_lastMetadata[x])) + UV2X_HDR_SIZE); const __uint8* metadataContents = contents + UV2X_META_HDR_SIZE; const size_t metadataContentsSize = ntohs(voxHdr->msgLen) - UV2X_META_HDR_SIZE; if ((UV2X_OVERHEAD + UV2X_META_HDR_SIZE + metadataContentsSize) > (total_data_size - x)) { break; // out of data } md.insert(md.end(), metadataContents,metadataContents + metadataContentsSize); x += UV2X_OVERHEAD + UV2X_META_HDR_SIZE + metadataContentsSize; } } utf8 md_s(md.begin(), md.end()); // do a sanity check when trying to form the metadata if a uvox 2 connection // is attempted from an older Winamp client e.g. 5.54 - means a connection // can be made from the client without it aborting if there's no metadata. utf8 title; if (!md_s.empty()) { title = metadata::get_song_title_from_3902(md_s); } doLogW3C(title); } } ////////////////////////////////////////////////////////////////////////////// void protocol_shoutcast2Client::timeSlice() throw(exception) { int ret = doTimeSlice(true); if (ret == 1) { m_state = &protocol_shoutcastClient::state_Stream; return; } else if (ret == 2) { return; } (this->*m_state)(); } void protocol_shoutcast2Client::setCallback (protocol_shoutcastClient::state_t callback, protocol_shoutcastClient::state_t next) { m_state = callback ? callback : m_nextState; m_nextState = callback ? next : NULL; } void protocol_shoutcast2Client::state_Close() throw(exception) { DEBUG_LOG(m_clientLogString + __FUNCTION__); m_result.done(); } void protocol_shoutcast2Client::state_SendText() throw(exception) { #if defined(_DEBUG) || defined(DEBUG) DEBUG_LOG(m_clientLogString + __FUNCTION__); #endif if (sendText()) { m_state = m_nextState; } } // find the appropriate stream and try to attach to it void protocol_shoutcast2Client::state_AttachToStream() throw(exception) { DEBUG_LOG(m_clientLogString + __FUNCTION__); int read_bitrate = 0; m_streamData = streamData::accessStream(m_streamID); if (!m_streamData) { if (processReject("Shoutcast 2", bandWidth::CLIENT_V2_SENT, MSG_HTTP404, MSG_HTTP404_LEN, &read_bitrate, 0, true)) { goto fall_through; } m_state = &protocol_shoutcast2Client::state_SendText; m_nextState = &protocol_shoutcast2Client::state_Close; } else { fall_through: const utf8 movedUrl = gOptions.stream_movedUrl(m_streamID); if (movedUrl.empty()) { // we use this to control the cdn mode so we'll only provide the // headers needed if it's all enabled and the client asks for it string cdn; if (isCDNMaster(m_streamID) && m_cdnSlave) { DEBUG_LOG(m_clientLogString + "CDN slave request received by master", LOGNAME, m_streamID); utf8 authhash = (m_streamData ? m_streamData->streamAuthhash() : ""); if (yp2::isValidAuthhash(authhash)) { utf8 key = XTEA_encipher(authhash.c_str(), authhash.size(), bob().c_str(), bob().size()); cdn = "cdn-master:1\r\n" "cdn-token:" + key.hideAsString() + "\r\n"; m_clientType = ((streamData::source_t)(m_clientType | streamData::SC_CDN_SLAVE)); } else { DEBUG_LOG(m_clientLogString + "CDN slave request not sent - invalid authhash provided", LOGNAME, m_streamID); } } const int add = processAdd("FLV", bandWidth::CLIENT_V2_SENT, MSG_HTTP404, MSG_HTTP404_LEN, movedUrl, (m_streamData ? m_streamData->streamBackupServer() : "")); if (add != 1) { m_state = &protocol_shoutcast2Client::state_SendText; m_nextState = &protocol_shoutcast2Client::state_Close; } else { utf8 pub = "0", genre = ""; const bool isPodcast = (!m_streamData && (gOptions.getBackupLoop(m_streamID) == 1)); if (!isPodcast) { pub = (m_streamData ? tos(m_streamData->streamPublic()) : "1"); if (m_streamData) { if (isUserAgentRelay(toLower(m_userAgent)) && (!m_streamData->allowPublicRelay())) { pub = "0"; } for (int i = 0; i < 5; i++) { if (!m_streamData->m_streamInfo.m_streamGenre[i].empty()) { genre += (i ? ", " : "") + m_streamData->m_streamInfo.m_streamGenre[i]; } } } // if running from a backup file then no need to set the states else { pub = toLower(gOptions.stream_publicServer(m_streamID)); if (pub.empty()) { pub = toLower(gOptions.publicServer()); } if (pub == "always") { pub = "1"; } else if (pub == "never") { pub = "0"; } } } else { // TODO how do we handle podcasts for 2.x // as we should somehow swap to 1.x } utf8 title = (m_streamData ? m_streamData->streamName() : gOptions.stream_backupTitle(m_streamID)); if (!m_streamData) { if (!gOptions.read_stream_backupTitle(m_streamID)) { title = gOptions.backupTitle(); } if (title.empty()) { title = gOptions.stream_backupFile(m_streamID); if (!gOptions.read_stream_backupFile(m_streamID)) { title = gOptions.backupFile(); } if (!title.empty()) { title = fileUtil::stripSuffix(fileUtil::stripPath(title)); } } } m_OKResponse = MSG_UVOX_HTTP200 + "icy-pub:" + pub + "\r\n" + cdn + "Ultravox-SID:" + tos(m_streamID) + "\r\n" + "Ultravox-Bitrate:" + tos(m_streamData ? m_streamData->streamAvgBitrate() : (read_bitrate * 1000)) + "\r\n" + "Ultravox-Samplerate:" + tos(m_streamData ? m_streamData->streamSampleRate() : 0) + "\r\n" + "Ultravox-Title:" + title + "\r\n" + "Ultravox-Genre:" + genre + "\r\n" + "Ultravox-URL:" + (m_streamData ? m_streamData->streamURL() : (utf8)""/*"TODO"*/) + "\r\n" + "Ultravox-Max-Msg:" + tos(MAX_PAYLOAD_SIZE) + "\r\n" + "Ultravox-Class-Type:" + tohex(m_streamData ? m_streamData->streamUvoxDataType() : MP3_DATA) + "\r\n" + (m_streamData && m_streamData->streamIsVBR() ? "Ultravox-VBR:1\r\n" : "") + (gOptions.clacks() ? "X-Clacks-Overhead:GNU Terry Pratchett\r\n\r\n" : "\r\n"); DEBUG_LOG(m_clientLogString + "Sending [" + eol() + stripWhitespace(m_OKResponse) + eol() + "]"); m_outBuffer = m_OKResponse.c_str(); bandWidth::updateAmount(bandWidth::CLIENT_V2_SENT, (m_outBufferSize = (int)m_OKResponse.size())); m_state = &protocol_shoutcast2Client::state_SendText; if (!m_headRequest) { m_nextState = &protocol_shoutcast2Client::state_InitiateStream; } else { m_removeClientFromStats = false; m_ignoreDisconnect = true; m_nextState = &protocol_shoutcast2Client::state_Close; } m_result.write(); m_result.timeoutSID(m_streamID); m_result.run(); // when the client is added, we get back the unique id of the connection // but we now check for being > 0 as we need to filter out some of the // YP connections from being counted as valid clients for stats, etc reportNewListener("Shoutcast 2"); } } else { // if we get to here then we attempt to redirect the clients to the moved url // which is useful if the stream has moved hosting or it has been deprecated. streamMovedOrRejected("Shoutcast 2", bandWidth::CLIENT_V2_SENT, movedUrl, 2); m_state = &protocol_shoutcast2Client::state_SendText; m_nextState = &protocol_shoutcast2Client::state_Close; } } } void protocol_shoutcast2Client::state_SendCachedMetadata() throw(exception) { DEBUG_LOG(m_clientLogString + __FUNCTION__); if (sendDataBuffer(m_streamID, m_outBuffer, m_outBufferSize, m_clientLogString)) { bool playingAlbumArt = false; // send intro file if we have it acquireIntroFile(true); // see if there's any albumart and attempt to send stream and then playing or whatever is present m_cachedMetadata = (m_streamData ? m_streamData->getSc21StreamAlbumArt(0xFFFFFFFF) : streamData::uvoxMetadata_t()); if (m_cachedMetadata.empty()) { m_cachedMetadata = m_streamData->getSc21PlayingAlbumArt(m_readPtr); playingAlbumArt = true; } if (m_cachedMetadata.empty()) { // send intro file if we have it acquireIntroFile(true); m_state = (m_introFile.empty() ? &protocol_shoutcastClient::state_Stream : &protocol_shoutcastClient::state_SendIntroFile); } else { bandWidth::updateAmount(bandWidth::CLIENT_V2_SENT, (m_outBufferSize = (int)m_cachedMetadata.size())); m_outBuffer = (uniString::utf8::value_type*)&(m_cachedMetadata[0]); // slam cast so we can reuse variable m_state = (playingAlbumArt == false ? &protocol_shoutcast2Client::state_SendCachedStreamAlbumArt : &protocol_shoutcast2Client::state_SendCachedPlayingAlbumArt); } } } void protocol_shoutcast2Client::state_InitiateStream() throw(exception) { DEBUG_LOG(m_clientLogString + __FUNCTION__); resetReadPtr(true); m_cachedMetadata = (m_streamData ? m_streamData->getSc21Metadata(m_readPtr) : streamData::uvoxMetadata_t()); if (m_cachedMetadata.empty()) { // send intro file if we have it acquireIntroFile(true); m_state = (m_introFile.empty() ? &protocol_shoutcastClient::state_Stream : &protocol_shoutcastClient::state_SendIntro); } else { bandWidth::updateAmount(bandWidth::CLIENT_V2_SENT, (m_outBufferSize = (int)m_cachedMetadata.size())); m_outBuffer = (uniString::utf8::value_type*)&(m_cachedMetadata[0]); // slam cast so we can reuse variable m_state = &protocol_shoutcast2Client::state_SendCachedMetadata; } m_result.run(); } void protocol_shoutcast2Client::state_SendCachedStreamAlbumArt() throw(exception) { DEBUG_LOG(m_clientLogString + __FUNCTION__ + "sending stream albumart: " + tos(m_outBufferSize) + " bytes, mime type: " + tos(m_streamData->streamAlbumArtMime())); if (sendDataBuffer(m_streamID, m_outBuffer, m_outBufferSize, m_clientLogString)) { // if all went ok then look to send the playing album art if it is present otherwise do intro / stream data m_cachedMetadata = m_streamData->getSc21PlayingAlbumArt(m_readPtr); if (m_cachedMetadata.empty()) { // send intro file if we have it acquireIntroFile(true); m_state = (m_introFile.empty() ? &protocol_shoutcastClient::state_Stream : &protocol_shoutcastClient::state_SendIntroFile); } else { bandWidth::updateAmount(bandWidth::CLIENT_V2_SENT, (m_outBufferSize = (int)m_cachedMetadata.size())); m_outBuffer = (uniString::utf8::value_type*)&(m_cachedMetadata[0]); // slam cast so we can reuse variable m_state = &protocol_shoutcast2Client::state_SendCachedPlayingAlbumArt; } } } void protocol_shoutcast2Client::state_SendCachedPlayingAlbumArt() throw(exception) { DEBUG_LOG(m_clientLogString + __FUNCTION__ + "sending playing albumart: " + tos(m_outBufferSize) + " bytes, mime type: " + tos(m_streamData->streamPlayingAlbumArtMime())); if (sendDataBuffer(m_streamID, m_outBuffer, m_outBufferSize, m_clientLogString)) { // send intro file if we have it acquireIntroFile(true); m_state = (m_introFile.empty() ? &protocol_shoutcastClient::state_Stream : &protocol_shoutcastClient::state_SendIntroFile); } } void protocol_shoutcast2Client::return_403(void) { protocol_shoutcastClient::return_403(); m_state = &protocol_shoutcast2Client::state_SendText; m_nextState = &protocol_shoutcast2Client::state_Close; } const int protocol_shoutcast2Client::doFrameSync(const int type, const bool debug, const int len, const int offset, const std::vector<__uint8>& inbuf, const time_t /*cur_time */, const int, const unsigned int, int &frames, bool &advert, bool fill_remainder) throw() { bool mp3; int end = 0; if (streamData::isAllowedType(type, mp3)) { int last = min (len, (int)(inbuf.size() - offset)); const unsigned char *buf; if (m_remainder.empty () || offset < last) { buf = &inbuf [offset]; // DLOG ("last " + tos(last) + ", off " + tos(offset)); } else { const std::vector<__uint8>::const_iterator pos = inbuf.begin(); size_t cc = min (inbuf.size(), (size_t)len); cc = min (cc, (size_t)4096); if (cc > m_remainder.size()) cc -= m_remainder.size(); if (cc < 1 || cc > inbuf.size()) { ILOG ("sync2, cc is " + tos (cc)); abort(); } m_remainder.insert (m_remainder.end(), inbuf.begin(), inbuf.begin() + cc); buf = &m_remainder [0]; last = min (len, (int)m_remainder.size()); fill_remainder = false; // DLOG ("merged remainder, now " + tos (last) + ", added " + tos(cc)); } if (last > 8) { int last_size = 0; //double fps_limit = m_fps*2; fill_remainder = false; for (int i = 0; (i < last-8) && !iskilled();) { const uv2xHdr *voxHdr = (const uv2xHdr*)(&(buf[i])); const int msgLen = ntohs(voxHdr->msgLen); const int found = ((voxHdr->sync == UVOX2_SYNC_BYTE) && (msgLen > UV2X_OVERHEAD) && (msgLen <= MAX_PAYLOAD_SIZE) ? (msgLen + UV2X_OVERHEAD) : 0); // need to find frames and that the input is the correct format! // // is a bit of a pain for AAC though as we've already done the // rough bitrate match when the advert / intro / backup was read // we'll just pass things through as though the bitrate is ok... if ((found > 0)) // && (found <= len)) { if (!frames) { end = i; protocol_shoutcastClient::createFrameRate(mp3, (m_streamData ? m_streamData->streamSampleRate() : 0)); } i += (last_size = found); // only count valid full-size frames if (i <= last) { //const std::vector<__uint8>::const_iterator pos = inbuf.begin(); m_output.insert(m_output.end(), buf+end, buf+end+last_size); end += last_size; // we only want to count audio frames and not metadata const __uint16 voxMsgType = ntohs(voxHdr->msgType); if ((voxMsgType >= 0x7000) && (voxMsgType < 0x9000)) { ++frames; } // DLOG ("frame " + tos(m_frameCount+frames) + " (" + tos(found) + ") last " + tos(last) + " end " + tos(end) + " i " + tos(i)); } else { // DLOG ("EOB, " + tos (i) + ", " + tos(offset) + ", " + tos(inbuf.size())); if (m_remainder.empty()) { if (i+offset > inbuf.size()) fill_remainder = true; } else if (end + offset < inbuf.size()) fill_remainder = true; break; } } else { // we now look for the "SCAdvert" marker // for detecting if we've got an advert. if (i < (len - 8) && ((buf[i]) == 'S') && ((buf[i+1]) == 'C')) { if (!memcmp(&buf[i], "SCAdvert", 8)) { if (frames == 0) { // DLOG ("Found SCAdvert"); advert = true; end += 8; m_remainder.clear(); } break; } } else { // if we found something but there is not enough // data in the read buffer then we'll abort asap if ((found > 0) && (found > len)) { break; } if (m_frameCount && debug) { DLOG(m_clientLogString + "Bad frame found at pos: " + tos(i) + " [" + tos(found) + "]", LOGNAME, m_streamID); } } // otherwise we just need to move on and keep // looking for what is a valid starting frame ++i; } } } else fill_remainder = true; if (m_remainder.empty() == false && frames) m_remainder.clear(); if (fill_remainder) { const int remainder = (last - end); if (remainder > 0) m_remainder.insert(m_remainder.end(), buf + end, buf + (end + remainder)); } } return (len - end); } void protocol_shoutcast2Client::setIntro (vector<__uint8> &buf, int uvoxDataType) { m_introFile.clear(); if (buf.empty()) return; streamData::convertRawToUvox (m_introFile, buf, uvoxDataType, 0, 0); }