/* * * @APPLE_LICENSE_HEADER_START@ * * Copyright (c) 1999-2008 Apple Inc. All Rights Reserved. * * This file contains Original Code and/or Modifications of Original Code * as defined in and that are subject to the Apple Public Source License * Version 2.0 (the 'License'). You may not use this file except in * compliance with the License. Please obtain a copy of the License at * http://www.opensource.apple.com/apsl/ and read it before using this * file. * * The Original Code and all software distributed under the License are * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. * Please see the License for the specific language governing rights and * limitations under the License. * * @APPLE_LICENSE_HEADER_END@ * */ /* File: QTSSMP3StreamingModule.cpp Contains: This module will reflect a ShoutCast/IceCast-style MP3 broadcast out to multiple clients. Written by: Steve Ussery */ #ifndef kVersionString #include "../../revision.h" #endif #include "QTSSMP3StreamingModule.h" #include "QTSSModuleUtils.h" #include "QTSSRollingLog.h" #include "StringParser.h" #include "OSMemory.h" #include "OS.h" #define DEBUG_MP3STREAMING_MODULE 0 #if DEBUG_MP3STREAMING_MODULE #define DTRACE(s) qtss_printf(s) #define DTRACE1(s,n1) qtss_printf(s,n1) #define DTRACE2(s,n1,n2) qtss_printf(s,n1,n2) #else #define DTRACE(s) #define DTRACE1(s,n1) #define DTRACE2(s,n1,n2) #endif class QTSSMP3AccessLog; //************************************************** #define kOKHeader "OK\r\n" #ifdef __MacOSX__ #define kClientAcceptHeader "HTTP/1.1 200 OK\r\nServer: QuickTime Streaming Server %s/%s\r\nContent-Type: audio/mpeg\r\nCache-Control: no-cache\r\nPragma: no-cache\r\nConnection: close\r\n" #define kM3UReplyHeader "HTTP/1.1 200 OK\r\nServer: QuickTime Streaming Server %s/%s\r\nContent-Type: audio/mpegurl\r\nCache-Control: no-cache\r\nPragma: no-cache\r\nConnection: close\r\nContent-Length:" #else #define kClientAcceptHeader "HTTP/1.1 200 OK\r\nServer: Darwin Streaming Server %s/%s\r\nContent-Type: audio/mpeg\r\nCache-Control: no-cache\r\nPragma: no-cache\r\nConnection: close\r\n" #define kM3UReplyHeader "HTTP/1.1 200 OK\r\nServer: Darwin Streaming Server %s/%s\r\nContent-Type: audio/mpegurl\r\nCache-Control: no-cache\r\nPragma: no-cache\r\nConnection: close\r\nContent-Length:" #endif #define kSourceReject "Error - Mount Point Taken or Invalid\r\n" #define kSourceBadPassword "Error - Bad Password\r\n" #define kClientCLHeader "Content-Length: 54000000\r\n\r\n" #define kRemoteAddressSize 20 #define kBandwidthToAddEstimate 16000 // STATIC DATA static QTSS_ModulePrefsObject sPrefs = NULL; static QTSS_PrefsObject sServerPrefs = NULL; static OSBufferPool* sOSBufferPoolPtr = NULL; static QTSSMP3AccessLog* sMP3AccessLog = NULL; static QTSS_ServerObject sServer = NULL; static OSMutex* sLogMutex = NULL; // MP3 access log isn't reentrant static OSMutex* sAtomicMutex = NULL; static Bool16 sServerIdle = true; // Server global MP3 Session management classes. static MP3BroadcasterQueue sMP3BroadcasterQueue; static MP3SessionTable sMP3SessionTable; // PREFERENCE VALUES static UInt32 sBroadcastBufferSize = 8192; static UInt32 sDefaultBroadcastBufferSize = 8192; static char* sBroadcastPassword = NULL; static char* sDefaultBroadcastPassword = " "; static Bool16 sMP3StreamingEnabled = true; static Bool16 sDefaultMP3StreamingEnabled = true; static SInt32 sMaximumConnections = 0; static SInt32 sMaximumBandwidth = 0; static UInt32 sDefaultRollInterval = 7; static Bool16 sDefaultLogTimeInGMT = true; static UInt32 sDefaultMaxLogBytes = 10240000; static SInt32 sDefaultFlowControlTimeInMSec = 10000; static SInt32 sMaxFlowControlTimeInMSec = 10000; static Bool16 sDefaultLogEnabled = true; static char* sDefaultLogName = "mp3_access"; static char* sDefaultLogDir = NULL; static char* sLogName = NULL; static char* sLogDir = NULL; static Bool16 sLogEnabled = true; static UInt32 sMaxLogBytes = 10240000; static UInt32 sRollInterval = 7; static Bool16 sLogTimeInGMT = true; static char* sLogHeader = "#Software: %s\n" "#Version: %s\n" //%s == version "#Date: %s\n" //%s == date/time "#Remark: All date values are in %s.\n" //%s == "GMT" or "local time" "#Remark: c-duration is in seconds.\n" "#Fields: c-ip c-user-agent [date time] cs-uri c-status c-bytes c-duration\n"; static char gClientAcceptHeader[1024]; static char gM3UReplyHeader[1024]; // FUNCTION PROTOTYPES static QTSS_Error QTSSMP3StreamingModuleDispatch(QTSS_Role inRole, QTSS_RoleParamPtr inParams); static QTSS_Error Register(QTSS_Register_Params* inParams); static QTSS_Error Initialize(QTSS_Initialize_Params* inParams); static QTSS_Error FilterRequest(QTSS_Filter_Params* inParams); static QTSS_Error RereadPrefs(); static QTSS_Error SessionClosing(QTSS_RTSPSession_Params* inParams); static QTSS_Error StateChange(QTSS_StateChange_Params* stateChangeParams); static QTSS_Error Shutdown(); static void IncrementMP3SessionCount(); static void DecrementMP3SessionCount(); static void IncrementTotalMP3Bytes(UInt32 bytes); static Bool16 CheckBandwidth(SInt32 bandwidth); #if DEBUG_MP3STREAMING_MODULE static void PrintStringBuffer(StrPtrLen& stringBuffer); #else #define PrintStringBuffer(stringBuffer) ; #endif static void KeepSession(QTSS_RTSPRequestObject theRequest,Bool16 keep); static UInt32 GetRTSPSessionID(QTSS_RTSPSessionObject session); static Bool16 IsActiveBroadcastSession(QTSS_RTSPSessionObject session); static MP3BroadcasterSession* FindBroadcastSession(QTSS_RTSPSessionObject session); static Bool16 IsBroadcastPassword(StrPtrLen& theRequest); static Bool16 IsShoutcastPassword(StrPtrLen& theRequest); static Bool16 IsHTTPGet(StrPtrLen& theRequest); static Bool16 IsA_m3u_URL(char* theURL); static Bool16 IsMetaDataURL(char* theURL); static void ParseMetaDataURL(char* theURL); static Bool16 CheckPassword(QTSS_Filter_Params* inParams, StrPtrLen& theRequest, StrPtrLen& mountpoint); static Bool16 NeedsMetaData(StrPtrLen& theRequest); static Bool16 ParseURL(StrPtrLen& theRequest, char* outURL, UInt16 maxlen); static QTSS_Error LogRequest(QTSS_RTSPSessionObject inRTSPSession, MP3ClientSession* client); static void WriteStartupMessage(); static void WriteShutdownMessage(); static void url_strcpy(char* dest, const char* src); static QTSS_Error ReEnterFilterRequest(QTSS_Filter_Params* inParams, MP3Session* mp3Session); static Bool16 IsHTTP(StrPtrLen& theRequest); static Bool16 IsRTSP(StrPtrLen& theRequest); //************************************************** // CLASS DECLARATIONS //************************************************** class QTSSMP3AccessLog : public QTSSRollingLog { public: QTSSMP3AccessLog(); virtual ~QTSSMP3AccessLog() {} virtual char* GetLogName() { return QTSSModuleUtils::GetStringAttribute(sPrefs, "mp3_request_logfile_name", sDefaultLogName); } virtual char* GetLogDir() { return QTSSModuleUtils::GetStringAttribute(sPrefs, "mp3_request_logfile_dir", sDefaultLogDir); } virtual UInt32 GetRollIntervalInDays() { return sRollInterval; } virtual UInt32 GetMaxLogBytes() { return sMaxLogBytes; } virtual time_t WriteLogHeader(FILE *inFile); }; // --------------------------------------------------------------------------- // CLASS IMPLEMENTATIONS // --------------------------------------------------------------------------- // **************************************************************************** // QTSSMP3AccessLog -- subclass of QTSSRollingLog // **************************************************************************** QTSSMP3AccessLog::QTSSMP3AccessLog() : QTSSRollingLog() { this->SetTaskName("QTSSMP3AccessLog"); } time_t QTSSMP3AccessLog::WriteLogHeader(FILE *inFile) { //The point of this header is to record the exact time the log file was created, //in a format that is easy to parse through whenever we open the file again. //This is necessary to support log rolling based on a time interval, and POSIX doesn't //support a create date in files. time_t calendarTime = ::time(NULL); Assert(-1 != calendarTime); if (-1 == calendarTime) return -1; struct tm timeResult; struct tm* theLocalTime = qtss_localtime(&calendarTime, &timeResult); Assert(NULL != theLocalTime); if (NULL == theLocalTime) return -1; char tempBuffer[1024] = { 0 }; qtss_strftime(tempBuffer, sizeof(tempBuffer), "#Log File Created On: %m/%d/%Y %H:%M:%S\n", theLocalTime); this->WriteToLog(tempBuffer, !kAllowLogToRoll); tempBuffer[0] = '\0'; //format a date for the startup time char theDateBuffer[QTSSRollingLog::kMaxDateBufferSizeInBytes] = { 0 }; Bool16 result = QTSSRollingLog::FormatDate(theDateBuffer, false); if (result) { StrPtrLen serverName; (void)QTSS_GetValuePtr(sServer, qtssSvrServerName, 0, (void**)&serverName.Ptr, &serverName.Len); StrPtrLen serverVersion; (void)QTSS_GetValuePtr(sServer, qtssSvrServerVersion, 0, (void**)&serverVersion.Ptr, &serverVersion.Len); qtss_sprintf(tempBuffer, sLogHeader, serverName.Ptr , serverVersion.Ptr, theDateBuffer, sLogTimeInGMT ? "GMT" : "local time"); this->WriteToLog(tempBuffer, !kAllowLogToRoll); } return calendarTime; } // **************************************************************************** // MP3Session -- This is a base class to hold all the MP3 related // session state info. // **************************************************************************** // initialize static value of class. SInt32 MP3Session::sTotalNumMP3Sessions = 0L; MP3Session::MP3Session(QTSS_RTSPSessionObject sess, QTSS_StreamRef stream) : fState(0), fResult(200), fStream(stream), fSession(sess), fSessID(0) { sTotalNumMP3Sessions++; sMP3SessionTable.RegisterSession(this); } MP3Session::MP3Session() : fState(0), fResult(0), fStream(NULL), fSession(NULL), fSessID(0) { // this should never be called Assert(0); } MP3Session::~MP3Session() { --sTotalNumMP3Sessions; sMP3SessionTable.UnRegisterSession(this); fStream = NULL; fSession = NULL; fSessID = 0; } UInt8 MP3Session::IsA() const { return kMP3UndefinedSessionType; } // **************************************************************************** // MP3BroadcasterSession -- This is a class to hold all the MP3 Broadcaster // session state info. // **************************************************************************** MP3BroadcasterSession::MP3BroadcasterSession() { // this should never be called. Assert(0); } MP3BroadcasterSession::MP3BroadcasterSession(QTSS_RTSPSessionObject sess, QTSS_StreamRef stream) : MP3Session(sess, stream), fMP3ClientQueue(NULL), fDataBufferLen(0), fDataBufferSize(sBroadcastBufferSize), fNewSongName(false) { fBuffer = (char*) sOSBufferPoolPtr->Get(); InitBroadcastSessionState(); fHeader[0] = '\0'; fSongName[0] = '\0'; } MP3BroadcasterSession::~MP3BroadcasterSession() { SetState(MP3BroadcasterSession::kBroadcasterShutDownState); delete fMP3ClientQueue; fMP3ClientQueue = NULL; sOSBufferPoolPtr->Put(fBuffer); fBuffer = NULL; } UInt8 MP3BroadcasterSession::IsA() const { return kMP3BroadcasterSessionType; } void MP3BroadcasterSession::InitBroadcastSessionState() { ::memset(fMountpoint, 0, kURLBufferSize); ::memset(fHeader, 0, kHeaderBufferSize); ::memset(fSongName, 0, kSongNameBufferSize); ::memset(fBuffer, 0, fDataBufferSize); SetMountpoint("/"); fDataBufferLen = 0; SetSessionID(0); if (fMP3ClientQueue == NULL) fMP3ClientQueue = NEW MP3ClientQueue(); SetState(MP3BroadcasterSession::kBroadcasterInitState); } // Run this broadcaster session's state machine. // This is called each time through the QTSS_FilterRequest mode which // is highly re-entrant. QTSS_Error MP3BroadcasterSession::ExecuteState() { QTSS_Error err = QTSS_NoErr; // The only states we handle here are kBroadcasterGetHeaderState and // the kBroadcasterRecvDataState. All others are ignored. if (GetState() == MP3BroadcasterSession::kBroadcasterGetHeaderState) { err = GetBroadcastHeaders(); } else if (GetState() == MP3BroadcasterSession::kBroadcasterRecvDataState) { err = GetBroadcastData(); } return err; } // Put this broadcast session in a state where it will accept incoming // broadcaster connection request. void MP3BroadcasterSession::AcceptBroadcastSessionState() { SetState(MP3BroadcasterSession::kBroadcasterValidatePasswordState); } // Put this broadcast session in a state where it has accepted // the broadcaster connection request. void MP3BroadcasterSession::AcceptPasswordState() { SetState(MP3BroadcasterSession::kBroadcasterGetHeaderState); } // Put this broadcast session in a state where it will reflect // MP3 data to connected clients. void MP3BroadcasterSession::AcceptDataStreamState() { SetState(MP3BroadcasterSession::kBroadcasterRecvDataState); } // Put this broadcast session in a state where it will no // longer accept connections or reflect MP3 data. void MP3BroadcasterSession::ShutDownState() { SetState(MP3BroadcasterSession::kBroadcasterShutDownState); } // Set the mount point URL that clients will associate // with this broadcaster session. void MP3BroadcasterSession::SetMountpoint(char* mp) { StrPtrLen mpStr(mp); SetMountpoint(mpStr); } // Set the mount point URL that clients will associate // with this broadcaster session. void MP3BroadcasterSession::SetMountpoint(StrPtrLen& mp) { ::memset(fMountpoint, 0, kURLBufferSize); if (mp.Ptr != NULL && mp.Len > 0 && mp.Len < kURLBufferSize) ::memcpy(fMountpoint, mp.Ptr, mp.Len); } // Set the song name of the currently broadcasting session. void MP3BroadcasterSession::SetSongName(char* sn) { if (sn != NULL && *sn != '\0') { // This mutex is to guard against updating a // song name string while we're updating our queued // clients. OSMutexLocker locker(&fSongNameMutex); url_strcpy(fSongName, sn); fNewSongName = true; } } // See if a given mount point URL matches. Bool16 MP3BroadcasterSession::MountpointEqual(char* mp) { StrPtrLen mpStr(mp); return MountpointEqual(mpStr); } // See if a given mount point URL matches. Bool16 MP3BroadcasterSession::MountpointEqual(StrPtrLen& mp) { if (0 == *fMountpoint) return false; return mp.Equal(fMountpoint); } // Add a MP3 client session ref to our broadcaster QTSS_Error MP3BroadcasterSession::AddClient(QTSS_RTSPSessionObject sess, QTSS_StreamRef stream) { if (fMP3ClientQueue == NULL) { Assert(0); return QTSS_RequestFailed; } return fMP3ClientQueue->AddClient(sess, stream, this); } // Is this RTSP session one of my clients? Bool16 MP3BroadcasterSession::IsMyClient(QTSS_RTSPSessionObject sess) { if (fMP3ClientQueue == NULL) { return false; } return fMP3ClientQueue->InQueue(sess); } // Remove a MP3 client session ref from our broadcaster QTSS_Error MP3BroadcasterSession::RemoveClient(QTSS_RTSPSessionObject sess) { if (fMP3ClientQueue == 0) { Assert(0); return QTSS_BadArgument; } return fMP3ClientQueue->RemoveClient(sess); } //Send acknowledgement response to broacaster QTSS_Error MP3BroadcasterSession::SendOKResponse() { QTSS_Error theErr = QTSS_NoErr; if (GetStreamRef() == 0) { Assert(0); return theErr; } theErr = QTSS_Write(GetStreamRef(), kOKHeader, ::strlen(kOKHeader), NULL, qtssWriteFlagsBufferData); if (theErr != QTSS_NoErr) { return theErr; } theErr = QTSS_Flush(GetStreamRef()); Assert(theErr == QTSS_NoErr); return theErr; } // Read the broadcast headers QTSS_Error MP3BroadcasterSession::GetBroadcastHeaders() { QTSS_Error theErr = QTSS_NoErr; UInt32 theLen = 0; Assert(GetStreamRef() != 0); ::memset(fHeader, 0, kHeaderBufferSize); theErr = QTSS_Read(GetStreamRef(), fHeader, kHeaderBufferSize, &theLen); if (theErr == QTSS_WouldBlock) { // we're blocked from reading the broadcast headers so we will // schedule a read so we can try again later. theErr = QTSS_RequestEvent(GetStreamRef(), QTSS_ReadableEvent); Assert(theErr == QTSS_NoErr); } else if (theErr == QTSS_NoErr) { // The read of the headers succeeded TerminateHeaders(); // parse headers here... if (strncmp(fHeader, "icy-name", 8) != 0) AcceptDataStreamState(); // Send another OK response. SendOKResponse(); // schedule the first read of the data stream. theErr = QTSS_RequestEvent(GetStreamRef(), QTSS_ReadableEvent); Assert(theErr == QTSS_NoErr); } else { // The read failed with an error other than QTSS_WouldBlock DTRACE1("MP3BroadcasterSession::GetBroadcastHeaders() - read failed with err = %"_S32BITARG_"\n", theErr); Assert(0); } return theErr; } // Read the broadcast data stream QTSS_Error MP3BroadcasterSession::GetBroadcastData() { QTSS_Error theErr = QTSS_NoErr; if (GetStreamRef() == 0) { Assert(0); return theErr; } theErr = QTSS_Read(GetStreamRef(), fBuffer, fDataBufferSize, &fDataBufferLen); if (theErr == QTSS_WouldBlock) { // we're blocked from reading the broadcast headers so we will // schedule a read so we can try again later. theErr = QTSS_RequestEvent(GetStreamRef(), QTSS_ReadableEvent); Assert(theErr == QTSS_NoErr); } else if (theErr == QTSS_NoErr) { // The read of the broadcast stream succeeded // Send all our clients a copy of the data theErr = SendClientsData(); // Set up to read the next chunk of incoming broadcast data // If we didn't do this we'd exit the kFilterRequest state after returning. // By doing this we get called back when more data arrives. theErr = QTSS_RequestEvent(GetStreamRef(), QTSS_ReadableEvent); Assert(theErr == QTSS_NoErr); } else if (theErr == QTSS_NotConnected) { DTRACE("MP3BroadcasterSession::GetBroadcastData() - read failed with err = QTSS_NotConnected\n"); ShutDownState(); } else { DTRACE1("MP3BroadcasterSession::GetBroadcastData() - read failed with err = %"_S32BITARG_"\n", theErr); Assert(0); ShutDownState(); } return theErr; } // Send the broadcast data stream to all our clients QTSS_Error MP3BroadcasterSession::SendClientsData() { QTSS_Error theErr = QTSS_NoErr; if (fMP3ClientQueue == 0) { return theErr; } if (fDataBufferLen > 0) { // Make sure all queued client sessions have the // current song name PreflightClients(); // Send MP3 data to everything in our client queue. theErr = fMP3ClientQueue->SendToAllClients(fBuffer, fDataBufferLen); } return theErr; } // Update all of our queued clients with the currently // active song name void MP3BroadcasterSession::PreflightClients() { if (fMP3ClientQueue == 0) { return; } if (fNewSongName) { OSMutexLocker locker(&fSongNameMutex); // Copy new song name to all queue MP3ClientSessions. // and attempt to resend data that was previously blocked. fMP3ClientQueue->PreflightClients(GetSongName()); fNewSongName = false; } } // Search through the audiocast headers and terminate them with a '\0' // to mark the end. void MP3BroadcasterSession::TerminateHeaders() { char *bp = fHeader; char *end = fHeader + kHeaderBufferSize; while (bp < end) { // scan for newlines until end of buffer. if (*bp++ == '\n') { // if the next line after newline starts with // 'x' or 'i' we assume it's an 'x-audiocast-xxx' line // or an 'icy-xxxx' line, otherwise we're done. if (*bp != 'x' && *bp != 'i') break; } } // make sure we didn't go past the end of buffer. if (bp > end) bp = end; // terminate the buffer. *bp = '\0'; } // **************************************************************************** // MP3ClientSession -- This is a class to hold all the MP3Client state info. // **************************************************************************** MP3ClientSession::MP3ClientSession() { Assert(0); // this should never be called. } MP3ClientSession::MP3ClientSession( QTSS_RTSPSessionObject sess, QTSS_StreamRef stream, MP3BroadcasterSession* owner) : MP3Session(sess, stream), fBytesSent(0), fCurrentBitRate(0), fLastBitRateBytes(0), fLastBitRateUpdateTime(0), fConnectTime(0), fSendCount(0), fRetryCount(0), fOwner(owner), fNewSongName(false), fWasBlocked(false), fBlockTime(0), fNeedsContentLength(false), fWantsMetaData(true) { QTSS_Error err = QTSS_NoErr; const char* kType = "MP3 Client"; ::memset(fSongName, 0, kSongNameBufferSize); ::memset(fHeader, 0, kHeaderBufferSize); ::memset(fHostName, 0, kHostNameBufferSize); ::memset(fUserAgent, 0, kUserAgentBufferSize); ::memset(fRequestBuffer, 0, kRequestBufferSize); ParseRequestParams(stream); QTSS_LockObject(sServer); UInt32 index; UInt32 tempInt; char tempBuffer[1024]; UInt32 len; QTSS_CreateObjectValue(sServer, qtssSvrConnectedUsers, qtssConnectedUserObjectType, &index, &fQTSSObject); QTSS_SetValue(fQTSSObject, qtssConnectionType, 0, kType, strlen(kType) + 1); if (fHostName[0] != '\0') QTSS_SetValue(fQTSSObject, qtssConnectionHostName, 0, fHostName, ::strlen(fHostName) + 1); QTSS_SetValuePtr(fQTSSObject, qtssConnectionBytesSent, &fBytesSent, sizeof(fBytesSent)); QTSS_SetValuePtr(fQTSSObject, qtssConnectionCurrentBitRate, &fCurrentBitRate, sizeof(fBytesSent)); tempInt = 0; QTSS_SetValue(fQTSSObject, qtssConnectionPacketLossPercent, 0, &tempInt, sizeof(tempInt)); fConnectTime = OS::Milliseconds(); QTSS_SetValue(fQTSSObject, qtssConnectionCreateTimeInMsec, 0, &fConnectTime, sizeof(fConnectTime)); len = sizeof(tempBuffer); err = QTSS_GetValue(sess, qtssRTSPSesLocalAddrStr, 0, tempBuffer, &len); if (err == QTSS_NoErr) (void)QTSS_SetValue(fQTSSObject, qtssConnectionSessLocalAddrStr, 0, tempBuffer, len); len = sizeof(tempBuffer); err = QTSS_GetValue(sess, qtssRTSPSesRemoteAddrStr, 0, tempBuffer, &len); if (err == QTSS_NoErr) (void)QTSS_SetValue(fQTSSObject, qtssConnectionSessRemoteAddrStr, 0, tempBuffer, len); char* mountpoint = owner->GetMountpoint(); (void)QTSS_SetValue(fQTSSObject, qtssConnectionMountPoint, 0, mountpoint, strlen(mountpoint) + 1); QTSS_UnlockObject(sServer); // Increment the server's MP3 session count. IncrementMP3SessionCount(); } MP3ClientSession::~MP3ClientSession() { SetState(MP3ClientSession::kClientShutDownState); QTSS_Object sessionObject; UInt32 len = sizeof(QTSS_Object); QTSS_LockObject(sServer); for (int x = 0; QTSS_GetValue(sServer, qtssSvrConnectedUsers, x, &sessionObject, &len) == QTSS_NoErr; x++) { Assert(sessionObject != NULL); Assert(len == sizeof(QTSS_Object)); if (sessionObject == fQTSSObject) { (void)QTSS_RemoveValue(sServer, qtssSvrConnectedUsers, x); break; } } QTSS_UnlockObject(sServer); // Decrease the server's count of MP3 sessions by one. DecrementMP3SessionCount(); } UInt8 MP3ClientSession::IsA() const { return kMP3ClientSessionType; } // Send the client it HTTP response QTSS_Error MP3ClientSession::SendResponse() { QTSS_Error theErr = QTSS_NoErr; UInt32 len = 0; char buffer[512]; if (GetState() == MP3ClientSession::kClientInitState) { // // Note: We don't Parse any client request HTTP parms here... // SetState(MP3ClientSession::kClientSendResponse); theErr = QTSS_Write(GetStreamRef(), gClientAcceptHeader, ::strlen(gClientAcceptHeader), NULL, qtssWriteFlagsBufferData); if (theErr != QTSS_NoErr) { SetState(MP3ClientSession::kClientShutDownState); return theErr; } // x-audiocast header parameters are added and sent here if (fWantsMetaData) { // add the "icy-metaint:xxxx" parameter to the header. qtss_sprintf(buffer, "icy-metaint:%d\r\n", kClientMetaInt); ::strcat(fHeader, buffer); } len = ::strlen(fHeader); if (len > 0) { theErr = QTSS_Write(GetStreamRef(), fHeader, len, NULL, qtssWriteFlagsBufferData); if (theErr != QTSS_NoErr) { SetState(MP3ClientSession::kClientShutDownState); return theErr; } } // ...and send the phony content length or just a newline to mark end of header if (WantsContentLength()) theErr = QTSS_Write(GetStreamRef(), kClientCLHeader, ::strlen(kClientCLHeader), NULL, qtssWriteFlagsBufferData); else theErr = QTSS_Write(GetStreamRef(), "\r\n", 2, NULL, qtssWriteFlagsBufferData); if (theErr != QTSS_NoErr) { Assert(0); SetState(MP3ClientSession::kClientShutDownState); return theErr; } else { SetState(MP3ClientSession::kClientSendDataState); } // // Make sure the client session remains in FilterRequest server role. // We do this by scheduling ourself to be called back in // kClientPollInterval milliseconds. When called back again we will // continue to schedule ourself until we get into another // KeepSession((QTSS_RTSPRequestObject) GetStreamRef(), true); theErr = QTSS_SetIdleTimer(kClientPollInterval); if (theErr != QTSS_NoErr) { Assert(0); SetState(MP3ClientSession::kClientShutDownState); } } return theErr; } // Send the broadcast data stream to our client including meta-data // if needed. QTSS_Error MP3ClientSession::SendMP3Data(char* buffer, UInt32 bufferlen) { QTSS_Error theErr = QTSS_NoErr; UInt32 sendcount; UInt32 sendlen = 0L; UInt32 remaininglen = 0L; // get the broadcast sendcount into our local copy. sendcount = fSendCount; Assert(buffer != NULL); // We only send data if we are int the kClientSendDataState. if (GetState() != MP3ClientSession::kClientSendDataState) { return theErr; } // If we are using meta data then check and see if we hit our // meta-data send interval. // if so calculate the second write size. if (fWantsMetaData && ((sendcount + bufferlen) > kClientMetaInt)) { // sendlen is the amount of buffered data to send before meta data // is interposed. sendlen = (UInt32) kClientMetaInt - sendcount; // remainglen is the reaming amount of buffered data to send // after sending the meta-data. remaininglen = bufferlen - sendlen; } else { // either it's not time to send metadata or we are not // doing meta-data for this client. sendlen = bufferlen; remaininglen = 0L; } // Send up to sendlen bytes of data from the buffer. QTSS_StreamRef stream = GetStreamRef(); Assert(stream != NULL); Assert(sendlen <= bufferlen); theErr = QTSS_Write(stream, buffer, sendlen, NULL, qtssWriteFlagsBufferData); sendcount += sendlen; if (theErr != QTSS_NoErr) { SetState(MP3ClientSession::kClientShutDownState); return theErr; } // If there is meta-data this time then send it followed by remaininglen bytes // from the buffer. if (remaininglen != 0L) { // send the meta-data theErr = SendMetaData(); if (theErr != QTSS_NoErr) { SetState(MP3ClientSession::kClientShutDownState); return theErr; } // sendcount must be zero after sending meta-data. sendcount = 0; // send the remainder of the data buffer. theErr = QTSS_Write(GetStreamRef(), buffer+sendlen, remaininglen, NULL, qtssWriteFlagsBufferData); sendcount += remaininglen; if (theErr != QTSS_NoErr) { SetState(MP3ClientSession::kClientShutDownState); return theErr; } } fSendCount = sendcount; // Flush to perform the actuall send to the client. theErr = QTSS_Flush(GetStreamRef()); if (theErr == QTSS_WouldBlock) { // the client is flow controlled. SInt64 curTime = OS::Milliseconds(); fWasBlocked = true; QTSS_RequestEvent(GetStreamRef(), QTSS_WriteableEvent); DTRACE2("Got blocked at time %qd, numRetries = %"_S32BITARG_"\n", curTime, fRetryCount); if (fBlockTime == 0) fBlockTime = curTime; fRetryCount++; if (curTime - fBlockTime > sMaxFlowControlTimeInMSec) { SetResult(453); DTRACE1("MP3ClientSession::SendMP3Data - too many tries. Terminating client session = %"_S32BITARG_"\n", GetSessionID()); SetState(MP3ClientSession::kClientShutDownState); } } else if (theErr != QTSS_NoErr) { DTRACE1("MP3ClientSession::SendMP3Data - Terminating client session = %"_S32BITARG_"\n", GetSessionID()); SetState(MP3ClientSession::kClientShutDownState); } else { // we successfully sent data to the client. // fSendCount only gets incremented if the QTSS_Flush() was successful SetResult(200); fWasBlocked = false; fBlockTime = 0; fRetryCount = 0; } fBytesSent += bufferlen; // Increase the server's total MP3 byte count attribute IncrementTotalMP3Bytes(bufferlen); UpdateBitRateInternal(OS::Milliseconds()); return theErr; } // Retry to send previously blocked data. QTSS_Error MP3ClientSession::RetrySendData() { QTSS_Error theErr = QTSS_NoErr; // Flush to perform the actuall send to the client. theErr = QTSS_Flush(GetStreamRef()); if (theErr == QTSS_WouldBlock) { // the client is still flow controlled. fWasBlocked = true; QTSS_RequestEvent(GetStreamRef(), QTSS_WriteableEvent); return theErr; } else if (theErr != QTSS_NoErr) { // some error other than QTSS_WouldBlock occured. SetResult(454); SetState(MP3ClientSession::kClientShutDownState); return theErr; } else { // we successfully sent data to the client. // cancel future retries fWasBlocked = false; SetResult(200); fRetryCount = 0; } return QTSS_NoErr; } // Send the broadcast meta data stream to our client QTSS_Error MP3ClientSession::SendMetaData() { QTSS_Error theErr = QTSS_NoErr; char buffer[1024]; UInt16 bufferlen; // Don't allow an update of the song name while we're sending // it to the client. OSMutexLocker locker(&fSongNameMutex); // format the meta-data ::memset(buffer, 0, 1024); // Make sure we have something to send if (!fNewSongName || fSongName[0] == '\0') { // setup to write a single NULL byte. bufferlen = 1; } else { // Setup to write meta-data + pad NULL bytes. // first value in the buffer is the number of 16 byte chunks // to send. char tmp[512]; qtss_sprintf(tmp, "StreamTitle='%s';StreamUrl='';", fSongName); bufferlen = ::strlen(tmp); buffer[0] = (unsigned char)((bufferlen/16) + 1); ::strcat(buffer, tmp); bufferlen = (buffer[0]*16) + 1; } // send the meta-data theErr = QTSS_Write(GetStreamRef(), buffer, bufferlen, NULL, qtssWriteFlagsBufferData); if (theErr == QTSS_NoErr) { // meta data was written successfully fNewSongName = false; } fBytesSent += bufferlen; return theErr; } // Set the x-audiocast headers of the currently broadcasting client. // Lines that end in '\n' will have a '\r' prepended. void MP3ClientSession::SetHeader(char* header) { char *bp = header; char *cp = fHeader; char *end = fHeader + kHeaderBufferSize; while (cp < end) { if (*bp == '\0') { // end of header buffer string break; } // scan for newlines . if (*bp == '\n') { // add a CR before the newline *cp = '\r'; cp++; *cp = '\n'; } else { // otherwise just copy the byte *cp = *bp; } cp++; bp++; } // make sure we didn't go past the end of buffer. if (cp > end) cp = end; // terminate the buffer. *cp = '\0'; } // Set the song name of the currently broadcasting client. void MP3ClientSession::SetSongName(char* sn) { if (sn != NULL && *sn != '\0') { // This mutex is to guard against updating a // song name string while we're trying to send it // to a remote client. OSMutexLocker locker(&fSongNameMutex); ::strcpy(fSongName, sn); fNewSongName = true; } } void MP3ClientSession::UpdateBitRateInternal(const SInt64& curTime) { if (curTime < fLastBitRateUpdateTime + 10000) return; // update the bitrate every 10 seconds UInt32 bitsInInterval = (fBytesSent - fLastBitRateBytes) * 8; SInt64 updateTime = (curTime - fLastBitRateUpdateTime) / 1000; if (updateTime > 0) // leave Bit Rate the same if updateTime is 0 also don't divide by 0. fCurrentBitRate = (UInt32) ( bitsInInterval / updateTime ); fLastBitRateBytes = fBytesSent; fLastBitRateUpdateTime = curTime; } void MP3ClientSession::ParseRequestParams(QTSS_StreamRef stream) { QTSS_Error err = QTSS_NoErr; char *bp = NULL; unsigned int idx; // get a copy of the first line of the HTTP request QTSS_RTSPRequestObject theRequest = (QTSS_RTSPRequestObject) stream; StrPtrLen theFullRequest; if (theRequest != NULL) err = QTSS_GetValuePtr(theRequest, qtssRTSPReqFullRequest, 0, (void**)&theFullRequest.Ptr, &theFullRequest.Len); else err = QTSS_BadArgument; if (err == QTSS_NoErr) { // make sure we don't overflow our buffer if (theFullRequest.Len >= kRequestBufferSize) theFullRequest.Len = kRequestBufferSize-1; // copy the request into our private class buffer. ::memcpy(fRequestBuffer, theFullRequest.Ptr, theFullRequest.Len); // first see if the client wants to recieve meta-data fWantsMetaData = NeedsMetaData(theFullRequest); // Do we need to send a phony HTTP content length field? if (::strstr(fRequestBuffer, "MSIE") != NULL) fNeedsContentLength = true; else if (::strstr(fRequestBuffer, "RMA/1.0") != NULL) fNeedsContentLength = true; else if (::strstr(fRequestBuffer, "NSPlayer") != NULL) fNeedsContentLength = true; else fNeedsContentLength = false; // Get a copy ot the requestor's host name if it exists bp = ::strstr(fRequestBuffer, "Host:"); if (bp != NULL) { bp += ::strlen("Host:"); while (*bp == ' ') bp++; idx = 0; while (bp[idx]) { if (idx >= kHostNameBufferSize-1) break; if (bp[idx] == '\n' || bp[idx] == '\r') break; fHostName[idx] = bp[idx]; idx++; } } // Get a copy ot the requesting User Agent's name if it exists bp = ::strstr(fRequestBuffer, "User-Agent:"); if (bp != NULL) { bp += ::strlen("User-Agent:"); while (*bp == ' ') bp++; idx = 0; while (bp[idx]) { if (idx >= kUserAgentBufferSize-1) break; if (bp[idx] == '\n' || bp[idx] == '\r') break; fUserAgent[idx] = bp[idx]; idx++; } } // Now that we're done parsing the request we will // terminate first line of the request with '\0'. for (idx=0; idx < theFullRequest.Len; idx++) { if (fRequestBuffer[idx] == '\r' || fRequestBuffer[idx] == '\n') { fRequestBuffer[idx] = '\0'; break; } } } else { // dummy value so that we have something to log ::strcpy(fRequestBuffer, "GET /* HTTP/1.0"); } } // **************************************************************************** // MP3SessionRef -- This class is just a wrapper class for handling the // mapping of RTSP Session refs to the corresponding MP3 Session class refs. // It will be the an element of our lookup hash table. // **************************************************************************** MP3SessionRef::MP3SessionRef(MP3Session* mp3Session) : fNextHashEntry(NULL), fHashValue(0), fMP3Session(mp3Session) { } MP3SessionRef::~MP3SessionRef() { } // **************************************************************************** // MP3SessionRefKey -- This class is used to generate hash keys for looking // up values in our MP3SessionTable. The Hash key is just the RTSP Session // reference value. // **************************************************************************** MP3SessionRefKey::MP3SessionRefKey(MP3SessionRef* mp3SessRef) : fKeyValue(mp3SessRef) { fHashValue = 0L; if (fKeyValue != NULL) { fMP3Session = fKeyValue->GetMP3Session(); if (fMP3Session != NULL) fHashValue = (PointerSizedInt)fMP3Session->GetSession(); } else { fMP3Session = NULL; } } MP3SessionRefKey::MP3SessionRefKey(QTSS_RTSPSessionObject rtspSessRef) : fKeyValue(NULL), fHashValue((PointerSizedInt)rtspSessRef), fMP3Session(NULL) { } MP3SessionRefKey::~MP3SessionRefKey() { } // **************************************************************************** // MP3SessionTable -- This class provides a way to map RTSP Session references // into the corresponding MP3Session class instances if any. // **************************************************************************** MP3SessionTable::MP3SessionTable(UInt32 tableSize) : fTable(tableSize), fMutex() { } MP3SessionTable::~MP3SessionTable() { } // Attempt to add a new MP3Session ref to the table's map. // returns true on success or false if it fails. Bool16 MP3SessionTable::RegisterSession(MP3Session* session) { // sanity check if (session == NULL) { Assert(0); return false; } // construct a new entry for the table. MP3SessionRef* newEntry = NEW MP3SessionRef(session); if (newEntry == NULL) { return false; } // generate it's hash key. MP3SessionRefKey key(newEntry); // Lock the table while we try and add the new entry. OSMutexLocker locker(&fMutex); // check for a duplicate entry; MP3SessionRef* duplicateRef = fTable.Map(&key); if (duplicateRef != NULL) { delete newEntry; Assert(0); return false; } // add the new entry to the table. fTable.Add(newEntry); return true; } // Given an QTSS_RTSPSessionObject resolve it into a MP3Session class // reference. Returns NULL if there's none in out map. MP3Session* MP3SessionTable::Resolve(QTSS_RTSPSessionObject rtspSession) { // sanity check if (rtspSession == NULL) { Assert(0); return NULL; } // generate it's hash key. MP3SessionRefKey key(rtspSession); // Lock the table while we do our lookup. OSMutexLocker locker(&fMutex); // Look for the entry in the table's map; MP3SessionRef* entry = fTable.Map(&key); if (entry != NULL) { // We found it. return the entry's MP3Session ref. return entry->GetMP3Session(); } // it's not in the table. return NULL; } // Attempt to remove a MP3Session ref from the table's map. // returns true on success or false if it fails. Bool16 MP3SessionTable::UnRegisterSession(MP3Session* session) { // sanity check if (session == NULL) { Assert(0); return false; } // generate it's hash key. MP3SessionRef entryRef(session); MP3SessionRefKey key(&entryRef); // Lock the table while we try and remove the entry. OSMutexLocker locker(&fMutex); // look for the entry in the table's map; MP3SessionRef* entry = fTable.Map(&key); if (entry != NULL) { // We found it. Remove it from the table and delete the // reference instance created by RegisterSession(). fTable.Remove(entry); delete entry; return true; } // it's not in the table. return false; } // **************************************************************************** // MP3BroadcasterQueue -- This is a class manages a queue of MP3Broadcaster // objects. // **************************************************************************** MP3BroadcasterQueue::MP3BroadcasterQueue() { } MP3BroadcasterQueue::~MP3BroadcasterQueue() { } // Create a new MP3BroadcasterSession object and queue it. QTSS_Error MP3BroadcasterQueue::CreateBroadcaster(QTSS_RTSPSessionObject sess, QTSS_StreamRef stream, StrPtrLen& mountpt) { QTSS_Error theErr = QTSS_NoErr; UInt32 uSessID = 0L; if (sess == 0 || stream == 0) { return QTSS_BadArgument; } if ((fQueue.GetLength() > 0) && (FindByMountPoint(mountpt) != NULL)) { theErr = QTSS_Write(stream, kSourceReject, ::strlen(kSourceReject), NULL, qtssWriteFlagsBufferData); if (theErr != QTSS_NoErr) { return theErr; } theErr = QTSS_Flush(stream); DTRACE("MP3BroadcasterQueue::CreateBroadcaster - duplicate mountpoint\n"); return QTSS_BadArgument; } if ( (fQueue.GetLength() > 0) && InQueue(sess) ) { theErr = QTSS_Write(stream, kSourceBadPassword, ::strlen(kSourceBadPassword), NULL, qtssWriteFlagsBufferData); if (theErr != QTSS_NoErr) { return theErr; } theErr = QTSS_Flush(stream); DTRACE("MP3BroadcasterQueue::CreateBroadcaster - duplicate session\n"); return QTSS_BadArgument; } MP3BroadcasterSession* broadcaster = NEW MP3BroadcasterSession(sess, stream); if (broadcaster == NULL) { return QTSS_NotEnoughSpace; } uSessID = GetRTSPSessionID(sess); broadcaster->SetSessionID(uSessID); broadcaster->SetMountpoint(mountpt); broadcaster->AcceptPasswordState(); OSQueueElem* elem = NEW OSQueueElem(broadcaster); if (elem != 0) { OSMutexLocker locker(&fMutex); fQueue.EnQueue(elem); DTRACE1("MP3BroadcasterQueue::CreateBroadcaster() Session(%"_S32BITARG_") broadcaster added!\n", uSessID); // send the "OK" back to the broadcaster theErr = broadcaster->SendOKResponse(); // If we failed to send the "OK" probably becuase of flow control // the only thing to do is to fail the attempt. if (theErr != QTSS_NoErr) broadcaster->ShutDownState(); } else { delete broadcaster; broadcaster = NULL; theErr = QTSS_NotEnoughSpace; } return theErr; } // Dequeue a MP3BroadcasterSession object and delete it. QTSS_Error MP3BroadcasterQueue::RemoveBroadcaster(QTSS_RTSPSessionObject sess) { UInt32 uSessID = 0L; if (sess == 0) { return QTSS_BadArgument; } OSMutexLocker locker(&fMutex); OSQueueIter iter(&fQueue); OSQueueElem* elem = NULL; while ((elem = iter.GetCurrent()) != 0) { MP3BroadcasterSession* current = (MP3BroadcasterSession*) elem->GetEnclosingObject(); if ((current != 0) && (current->GetSession() == sess)) { elem->Remove(); uSessID = current->GetSessionID(); DTRACE1("MP3BroadcasterQueue::RemoveBroadcaster() Session(%"_S32BITARG_") broadcaster removed!\n", uSessID); delete elem; delete current; return QTSS_NoErr; } iter.Next(); } return QTSS_BadArgument; } // Find and dequeue a MP3ClientSession object and delete it. QTSS_Error MP3BroadcasterQueue::RemoveClient(QTSS_RTSPSessionObject sess) { if (sess == 0) { return QTSS_BadArgument; } OSMutexLocker locker(&fMutex); OSQueueIter iter(&fQueue); OSQueueElem* elem = NULL; while ((elem = iter.GetCurrent()) != 0) { MP3BroadcasterSession* current = (MP3BroadcasterSession*) elem->GetEnclosingObject(); if ((current != 0) && (current->IsMyClient(sess))) { return current->RemoveClient(sess); } iter.Next(); } return QTSS_BadArgument; } // See if a particular Broadcaster RTSP session is in our queue. Bool16 MP3BroadcasterQueue::InQueue(QTSS_RTSPSessionObject sess) { if (sess == 0) { return false; } OSMutexLocker locker(&fMutex); OSQueueIter iter(&fQueue); OSQueueElem* elem = NULL; while ((elem = iter.GetCurrent()) != 0) { MP3BroadcasterSession* current = (MP3BroadcasterSession*) elem->GetEnclosingObject(); if ((current != 0) && (current->GetSession() == sess)) { return true; } iter.Next(); } return false; } // See if a particular Client RTSP session is in any of our broadcasters' queues. Bool16 MP3BroadcasterQueue::IsActiveClient(QTSS_RTSPSessionObject sess) { if (sess == 0) { return false; } OSMutexLocker locker(&fMutex); OSQueueIter iter(&fQueue); OSQueueElem* elem = NULL; while ((elem = iter.GetCurrent()) != 0) { MP3BroadcasterSession* current = (MP3BroadcasterSession*) elem->GetEnclosingObject(); if ((current != 0) && (current->IsMyClient(sess))) { return true; } iter.Next(); } return false; } // Find a MP3BroadcasterSession in the queue by it's mountpoint. MP3BroadcasterSession* MP3BroadcasterQueue::FindByMountPoint(char* mountpoint) { if (mountpoint == NULL || mountpoint[0] == '\0') { return NULL; } OSMutexLocker locker(&fMutex); OSQueueIter iter(&fQueue); OSQueueElem* elem = NULL; while ((elem = iter.GetCurrent()) != 0) { MP3BroadcasterSession* current = (MP3BroadcasterSession*) elem->GetEnclosingObject(); if ((current != 0) && (current->MountpointEqual(mountpoint))) { return current; } iter.Next(); } return NULL; } // Find a MP3BroadcasterSession in the queue by it's mountpoint. MP3BroadcasterSession* MP3BroadcasterQueue::FindByMountPoint(StrPtrLen& mountpoint) { if (mountpoint.Ptr == NULL || mountpoint.Len == 0) { return NULL; } OSMutexLocker locker(&fMutex); OSQueueIter iter(&fQueue); OSQueueElem* elem = NULL; while ((elem = iter.GetCurrent()) != 0) { MP3BroadcasterSession* current = (MP3BroadcasterSession*) elem->GetEnclosingObject(); if ((current != 0) && (current->MountpointEqual(mountpoint))) { return current; } iter.Next(); } return NULL; } // Find a MP3BroadcasterSession in the queue by it's RTSP Session. MP3BroadcasterSession* MP3BroadcasterQueue::FindBySession(QTSS_RTSPSessionObject sess) { if (sess == 0) { return NULL; } OSMutexLocker locker(&fMutex); OSQueueIter iter(&fQueue); OSQueueElem* elem = NULL; while ((elem = iter.GetCurrent()) != 0) { MP3BroadcasterSession* current = (MP3BroadcasterSession*) elem->GetEnclosingObject(); if ((current != 0) && (current->GetSession() == sess)) { return current; } iter.Next(); } return NULL; } // Mark all broadcast sessions to be terminated when they are next called. void MP3BroadcasterQueue::TerminateAllBroadcastSessions() { OSMutexLocker locker(&fMutex); OSQueueElem* elem = NULL; OSQueueIter iter(&fQueue); while ((elem = iter.GetCurrent()) != NULL) { MP3BroadcasterSession* current = (MP3BroadcasterSession*) elem->GetEnclosingObject(); if (current != NULL) { current->SetState(MP3BroadcasterSession::kBroadcasterShutDownState); } iter.Next(); } } // **************************************************************************** // MP3ClientQueue -- This is a class maintains a queue of MP3Client objects. // **************************************************************************** MP3ClientQueue::MP3ClientQueue() { } MP3ClientQueue::~MP3ClientQueue() { // Make sure all queued client sessions are marked for termination since // since they are now orphaned and no longer have an owner. TerminateClients(); } // Create a new MP3ClientSession object and queue it. QTSS_Error MP3ClientQueue::AddClient(QTSS_RTSPSessionObject sess, QTSS_StreamRef stream, MP3BroadcasterSession* owner) { QTSS_Error theErr = QTSS_NoErr; UInt32 uSessID = 0L; if (sess == 0 || stream == 0) { return QTSS_BadArgument; } if ( InQueue (sess) ) { return QTSS_NoErr; } MP3ClientSession* client = NEW MP3ClientSession(sess, stream, owner); if (client == 0) { return QTSS_NotEnoughSpace; } uSessID = GetRTSPSessionID(sess); client->SetSessionID(uSessID); if (owner != NULL) { // Get a copy of the x-audiocast headers from the // broadcaster's cached copy. client->SetHeader(owner->GetHeader()); // Make sure we have a song name also client->SetSongName(owner->GetSongName()); } OSQueueElem* elem = NEW OSQueueElem(client); if (elem != 0) { OSMutexLocker locker(&fMutex); fQueue.EnQueue(elem); DTRACE1("MP3ClientQueue::AddClient() Session(%"_S32BITARG_") client added!\n", uSessID); // send the "OK" back to the broadcaster theErr = client->SendResponse(); } else { delete client; theErr = QTSS_NotEnoughSpace; } return theErr; } // Dequeue a MP3ClientSession object and delete it. QTSS_Error MP3ClientQueue::RemoveClient(QTSS_RTSPSessionObject sess) { UInt32 uSessID = 0L; if (sess == 0) { return QTSS_BadArgument; } OSMutexLocker locker(&fMutex); OSQueueIter iter(&fQueue); OSQueueElem* elem = NULL; while ((elem = iter.GetCurrent()) != 0) { MP3ClientSession* curClient = (MP3ClientSession*) elem->GetEnclosingObject(); if ((curClient != 0) && (curClient->GetSession() == sess)) { elem->Remove(); uSessID = curClient->GetSessionID(); DTRACE1("MP3ClientQueue::RemoveClient() Session(%"_S32BITARG_") client removed!\n", uSessID); delete elem; delete curClient; return QTSS_NoErr; } iter.Next(); } return QTSS_BadArgument; } // See if a particular Client RTSP session is in our queue. Bool16 MP3ClientQueue::InQueue(QTSS_RTSPSessionObject sess) { if (sess == 0) { return false; } OSMutexLocker locker(&fMutex); OSQueueIter iter(&fQueue); OSQueueElem* elem = NULL; while ((elem = iter.GetCurrent()) != 0) { MP3ClientSession* curClient = (MP3ClientSession*) elem->GetEnclosingObject(); if ((curClient != 0) && (curClient->GetSession() == sess)) { return true; } iter.Next(); } return false; } // Send MP3 data to all queued MP3ClientSession objects. QTSS_Error MP3ClientQueue::SendToAllClients(char* buffer, UInt32 bufferlen) { QTSS_Error theErr = QTSS_NoErr; OSMutexLocker locker(&fMutex); OSQueueElem* elem = NULL; OSQueueIter iter(&fQueue); while ((elem = iter.GetCurrent()) != NULL) { MP3ClientSession* curClient = (MP3ClientSession*) elem->GetEnclosingObject(); if (curClient != NULL) { QTSS_Error err; err = curClient->SendMP3Data(buffer, bufferlen); if (err != QTSS_NoErr) theErr = err; } iter.Next(); } return theErr; } // Update the current song name of queued MP3ClientSession objects. // and attempt to resend data thatwas previously blocked. void MP3ClientQueue::PreflightClients(char* sn) { QTSS_Error theErr = QTSS_NoErr; OSMutexLocker locker(&fMutex); OSQueueElem* elem = NULL; OSQueueIter iter(&fQueue); while ((elem = iter.GetCurrent()) != NULL) { MP3ClientSession* curClient = (MP3ClientSession*) elem->GetEnclosingObject(); if (curClient != NULL) { if (curClient->WasBlocked()) { theErr = curClient->RetrySendData(); } curClient->SetSongName(sn); } iter.Next(); } } // Mark all clients to be terminated when they are next polled. // This should be called by the MP3BroadcasterSession whenever it dies // with active client else we'll have a nasty memory leak and clients // will pause until they timeout. void MP3ClientQueue::TerminateClients() { OSMutexLocker locker(&fMutex); OSQueueElem* elem = NULL; OSQueueIter iter(&fQueue); while ((elem = iter.GetCurrent()) != NULL) { MP3ClientSession* curClient = (MP3ClientSession*) elem->GetEnclosingObject(); if (curClient != NULL) { curClient->SetOwner(NULL); curClient->SetState(MP3ClientSession::kClientShutDownState); } iter.Next(); } } // **************************************************************************** // MODULE FUNCTION IMPLEMENTATIONS // **************************************************************************** #if DEBUG_MP3STREAMING_MODULE void PrintStringBuffer(StrPtrLen& stringBuffer) { int len = (int)stringBuffer.Len; char localBuffer[256]; if (len > 255) len = 255; ::memcpy(localBuffer, stringBuffer.Ptr, len); localBuffer[len] = '\0'; qtss_printf("%s", localBuffer); } #endif inline void KeepSession(QTSS_RTSPRequestObject theRequest,Bool16 keep) { (void)QTSS_SetValue(theRequest, qtssRTSPReqRespKeepAlive, 0, &keep, sizeof(keep)); } QTSS_Error QTSSMP3StreamingModule_Main(void* inPrivateArgs) { return _stublibrary_main(inPrivateArgs, QTSSMP3StreamingModuleDispatch); } // QTSSMP3StreamingModuleDispatch - Dispatch all the API roles that this module handles QTSS_Error QTSSMP3StreamingModuleDispatch(QTSS_Role inRole, QTSS_RoleParamPtr inParams) { switch (inRole) { case QTSS_Register_Role: return Register(&inParams->regParams); case QTSS_Initialize_Role: return Initialize(&inParams->initParams); case QTSS_RTSPFilter_Role: return FilterRequest(&inParams->rtspFilterParams); case QTSS_RereadPrefs_Role: return RereadPrefs(); case QTSS_RTSPSessionClosing_Role: return SessionClosing(&inParams->rtspSessionClosingParams); case QTSS_StateChange_Role: return StateChange(&inParams->stateChangeParams); case QTSS_Shutdown_Role: return Shutdown(); } return QTSS_NoErr; } // Register - register all the API roles that this module handles QTSS_Error Register(QTSS_Register_Params* inParams) { // Do role setup (void)QTSS_AddRole(QTSS_Initialize_Role); (void)QTSS_AddRole(QTSS_RereadPrefs_Role); (void)QTSS_AddRole(QTSS_RTSPFilter_Role); (void)QTSS_AddRole(QTSS_RTSPSessionClosing_Role); (void)QTSS_AddRole(QTSS_StateChange_Role); (void)QTSS_AddRole(QTSS_Shutdown_Role); // Tell the server our name! static char* sModuleName = "QTSSMP3StreamingModule"; ::strcpy(inParams->outModuleName, sModuleName); return QTSS_NoErr; } QTSS_Error Initialize(QTSS_Initialize_Params* inParams) { // Setup module utils QTSSModuleUtils::Initialize(inParams->inMessages, inParams->inServer, inParams->inErrorLogStream); sServer = inParams->inServer; sServerPrefs = inParams->inPrefs; sPrefs = QTSSModuleUtils::GetModulePrefsObject(inParams->inModule); RereadPrefs(); sOSBufferPoolPtr = NEW OSBufferPool(sBroadcastBufferSize); sLogMutex = NEW OSMutex(); sAtomicMutex = NEW OSMutex(); // pre-format the standard HTTP reply headers qtss_sprintf(gClientAcceptHeader, kClientAcceptHeader, kVersionString, kBuildString); qtss_sprintf(gM3UReplyHeader, kM3UReplyHeader, kVersionString, kBuildString); sMP3AccessLog = NEW QTSSMP3AccessLog(); if (sMP3AccessLog != NULL && sLogEnabled) sMP3AccessLog->EnableLog(); WriteStartupMessage(); return QTSS_NoErr; } // RereadPrefs - refread our prefs from the prefs file. QTSS_Error RereadPrefs() { delete [] sDefaultLogDir; (void)QTSS_GetValueAsString(sServerPrefs, qtssPrefsErrorLogDir, 0, &sDefaultLogDir); delete [] sLogName; sLogName = QTSSModuleUtils::GetStringAttribute(sPrefs, "mp3_request_logfile_name", sDefaultLogName); delete [] sLogDir; sLogDir = QTSSModuleUtils::GetStringAttribute(sPrefs, "mp3_request_logfile_dir", sDefaultLogDir); QTSSModuleUtils::GetAttribute(sPrefs, "mp3_streaming_enabled", qtssAttrDataTypeBool16, &sMP3StreamingEnabled, &sDefaultMP3StreamingEnabled, sizeof(sMP3StreamingEnabled)); delete [] sBroadcastPassword; sBroadcastPassword = QTSSModuleUtils::GetStringAttribute(sPrefs, "mp3_broadcast_password", sDefaultBroadcastPassword); // if broadcast password is blank or empty don't allow MP3 streaming. if (sBroadcastPassword == NULL || *sBroadcastPassword == '\0' || *sBroadcastPassword == ' ') { sMP3StreamingEnabled = false; } QTSSModuleUtils::GetAttribute(sPrefs, "mp3_broadcast_buffer_size", qtssAttrDataTypeUInt32, &sBroadcastBufferSize, &sDefaultBroadcastBufferSize, sizeof(sBroadcastBufferSize)); QTSSModuleUtils::GetAttribute(sPrefs, "mp3_max_flow_control_time", qtssAttrDataTypeSInt32, &sMaxFlowControlTimeInMSec, &sDefaultFlowControlTimeInMSec, sizeof(sMaxFlowControlTimeInMSec)); QTSSModuleUtils::GetAttribute(sPrefs, "mp3_request_logging", qtssAttrDataTypeBool16, &sLogEnabled, &sDefaultLogEnabled, sizeof(sLogEnabled)); QTSSModuleUtils::GetAttribute(sPrefs, "mp3_request_logfile_size", qtssAttrDataTypeUInt32, &sMaxLogBytes, &sDefaultMaxLogBytes, sizeof(sMaxLogBytes)); QTSSModuleUtils::GetAttribute(sPrefs, "mp3_request_logfile_interval", qtssAttrDataTypeUInt32, &sRollInterval, &sDefaultRollInterval, sizeof(sRollInterval)); QTSSModuleUtils::GetAttribute(sPrefs, "mp3_request_logtime_in_gmt", qtssAttrDataTypeBool16, &sLogTimeInGMT, &sDefaultLogTimeInGMT, sizeof(sLogTimeInGMT)); UInt32 len = sizeof(SInt32); (void) QTSS_GetValue(sServerPrefs, qtssPrefsMaximumConnections, 0, (void*)&sMaximumConnections, &len); (void) QTSS_GetValue(sServerPrefs, qtssPrefsMaximumBandwidth, 0, (void*)&sMaximumBandwidth, &len); // handle changing the sLogEnabled state of the access log. if (sMP3AccessLog != NULL) { if (sLogEnabled) { if (!sMP3AccessLog->IsLogEnabled()) sMP3AccessLog->EnableLog(); } else { if (sMP3AccessLog->IsLogEnabled()) sMP3AccessLog->CloseLog(); } } return QTSS_NoErr; } // SessionClosing - clean up broadcaster/client connections. QTSS_Error SessionClosing(QTSS_RTSPSession_Params* inParams) { Assert(inParams != NULL); #if DEBUG_MP3STREAMING_MODULE UInt32 uSessID = 0L; uSessID = GetRTSPSessionID(inParams->inRTSPSession); DTRACE1("Closing SessionID = %"_S32BITARG_"\n", uSessID); #endif MP3Session* mp3sess = sMP3SessionTable.Resolve(inParams->inRTSPSession); // If Resolve() returns a non-NULL value then we are closing // the RTSP session related to either a reflected MP3 broadcast // session or we are closing an RTSP session related to an // MP3 client session. // // If Resolve() returns NULL this RTSP session does not belong to // this module and we just ignore it by returning QTSS_NoErr. if (mp3sess != NULL) { if (mp3sess->IsA() == kMP3BroadcasterSessionType) { // Remove the MP3BroadcasterSession from the global // server queue and delete its instance. return sMP3BroadcasterQueue.RemoveBroadcaster(inParams->inRTSPSession); } else if (mp3sess->IsA() == kMP3ClientSessionType) { MP3ClientSession* client = (MP3ClientSession*)mp3sess; MP3BroadcasterSession* owner = client->GetOwner(); if (sLogEnabled) LogRequest(inParams->inRTSPSession, client); if (owner != NULL) { // Let the the owner, a MP3BroadcasterSession, // remove the client from its queue and delete it. return owner->RemoveClient(inParams->inRTSPSession); } else { // otherwise, we no longer have an owner for this // client session and we just delete the instance. delete client; return QTSS_NoErr; } } } return QTSS_NoErr; } QTSS_Error Shutdown() { WriteShutdownMessage(); return QTSS_NoErr; } QTSS_Error StateChange(QTSS_StateChange_Params* stateChangeParams) { if (stateChangeParams->inNewState == qtssIdleState) { WriteShutdownMessage(); sMP3BroadcasterQueue.TerminateAllBroadcastSessions(); } else { WriteStartupMessage(); } return QTSS_NoErr; } // IncrementMP3SessionCount - This increments the MP3 session counter in the server's attributes void IncrementMP3SessionCount() { QTSS_Error err = QTSS_NoErr; // Increment the server's MP3 session count. UInt32* ptrValue = NULL; UInt32 valueLen = sizeof(ptrValue); OSMutexLocker locker(sAtomicMutex); err = QTSS_GetValuePtr(sServer, qtssMP3SvrCurConn, 0, (void**)&ptrValue, &valueLen); if ((err == QTSS_NoErr) && (ptrValue != NULL)) { *ptrValue += 1; } else { DTRACE1("QTSS_GetValuePtr() for qtssMP3SvrCurConn failed with err = %"_S32BITARG_"\n", (SInt32)err); } // bump the total sessions count up also err = QTSS_GetValuePtr(sServer, qtssMP3SvrTotalConn, 0, (void**)&ptrValue, &valueLen); if ((err == QTSS_NoErr) && (ptrValue != NULL)) { *ptrValue += 1; } else { DTRACE1("QTSS_GetValuePtr() for qtssMP3SvrTotalConn failed with err = %"_S32BITARG_"\n", (SInt32)err); } } // DecrementMP3SessionCount - This decrements the MP3 session counter in the server's attributes void DecrementMP3SessionCount() { QTSS_Error err = QTSS_NoErr; // Decrement the server's MP3 session count. UInt32* ptrValue = NULL; UInt32 valueLen = sizeof(ptrValue); err = QTSS_GetValuePtr(sServer, qtssMP3SvrCurConn, 0, (void**)&ptrValue, &valueLen); if ((err == QTSS_NoErr) && (ptrValue != NULL)) { OSMutexLocker locker(sAtomicMutex); *ptrValue -= 1; } else { DTRACE1("QTSS_GetValuePtr() for qtssMP3SvrCurConn failed with err = %"_S32BITARG_"\n", (SInt32)err); } } // IncrementTotalMP3Bytes - This increments the MP3 byte counter in the server's attributes void IncrementTotalMP3Bytes(UInt32 bytes) { QTSS_Error err = QTSS_NoErr; // Increment the server's MP3 total byte count. UInt64* ptrValue = NULL; UInt32 valueLen = sizeof(ptrValue); err = QTSS_GetValuePtr(sServer, qtssMP3SvrTotalBytes, 0, (void**)&ptrValue, &valueLen); if ((err == QTSS_NoErr) && (ptrValue != NULL)) { OSMutexLocker locker(sAtomicMutex); *ptrValue += bytes; } else { DTRACE1("QTSS_GetValuePtr() for qtssMP3SvrTotalBytes failed with err = %"_S32BITARG_"\n", (SInt32)err); } } // CheckBandwidth - Check and see if we can handle any more bandwidth. Bool16 CheckBandwidth(SInt32 bandwidth) { QTSS_Error err = QTSS_NoErr; // Get our current bandwidth and check against server's maximum. SInt32* ptrValue = NULL; UInt32 valueLen = sizeof(ptrValue); err = QTSS_GetValuePtr(sServer, qtssMP3SvrCurBandwidth, 0, (void**)&ptrValue, &valueLen); if ((err == QTSS_NoErr) && (ptrValue != NULL)) { OSMutexLocker locker(sAtomicMutex); if (((*ptrValue + bandwidth)/1000) <= sMaximumBandwidth) return true; } else { DTRACE1("QTSS_GetValuePtr() for qtssMP3SvrCurBandwidth failed with err = %"_S32BITARG_"\n", (SInt32)err); } return false; } // GetRTSPSessionID - returns the RTSP Session ID for the given RTSPSession object. // return 0 on failure. UInt32 GetRTSPSessionID(QTSS_RTSPSessionObject session) { UInt32 uSessID = 0L; UInt32 paramLen = sizeof(uSessID); QTSS_Error err = QTSS_GetValue(session, qtssRTSPSesID, 0, (void*)&uSessID, ¶mLen); if (err != QTSS_NoErr) { uSessID = 0L; } return uSessID; } // IsActiveBroadcastSession - This is true when the specified session belongs to // a current broadcast session. We use this to let us know if we were called back // in our FilterRequest() role because a read or write was blocked previously. inline Bool16 IsActiveBroadcastSession(QTSS_RTSPSessionObject session) { return (FindBroadcastSession(session) != NULL); } // FindBroadcastSession - Given a RTSP session find the MP3BroadcasterSession // object that handles this session. returns NULL pointer if none. MP3BroadcasterSession* FindBroadcastSession(QTSS_RTSPSessionObject session) { MP3Session* mp3sess = sMP3SessionTable.Resolve(session); if (mp3sess != NULL && mp3sess->IsA() == kMP3BroadcasterSessionType) return (MP3BroadcasterSession*) mp3sess; else return NULL; } // This determines if an incoming request is an broadcaster sending // us his password. It will be in the format of: // "SOURCE \n". Bool16 IsBroadcastPassword(StrPtrLen& theRequest) { if (IsShoutcastPassword(theRequest)) return true; StrPtrLen token = theRequest; token.Len = 6; return token.Equal(StrPtrLen("SOURCE")); } // This determines if an incoming request is a Shoutcast broadcaster sending // us his password. It will be in the format of: // \n". Bool16 IsShoutcastPassword(StrPtrLen& theRequest) { char* bp = (char*)theRequest.Ptr; int i; int n = (int)theRequest.Len; for (i=0; i 0) do { lastWord = currentWord; lineParse.ConsumeUntilWhitespace(¤tWord); lineParse.ConsumeWhitespace(); } while (currentWord.Len > 0); if (IsProtocolString(lastWord, protocol))// test for line ending with protocol/version as in a request from the client return true; return false; } Bool16 IsHTTP(StrPtrLen& theRequest) { return IsProtocol(theRequest, "HTTP"); } Bool16 IsRTSP(StrPtrLen& theRequest) { return IsProtocol(theRequest, "RTSP"); } // This determines if an incoming request is an HTTP GET // request. Bool16 IsHTTPGet(StrPtrLen& theRequest) { StrPtrLen token = theRequest; token.Len = 3; Bool16 found = false; if (token.EqualIgnoreCase(StrPtrLen("GET")) && IsHTTP(theRequest) ) found = true; return found; } // Is this URL a *.m3u file request. Bool16 IsA_m3u_URL(char* theURL) { if (::strstr(theURL, ".m3u") != 0) return true; return false; } // Is this URL a metdata xfer. Bool16 IsMetaDataURL(char* theURL) { if (::strncmp(theURL, "/admin.cgi?mode=updinfo", 23) == 0) return true; return false; } // Parse this URL and extract metdata. void ParseMetaDataURL(char* theURL) { char URLBuffer[kURLBufferSize]; if (theURL == NULL) { Assert(0); return; } else if (::strlen(theURL) == 0) { Assert(0); return; } DTRACE1("##ParseMetaDataURL() - meta-data URL <%s>\n", theURL); StrPtrLen urlPtr(theURL); StrPtrLen token; StringParser urlParser(&urlPtr); urlParser.ConsumeUntil(&token, '&'); if ( !token.Equal(StrPtrLen("/admin.cgi?mode=updinfo")) ) { // expected '/admin.cgi?mode=updinfo' in URL! Assert(0); return; } if ( !urlParser.Expect('&') ) { // expected '&' in URL after '/admin.cgi?mode=updinfo' Assert(0); return; } urlParser.ConsumeUntil(&token, '='); if ( !token.Equal(StrPtrLen("pass")) ) { // expected 'pass=' in URL! Assert(0); return; } if ( !urlParser.Expect('=') ) { // expected '=' in URL after 'pass'! Assert(0); return; } urlParser.ConsumeUntil(&token, '&'); if ( !token.Equal((const char*)sBroadcastPassword) ) { DTRACE1("##ParseMetaDataURL() - expected Password of '%s' in URL!\n", sBroadcastPassword); return; } else { if ( !urlParser.Expect('&') ) { // expected '&' in URL after 'pass=xxx'! Assert(0); return; } } urlParser.ConsumeUntil(&token, '='); if ( !token.Equal(StrPtrLen("mount")) ) { // expected 'mount=' in URL! Assert(0); return; } if ( !urlParser.Expect('=') ) { // expected '=' in URL after 'mount'! Assert(0); return; } urlParser.ConsumeUntil(&token, '&'); if ( token.Len < 1 ) { // expected mountpoint in URL! Assert(0); return; } else { if ( !urlParser.Expect('&') ) { // expected '&' in URL after 'mount=xxx'! Assert(0); return; } } ::memset(URLBuffer, 0, kURLBufferSize); ::memcpy(URLBuffer, token.Ptr, token.Len); DTRACE1("##ParseMetaDataURL() - got mountpoint of '%s' in URL.\n", URLBuffer); MP3BroadcasterSession* xs = sMP3BroadcasterQueue.FindByMountPoint(token); if (xs == NULL) { DTRACE("##ParseMetaDataURL() - no matching mountpoint found!\n"); return; } urlParser.ConsumeUntil(&token, '='); if ( !token.Equal(StrPtrLen("song")) ) { // expected 'song=' in URL! Assert(0); return; } if ( !urlParser.Expect('=') ) { // expected '=' in URL after 'song'! Assert(0); return; } xs->SetSongName(urlParser.GetCurrentPosition()); } void SendBroadcastAuthErr(QTSS_Filter_Params* inParams, char * errormessage) { if (errormessage != NULL) { UInt32 len = ::strlen(errormessage); (void)QTSS_Write(inParams->inRTSPRequest,errormessage ,len, NULL, 0); (void)QTSS_SetValue(inParams->inRTSPRequest, qtssRTSPReqRespMsg, 0, errormessage, len); } QTSS_SessionStatusCode inStatusCode = qtssClientUnAuthorized; (void)QTSS_SetValue(inParams->inRTSPRequest, qtssRTSPReqStatusCode, 0, &inStatusCode, sizeof(inStatusCode)); const Bool16 sFalse = false; (void)QTSS_SetValue(inParams->inRTSPRequest, qtssRTSPReqRespKeepAlive, 0, &sFalse, sizeof(sFalse)); } // This parses and checks the password in the broadcaster's // incoming request. Bool16 CheckPassword(QTSS_Filter_Params* inParams, StrPtrLen& theRequest, StrPtrLen& mountpoint) { Bool16 passwordOK = false; StrPtrLen strPtr; mountpoint.Ptr = NULL; mountpoint.Len = 0; StringParser reqParse(&theRequest); reqParse.ConsumeUntilWhitespace(&strPtr); char* tmp; if ( strPtr.Equal(StrPtrLen("SOURCE")) ) { //it's an x-audio broadcast request // PARSE X-AUDIO HERE!!!! reqParse.ConsumeWhitespace(); reqParse.ConsumeUntilWhitespace(&strPtr); tmp = strPtr.GetAsCString(); // Let's see if the password matches... DTRACE1("CheckPassword() - #Got X-AUDIO password: %s\n", tmp); if (strcmp(tmp, sBroadcastPassword) != 0) { SendBroadcastAuthErr(inParams, "ERROR - Bad Password\r\n"); //ice cast style error delete[] tmp; return false; } delete[] tmp; // get the mount point reqParse.ConsumeWhitespace(); reqParse.ConsumeUntilWhitespace(&strPtr); mountpoint.Ptr = strPtr.Ptr; mountpoint.Len = strPtr.Len; tmp = strPtr.GetAsCString(); DTRACE1("CheckPassword() - #Got X-AUDIO mountpoint: %s\n", tmp); delete[] tmp; passwordOK = true; } else if ( IsShoutcastPassword(strPtr) ) { tmp = strPtr.GetAsCString(); if (strPtr.Len > 1) tmp[strPtr.Len] = '\0'; // Let's see if the password matches... DTRACE1("CheckPassword() - #Got Shoutcast password: %s\n", tmp); if (strcmp(tmp, sBroadcastPassword) != 0) { SendBroadcastAuthErr(inParams, "invalid password\r\n"); // shoutcast style error delete[] tmp; return false; } delete[] tmp; mountpoint.Ptr = "/"; mountpoint.Len = 1; passwordOK = true; } return passwordOK; } Bool16 GetHeaderAndValueFromeLine(StrPtrLen *line, StrPtrLen* header, char separator, StrPtrLen* value) { StringParser lineParser(line); lineParser.ConsumeWhitespace(); Bool16 foundHeader = lineParser.GetThru(header, separator); lineParser.ConsumeWhitespace(); lineParser.GetThruEOL(value); return foundHeader; } // Find out if the HTTP header wants meta-data. Bool16 NeedsMetaData(StrPtrLen& theRequest) { StrPtrLen line; StrPtrLen header; StrPtrLen value; StringParser requestParser(&theRequest); requestParser.GetThruEOL(NULL); // strip off command line while ( requestParser.GetThruEOL(&line) || line.Len > 0 ) // pull out each line { if (!GetHeaderAndValueFromeLine(&line, &header, ':', &value)) return false; // no separator found if (header.EqualIgnoreCase("icy-metadata") && (StringParser(&value).ConsumeInteger() > 0) ) return true; // icy-metadata: (non zero value) } return false; } /* NeedsMetaData Test cases StrPtrLen temp; temp.Set("\r\nIcy-metadatA: 1\r"); if ( NeedsMetaData(temp) ) printf("icy-metadata: 1-1\n"); temp.Set("\ricy-metadata: 1\r"); if ( NeedsMetaData(temp) ) printf("icy-metadata: 1-2\n"); temp.Set("\nicy-metadata: 1\r"); if ( NeedsMetaData(temp) ) printf("icy-metadata: 1 -3\n"); temp.Set("\r\nicy-metadata:1\r"); if ( NeedsMetaData(temp) ) printf("icy-metadata:1-4\n"); temp.Set("\r\nicy-metadata:\r"); if ( NeedsMetaData(temp) ) printf("icy-metadata:-5\n"); temp.Set("\r\nicy-metadata 1\n"); if ( NeedsMetaData(temp) ) printf("icy-metadata 1-6\n"); temp.Set("\r\nicy-metadata: 0\r"); if ( NeedsMetaData(temp) ) printf("icy-metadata: 0-7\n"); temp.Set("\r\nicy-metadata: 3\r"); if ( NeedsMetaData(temp) ) printf("icy-metadata: 3-8\n"); temp.Set("\r\nIcy-metadatA: 1"); if ( NeedsMetaData(temp) ) printf("icy-metadata: 1-0\n"); temp.Set("\nIcy-metadatA: 1"); if ( NeedsMetaData(temp) ) printf("icy-metadata: 1-10\n"); */ // Parse out the URL from the HTTP GET line. Bool16 ParseURL(StrPtrLen& theRequest, char* outURL, UInt16 maxlen) { StringParser reqParse(&theRequest); StrPtrLen strPtr; ::memset(outURL, 0, maxlen); reqParse.ConsumeWord(&strPtr); if ( !strPtr.Equal(StrPtrLen("GET")) ) { return false; } reqParse.ConsumeWhitespace(); reqParse.ConsumeUntilWhitespace(&strPtr); if (strPtr.Len == 0) return false; else if ((UInt16)strPtr.Len > maxlen-1) strPtr.Len = maxlen-1; ::memcpy(outURL, strPtr.Ptr, strPtr.Len); return true; } void WriteStartupMessage() { if (!sServerIdle) return; sServerIdle = false; //format a date for the startup time char theDateBuffer[QTSSRollingLog::kMaxDateBufferSizeInBytes]; Bool16 result = QTSSRollingLog::FormatDate(theDateBuffer, false); char tempBuffer[1024]; if (result) qtss_sprintf(tempBuffer, "#Remark: Streaming beginning STARTUP %s\n", theDateBuffer); // log startup message to error log as well. if ((result) && (sMP3AccessLog != NULL)) sMP3AccessLog->WriteToLog(tempBuffer, kAllowLogToRoll); } void WriteShutdownMessage() { if (sServerIdle) return; sServerIdle = true; //log shutdown message //format a date for the shutdown time char theDateBuffer[QTSSRollingLog::kMaxDateBufferSizeInBytes]; Bool16 result = QTSSRollingLog::FormatDate(theDateBuffer, false); char tempBuffer[1024]; if (result) qtss_sprintf(tempBuffer, "#Remark: Streaming beginning SHUTDOWN %s\n", theDateBuffer); if ( result && sMP3AccessLog != NULL ) sMP3AccessLog->WriteToLog(tempBuffer, kAllowLogToRoll); } QTSS_Error LogRequest(QTSS_RTSPSessionObject inRTSPSession, MP3ClientSession* client) { char logbuffer[1024]; char reqBuffer[kRequestBufferSize]; char userAgent[kUserAgentBufferSize]; char theDateBuffer[QTSSRollingLog::kMaxDateBufferSizeInBytes]; UInt16 reqResult = 200; UInt32 byteCount = 54000000; UInt32 duration = 0; QTSS_Error theError = QTSS_NoErr; // Sanity check... if (sMP3AccessLog == NULL) return QTSS_NoErr; // LogRequest() is not re-entrant. Hold a mutex until done. OSMutexLocker locker(sLogMutex); // Construct the timestamp for the entry Bool16 result = QTSSRollingLog::FormatDate(theDateBuffer, sLogTimeInGMT); // this should never happen, but just in case... if (!result) theDateBuffer[0] = '\0'; // Get the IP address of the client who made the request char remoteAddress[kRemoteAddressSize] = {0}; StrPtrLen theClientIPAddressStr; theClientIPAddressStr.Set(remoteAddress,kRemoteAddressSize); QTSS_Error err = QTSS_GetValue( inRTSPSession, qtssRTSPSesRemoteAddrStr, 0, (void*)theClientIPAddressStr.Ptr, &theClientIPAddressStr.Len); if (err != QTSS_NoErr) ::strcpy(remoteAddress, "127.0.0.1"); // Pull the actual HTTP request line from the MP3 client session. ::strcpy(reqBuffer, client->GetRequest()); // Get the user agent's name or "unknown user agent" if none was specified // in the original HTTP request. userAgent[0] = '\0'; ::strcpy(userAgent, client->GetUserAgent()); if (userAgent[0] == '\0') ::strcpy(userAgent, "unknown user agent"); // Determine how may bytes this client was streamed. byteCount = client->GetTotalCount(); // Calculate the total connect duration in seconds. duration = (UInt32)((OS::Milliseconds() - client->GetConnectTime())/1000); // Finally, get the HTTP result code. reqResult = client->GetResult(); // Format the access log entry parameters here... qtss_sprintf(logbuffer, "%s \"%s\" [%s] \"%s\" %d %"_S32BITARG_" %"_S32BITARG_"\n", remoteAddress, userAgent, theDateBuffer, reqBuffer, reqResult, byteCount, duration ); // Commit it to disk... sMP3AccessLog->WriteToLog(logbuffer, kAllowLogToRoll); return theError; } // url_strcpy - works like strcpy except that it handles URL escape // conversions as it copies the string. void url_strcpy(char* dest, const char* src) { int c1, c2; while(*src) { if (*src == '%') { src++; c1 = *src++; if (c1 >= '0' && c1 <= '9') c1 -= '0'; else if (c1 >= 'A' && c1 <= 'F') c1 -= 'A' + 10; else if (c1 >= 'a' && c1 <= 'f') c1 -= 'a' + 10; else c1 = 0; c2 = *src; if (c2 >= '0' && c2 <= '9') c2 -= '0'; else if (c2 >= 'A' && c2 <= 'F') c2 -= 'A' + 10; else if (c2 >= 'a' && c2 <= 'f') c2 -= 'a' + 10; else c2 = 32; *dest = (c1 * 16) + c2; } else { *dest = *src; } // we need to replace single quotes with // a tick so that the client can parse meta-data. if (*dest == '\'') { *dest = '`'; } dest++; src++; } *dest = '\0'; } // ReEnterFilterRequest - This function is called whenever we // have called back our module in the QTSS_FilterRequest role // after calling it the first time. // This will happen in one of two ways: // 1. A MP3BroadcasterSession posted a read request event and data // became available on its stream. // 2. The Idle timer called us back to poll a MP3ClientSession to // see if it needs to die. QTSS_Error ReEnterFilterRequest(QTSS_Filter_Params* inParams, MP3Session* mp3Session) { QTSS_Error err = QTSS_NoErr; QTSS_RTSPRequestObject theRequest = inParams->inRTSPRequest; if (mp3Session != NULL) { UInt8 sessTyp = mp3Session->IsA(); switch(sessTyp) { case kMP3BroadcasterSessionType: // this is a MP3 broadcaster session. Invoke it's state machine. return ((MP3BroadcasterSession*)mp3Session)->ExecuteState(); case kMP3ClientSessionType: // this is a MP3 client session. If we are active schedule ourself // to be called back again in kClientPollInterval milliseconds. if (mp3Session->GetState() == MP3ClientSession::kClientSendDataState) { MP3ClientSession* client = (MP3ClientSession*)mp3Session; if (client->WasBlocked()) client->RetrySendData(); KeepSession(theRequest, true); err = QTSS_SetIdleTimer(kClientPollInterval); return err; } else { KeepSession(theRequest, false); } return QTSS_NoErr; default: Assert(0); return QTSS_NoErr; } } return err; } // FilterRequest - filter the incoming HTTP/Broadcast request. QTSS_Error FilterRequest(QTSS_Filter_Params* inParams) { QTSS_Error err = QTSS_NoErr; QTSS_RTSPRequestObject theRequest = inParams->inRTSPRequest; // Pull the actual request data from this RTSP session's attributes. StrPtrLen theFullRequest; err = QTSS_GetValuePtr(theRequest, qtssRTSPReqFullRequest, 0, (void**)&theFullRequest.Ptr, &theFullRequest.Len); if (err != QTSS_NoErr) { return QTSS_NoErr; } if (IsRTSP(theFullRequest)) // don't process rtsp requests return QTSS_NoErr; if (!sMP3StreamingEnabled || sServerIdle) { // This allows MP3 streaming to be disabled through a pref setting // or ignore requests when the server is in the idle state. if (NeedsMetaData(theFullRequest)) // this is an HTTP icy request return QTSSModuleUtils::SendHTTPErrorResponse(theRequest,qtssServerUnavailable,true, NULL); return QTSS_NoErr; } // See if this RTSP session is an existing MP3 Session. // If it is then we have re-entered FilterRequest() from a previous // invokation and we will call ReEnterFilterRequest() to do its thing. MP3Session* mp3Session = sMP3SessionTable.Resolve(inParams->inRTSPSession); if (mp3Session != NULL) { err = ReEnterFilterRequest(inParams, mp3Session); return err; } // Check and see if this request is a broadcaster sending it's password or // an HTTP GET. If it's not, it's assumed to be a RTSP request and we ignore // it. In that case, some other module must handle this request. #if DEBUG_MP3STREAMING_MODULE DTRACE("####\n"); PrintStringBuffer(theFullRequest); #endif if (!IsBroadcastPassword(theFullRequest) && !IsHTTPGet(theFullRequest)) { return QTSS_NoErr; } MP3BroadcasterSession* xs = NULL; #if DEBUG_MP3STREAMING_MODULE // Debugging info - Retrieve the RTSP session ID associated with this session. UInt32 uSessID = 0L; uSessID = GetRTSPSessionID(inParams->inRTSPSession); DTRACE1("####\n###Filtering SessionID = %"_S32BITARG_"\n", uSessID); #endif // See if we have exceeded our maximum number of client connections if (MP3Session::GetTotalNumMP3Sessions() >= sMaximumConnections) { // we are currently at our connection limit. return a 400 error for compatibility with icecast style error. return QTSSModuleUtils::SendHTTPErrorResponse(theRequest,qtssClientBadRequest,true, NULL); } else if (!CheckBandwidth(kBandwidthToAddEstimate)) { // we are currently at our bandwidth limit. return a 400 error for compatibility with icecast style error. return QTSSModuleUtils::SendHTTPErrorResponse(theRequest,qtssClientBadRequest,true, NULL); } StrPtrLen mountpoint; if (!IsHTTP(theFullRequest) && IsBroadcastPassword(theFullRequest) && CheckPassword(inParams, theFullRequest, mountpoint)) { err = sMP3BroadcasterQueue.CreateBroadcaster(inParams->inRTSPSession, inParams->inRTSPRequest, mountpoint); if (err != QTSS_NoErr) { // CreateBroadcaster returned an error but it has already sent the reject reply // so there's nothing left to do. return QTSS_NoErr; } // If we're here this is a new connection request from a broadcaster. // Read the broadcast headers from the broadcaster xs = FindBroadcastSession(inParams->inRTSPSession); if (xs != NULL) err = xs->GetBroadcastHeaders(); else err = QTSS_NoErr; return err; } else if ( IsHTTPGet(theFullRequest) ) { char theURL[kURLBufferSize]; MP3BroadcasterSession* xs = NULL; if (!ParseURL(theFullRequest, theURL, kURLBufferSize)) { return QTSS_NoErr; } // if this was a '*.m3u' file URL then handle it here // sendind back a reply containing the URL. // This effectively accomplishes a redirect. if ( IsA_m3u_URL(theURL) ) { char tmpbuf[1024]; tmpbuf[sizeof(tmpbuf) -1] = 0; char tmp[1024]; tmp[sizeof(tmp) -1] = 0; // remove the '.m3u' prefix from the URL UInt32 ulen = ::strlen(theURL) - 4; UInt32 serverIPAddr; UInt8 x1, x2, x3, x4; theURL[ulen] = '\0'; // Get the server IP address for building the reply playlist path ulen = sizeof(serverIPAddr); err = QTSS_GetValue(sServer, qtssSvrDefaultIPAddr, 0, &serverIPAddr, &ulen); if (err == QTSS_NoErr) { x1 = (UInt8)((serverIPAddr >> 24) & 0xff); x2 = (UInt8)((serverIPAddr >> 16) & 0xff); x3 = (UInt8)((serverIPAddr >> 8) & 0xff); x4 = (UInt8)((serverIPAddr) & 0xff); } else { x1 = 127; x2 = x3 = 0; x4 = 1; } // construct the reply string for the client. qtss_snprintf(tmp,sizeof(tmp) -1, "http://%d.%d.%d.%d:8000%s", x1,x2,x3,x4, theURL); ulen = ::strlen(tmp); qtss_snprintf(tmpbuf,sizeof(tmpbuf) -1, "%s %"_U32BITARG_"\r\n\r\n%s\r\n", gM3UReplyHeader, ulen, tmp); ulen = ::strlen(tmpbuf); // send the reply to the client. err = QTSS_Write(inParams->inRTSPRequest, tmpbuf, ulen, NULL, qtssWriteFlagsBufferData); return QTSS_NoErr; } // if this was a meta-data transfer URL then handle it here if ( IsMetaDataURL(theURL) ) { // Parse meta-data request here ParseMetaDataURL(theURL); // tell the broadcaster we got it. err = QTSS_Write(inParams->inRTSPRequest, kOKHeader, ::strlen(kOKHeader), NULL, qtssWriteFlagsBufferData); return QTSS_NoErr; } // Since this URL was not a metadata transfer let's assume it is // a broadcast mount point and search for it. xs = sMP3BroadcasterQueue.FindByMountPoint(theURL); if (xs == NULL) { if (NeedsMetaData(theFullRequest)) // this is an HTTP icy request return QTSSModuleUtils::SendHTTPErrorResponse(theRequest,qtssClientNotFound,true, NULL); // Let some other module handle this URL. return QTSS_NoErr; } // if we're here this is a new MP3 connection request from a client. // Start the clients stream up... err = xs->AddClient(inParams->inRTSPSession, inParams->inRTSPRequest); return err; } return QTSS_NoErr; }