| /********** |
| This library is free software; you can redistribute it and/or modify it under |
| the terms of the GNU Lesser General Public License as published by the |
| Free Software Foundation; either version 3 of the License, or (at your |
| option) any later version. (See <http://www.gnu.org/copyleft/lesser.html>.) |
| |
| This library is distributed in the hope that it will be useful, but WITHOUT |
| ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for |
| more details. |
| |
| You should have received a copy of the GNU Lesser General Public License |
| along with this library; if not, write to the Free Software Foundation, Inc., |
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
| **********/ |
| // "liveMedia" |
| // Copyright (c) 1996-2020 Live Networks, Inc. All rights reserved. |
| // A RTSP server |
| // Implementation |
| |
| #include "RTSPServer.hh" |
| #include "RTSPCommon.hh" |
| #include "RTSPRegisterSender.hh" |
| #include "Base64.hh" |
| #include <GroupsockHelper.hh> |
| |
| ////////// RTSPServer implementation ////////// |
| |
| RTSPServer* |
| RTSPServer::createNew(UsageEnvironment& env, Port ourPort, |
| UserAuthenticationDatabase* authDatabase, |
| unsigned reclamationSeconds) { |
| int ourSocket = setUpOurSocket(env, ourPort); |
| if (ourSocket == -1) return NULL; |
| |
| return new RTSPServer(env, ourSocket, ourPort, authDatabase, reclamationSeconds); |
| } |
| |
| Boolean RTSPServer::lookupByName(UsageEnvironment& env, |
| char const* name, |
| RTSPServer*& resultServer) { |
| resultServer = NULL; // unless we succeed |
| |
| Medium* medium; |
| if (!Medium::lookupByName(env, name, medium)) return False; |
| |
| if (!medium->isRTSPServer()) { |
| env.setResultMsg(name, " is not a RTSP server"); |
| return False; |
| } |
| |
| resultServer = (RTSPServer*)medium; |
| return True; |
| } |
| |
| char* RTSPServer |
| ::rtspURL(ServerMediaSession const* serverMediaSession, int clientSocket) const { |
| char* urlPrefix = rtspURLPrefix(clientSocket); |
| char const* sessionName = serverMediaSession->streamName(); |
| |
| char* resultURL = new char[strlen(urlPrefix) + strlen(sessionName) + 1]; |
| sprintf(resultURL, "%s%s", urlPrefix, sessionName); |
| |
| delete[] urlPrefix; |
| return resultURL; |
| } |
| |
| char* RTSPServer::rtspURLPrefix(int clientSocket) const { |
| struct sockaddr_in ourAddress; |
| if (clientSocket < 0) { |
| // Use our default IP address in the URL: |
| ourAddress.sin_addr.s_addr = ReceivingInterfaceAddr != 0 |
| ? ReceivingInterfaceAddr |
| : ourIPAddress(envir()); // hack |
| } else { |
| SOCKLEN_T namelen = sizeof ourAddress; |
| getsockname(clientSocket, (struct sockaddr*)&ourAddress, &namelen); |
| } |
| |
| char urlBuffer[100]; // more than big enough for "rtsp://<ip-address>:<port>/" |
| |
| portNumBits portNumHostOrder = ntohs(fServerPort.num()); |
| if (portNumHostOrder == 554 /* the default port number */) { |
| sprintf(urlBuffer, "rtsp://%s/", AddressString(ourAddress).val()); |
| } else { |
| sprintf(urlBuffer, "rtsp://%s:%hu/", |
| AddressString(ourAddress).val(), portNumHostOrder); |
| } |
| |
| return strDup(urlBuffer); |
| } |
| |
| UserAuthenticationDatabase* RTSPServer::setAuthenticationDatabase(UserAuthenticationDatabase* newDB) { |
| UserAuthenticationDatabase* oldDB = fAuthDB; |
| fAuthDB = newDB; |
| |
| return oldDB; |
| } |
| |
| Boolean RTSPServer::setUpTunnelingOverHTTP(Port httpPort) { |
| fHTTPServerSocket = setUpOurSocket(envir(), httpPort); |
| if (fHTTPServerSocket >= 0) { |
| fHTTPServerPort = httpPort; |
| envir().taskScheduler().turnOnBackgroundReadHandling(fHTTPServerSocket, |
| incomingConnectionHandlerHTTP, this); |
| return True; |
| } |
| |
| return False; |
| } |
| |
| portNumBits RTSPServer::httpServerPortNum() const { |
| return ntohs(fHTTPServerPort.num()); |
| } |
| |
| char const* RTSPServer::allowedCommandNames() { |
| return "OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER"; |
| } |
| |
| UserAuthenticationDatabase* RTSPServer::getAuthenticationDatabaseForCommand(char const* /*cmdName*/) { |
| // default implementation |
| return fAuthDB; |
| } |
| |
| Boolean RTSPServer::specialClientAccessCheck(int /*clientSocket*/, struct sockaddr_in& /*clientAddr*/, char const* /*urlSuffix*/) { |
| // default implementation |
| return True; |
| } |
| |
| Boolean RTSPServer::specialClientUserAccessCheck(int /*clientSocket*/, struct sockaddr_in& /*clientAddr*/, |
| char const* /*urlSuffix*/, char const * /*username*/) { |
| // default implementation; no further access restrictions: |
| return True; |
| } |
| |
| |
| RTSPServer::RTSPServer(UsageEnvironment& env, |
| int ourSocket, Port ourPort, |
| UserAuthenticationDatabase* authDatabase, |
| unsigned reclamationSeconds) |
| : GenericMediaServer(env, ourSocket, ourPort, reclamationSeconds), |
| fHTTPServerSocket(-1), fHTTPServerPort(0), |
| fClientConnectionsForHTTPTunneling(NULL), // will get created if needed |
| fTCPStreamingDatabase(HashTable::create(ONE_WORD_HASH_KEYS)), |
| fPendingRegisterOrDeregisterRequests(HashTable::create(ONE_WORD_HASH_KEYS)), |
| fRegisterOrDeregisterRequestCounter(0), fAuthDB(authDatabase), fAllowStreamingRTPOverTCP(True) { |
| } |
| |
| // A data structure that is used to implement "fTCPStreamingDatabase" |
| // (and the "noteTCPStreamingOnSocket()" and "stopTCPStreamingOnSocket()" member functions): |
| class streamingOverTCPRecord { |
| public: |
| streamingOverTCPRecord(u_int32_t sessionId, unsigned trackNum, streamingOverTCPRecord* next) |
| : fNext(next), fSessionId(sessionId), fTrackNum(trackNum) { |
| } |
| virtual ~streamingOverTCPRecord() { |
| delete fNext; |
| } |
| |
| streamingOverTCPRecord* fNext; |
| u_int32_t fSessionId; |
| unsigned fTrackNum; |
| }; |
| |
| RTSPServer::~RTSPServer() { |
| // Turn off background HTTP read handling (if any): |
| envir().taskScheduler().turnOffBackgroundReadHandling(fHTTPServerSocket); |
| ::closeSocket(fHTTPServerSocket); |
| |
| cleanup(); // Removes all "ClientSession" and "ClientConnection" objects, and their tables. |
| delete fClientConnectionsForHTTPTunneling; |
| |
| // Delete any pending REGISTER requests: |
| RTSPRegisterOrDeregisterSender* r; |
| while ((r = (RTSPRegisterOrDeregisterSender*)fPendingRegisterOrDeregisterRequests->getFirst()) != NULL) { |
| delete r; |
| } |
| delete fPendingRegisterOrDeregisterRequests; |
| |
| // Empty out and close "fTCPStreamingDatabase": |
| streamingOverTCPRecord* sotcp; |
| while ((sotcp = (streamingOverTCPRecord*)fTCPStreamingDatabase->getFirst()) != NULL) { |
| delete sotcp; |
| } |
| delete fTCPStreamingDatabase; |
| } |
| |
| Boolean RTSPServer::isRTSPServer() const { |
| return True; |
| } |
| |
| void RTSPServer::incomingConnectionHandlerHTTP(void* instance, int /*mask*/) { |
| RTSPServer* server = (RTSPServer*)instance; |
| server->incomingConnectionHandlerHTTP(); |
| } |
| void RTSPServer::incomingConnectionHandlerHTTP() { |
| incomingConnectionHandlerOnSocket(fHTTPServerSocket); |
| } |
| |
| void RTSPServer |
| ::noteTCPStreamingOnSocket(int socketNum, RTSPClientSession* clientSession, unsigned trackNum) { |
| streamingOverTCPRecord* sotcpCur |
| = (streamingOverTCPRecord*)fTCPStreamingDatabase->Lookup((char const*)socketNum); |
| streamingOverTCPRecord* sotcpNew |
| = new streamingOverTCPRecord(clientSession->fOurSessionId, trackNum, sotcpCur); |
| fTCPStreamingDatabase->Add((char const*)socketNum, sotcpNew); |
| } |
| |
| void RTSPServer |
| ::unnoteTCPStreamingOnSocket(int socketNum, RTSPClientSession* clientSession, unsigned trackNum) { |
| if (socketNum < 0) return; |
| streamingOverTCPRecord* sotcpHead |
| = (streamingOverTCPRecord*)fTCPStreamingDatabase->Lookup((char const*)socketNum); |
| if (sotcpHead == NULL) return; |
| |
| // Look for a record of the (session,track); remove it if found: |
| streamingOverTCPRecord* sotcp = sotcpHead; |
| streamingOverTCPRecord* sotcpPrev = sotcpHead; |
| do { |
| if (sotcp->fSessionId == clientSession->fOurSessionId && sotcp->fTrackNum == trackNum) break; |
| sotcpPrev = sotcp; |
| sotcp = sotcp->fNext; |
| } while (sotcp != NULL); |
| if (sotcp == NULL) return; // not found |
| |
| if (sotcp == sotcpHead) { |
| // We found it at the head of the list. Remove it and reinsert the tail into the hash table: |
| sotcpHead = sotcp->fNext; |
| sotcp->fNext = NULL; |
| delete sotcp; |
| |
| if (sotcpHead == NULL) { |
| // There were no more entries on the list. Remove the original entry from the hash table: |
| fTCPStreamingDatabase->Remove((char const*)socketNum); |
| } else { |
| // Add the rest of the list into the hash table (replacing the original): |
| fTCPStreamingDatabase->Add((char const*)socketNum, sotcpHead); |
| } |
| } else { |
| // We found it on the list, but not at the head. Unlink it: |
| sotcpPrev->fNext = sotcp->fNext; |
| sotcp->fNext = NULL; |
| delete sotcp; |
| } |
| } |
| |
| void RTSPServer::stopTCPStreamingOnSocket(int socketNum) { |
| // Close any stream that is streaming over "socketNum" (using RTP/RTCP-over-TCP streaming): |
| streamingOverTCPRecord* sotcp |
| = (streamingOverTCPRecord*)fTCPStreamingDatabase->Lookup((char const*)socketNum); |
| if (sotcp != NULL) { |
| do { |
| RTSPClientSession* clientSession |
| = (RTSPServer::RTSPClientSession*)lookupClientSession(sotcp->fSessionId); |
| if (clientSession != NULL) { |
| clientSession->deleteStreamByTrack(sotcp->fTrackNum); |
| } |
| |
| streamingOverTCPRecord* sotcpNext = sotcp->fNext; |
| sotcp->fNext = NULL; |
| delete sotcp; |
| sotcp = sotcpNext; |
| } while (sotcp != NULL); |
| fTCPStreamingDatabase->Remove((char const*)socketNum); |
| } |
| } |
| |
| |
| ////////// RTSPServer::RTSPClientConnection implementation ////////// |
| |
| RTSPServer::RTSPClientConnection |
| ::RTSPClientConnection(RTSPServer& ourServer, int clientSocket, struct sockaddr_in clientAddr) |
| : GenericMediaServer::ClientConnection(ourServer, clientSocket, clientAddr), |
| fOurRTSPServer(ourServer), fClientInputSocket(fOurSocket), fClientOutputSocket(fOurSocket), |
| fIsActive(True), fRecursionCount(0), fOurSessionCookie(NULL) { |
| resetRequestBuffer(); |
| } |
| |
| RTSPServer::RTSPClientConnection::~RTSPClientConnection() { |
| if (fOurSessionCookie != NULL) { |
| // We were being used for RTSP-over-HTTP tunneling. Also remove ourselves from the 'session cookie' hash table before we go: |
| fOurRTSPServer.fClientConnectionsForHTTPTunneling->Remove(fOurSessionCookie); |
| delete[] fOurSessionCookie; |
| } |
| |
| closeSocketsRTSP(); |
| } |
| |
| // Handler routines for specific RTSP commands: |
| |
| void RTSPServer::RTSPClientConnection::handleCmd_OPTIONS() { |
| snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, |
| "RTSP/1.0 200 OK\r\nCSeq: %s\r\n%sPublic: %s\r\n\r\n", |
| fCurrentCSeq, dateHeader(), fOurRTSPServer.allowedCommandNames()); |
| } |
| |
| void RTSPServer::RTSPClientConnection |
| ::handleCmd_GET_PARAMETER(char const* /*fullRequestStr*/) { |
| // By default, we implement "GET_PARAMETER" (on the entire server) just as a 'no op', and send back a dummy response. |
| // (If you want to handle this type of "GET_PARAMETER" differently, you can do so by defining a subclass of "RTSPServer" |
| // and "RTSPServer::RTSPClientConnection", and then reimplement this virtual function in your subclass.) |
| setRTSPResponse("200 OK", LIVEMEDIA_LIBRARY_VERSION_STRING); |
| } |
| |
| void RTSPServer::RTSPClientConnection |
| ::handleCmd_SET_PARAMETER(char const* /*fullRequestStr*/) { |
| // By default, we implement "SET_PARAMETER" (on the entire server) just as a 'no op', and send back an empty response. |
| // (If you want to handle this type of "SET_PARAMETER" differently, you can do so by defining a subclass of "RTSPServer" |
| // and "RTSPServer::RTSPClientConnection", and then reimplement this virtual function in your subclass.) |
| setRTSPResponse("200 OK"); |
| } |
| |
| void RTSPServer::RTSPClientConnection |
| ::handleCmd_DESCRIBE(char const* urlPreSuffix, char const* urlSuffix, char const* fullRequestStr) { |
| ServerMediaSession* session = NULL; |
| char* sdpDescription = NULL; |
| char* rtspURL = NULL; |
| do { |
| char urlTotalSuffix[2*RTSP_PARAM_STRING_MAX]; |
| // enough space for urlPreSuffix/urlSuffix'\0' |
| urlTotalSuffix[0] = '\0'; |
| if (urlPreSuffix[0] != '\0') { |
| strcat(urlTotalSuffix, urlPreSuffix); |
| strcat(urlTotalSuffix, "/"); |
| } |
| strcat(urlTotalSuffix, urlSuffix); |
| |
| if (!authenticationOK("DESCRIBE", urlTotalSuffix, fullRequestStr)) break; |
| |
| // We should really check that the request contains an "Accept:" ##### |
| // for "application/sdp", because that's what we're sending back ##### |
| |
| // Begin by looking up the "ServerMediaSession" object for the specified "urlTotalSuffix": |
| session = fOurServer.lookupServerMediaSession(urlTotalSuffix); |
| if (session == NULL) { |
| handleCmd_notFound(); |
| break; |
| } |
| |
| // Increment the "ServerMediaSession" object's reference count, in case someone removes it |
| // while we're using it: |
| session->incrementReferenceCount(); |
| |
| // Then, assemble a SDP description for this session: |
| sdpDescription = session->generateSDPDescription(); |
| if (sdpDescription == NULL) { |
| // This usually means that a file name that was specified for a |
| // "ServerMediaSubsession" does not exist. |
| setRTSPResponse("404 File Not Found, Or In Incorrect Format"); |
| break; |
| } |
| unsigned sdpDescriptionSize = strlen(sdpDescription); |
| |
| // Also, generate our RTSP URL, for the "Content-Base:" header |
| // (which is necessary to ensure that the correct URL gets used in subsequent "SETUP" requests). |
| rtspURL = fOurRTSPServer.rtspURL(session, fClientInputSocket); |
| |
| snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, |
| "RTSP/1.0 200 OK\r\nCSeq: %s\r\n" |
| "%s" |
| "Content-Base: %s/\r\n" |
| "Content-Type: application/sdp\r\n" |
| "Content-Length: %d\r\n\r\n" |
| "%s", |
| fCurrentCSeq, |
| dateHeader(), |
| rtspURL, |
| sdpDescriptionSize, |
| sdpDescription); |
| } while (0); |
| |
| if (session != NULL) { |
| // Decrement its reference count, now that we're done using it: |
| session->decrementReferenceCount(); |
| if (session->referenceCount() == 0 && session->deleteWhenUnreferenced()) { |
| fOurServer.removeServerMediaSession(session); |
| } |
| } |
| |
| delete[] sdpDescription; |
| delete[] rtspURL; |
| } |
| |
| static void lookForHeader(char const* headerName, char const* source, unsigned sourceLen, char* resultStr, unsigned resultMaxSize) { |
| resultStr[0] = '\0'; // by default, return an empty string |
| unsigned headerNameLen = strlen(headerName); |
| for (int i = 0; i < (int)(sourceLen-headerNameLen); ++i) { |
| if (strncmp(&source[i], headerName, headerNameLen) == 0 && source[i+headerNameLen] == ':') { |
| // We found the header. Skip over any whitespace, then copy the rest of the line to "resultStr": |
| for (i += headerNameLen+1; i < (int)sourceLen && (source[i] == ' ' || source[i] == '\t'); ++i) {} |
| for (unsigned j = i; j < sourceLen; ++j) { |
| if (source[j] == '\r' || source[j] == '\n') { |
| // We've found the end of the line. Copy it to the result (if it will fit): |
| if (j-i+1 > resultMaxSize) return; // it wouldn't fit |
| char const* resultSource = &source[i]; |
| char const* resultSourceEnd = &source[j]; |
| while (resultSource < resultSourceEnd) *resultStr++ = *resultSource++; |
| *resultStr = '\0'; |
| return; |
| } |
| } |
| } |
| } |
| } |
| |
| void RTSPServer::RTSPClientConnection::handleCmd_bad() { |
| // Don't do anything with "fCurrentCSeq", because it might be nonsense |
| snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, |
| "RTSP/1.0 400 Bad Request\r\n%sAllow: %s\r\n\r\n", |
| dateHeader(), fOurRTSPServer.allowedCommandNames()); |
| } |
| |
| void RTSPServer::RTSPClientConnection::handleCmd_notSupported() { |
| snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, |
| "RTSP/1.0 405 Method Not Allowed\r\nCSeq: %s\r\n%sAllow: %s\r\n\r\n", |
| fCurrentCSeq, dateHeader(), fOurRTSPServer.allowedCommandNames()); |
| } |
| |
| void RTSPServer::RTSPClientConnection::handleCmd_notFound() { |
| setRTSPResponse("404 Stream Not Found"); |
| } |
| |
| void RTSPServer::RTSPClientConnection::handleCmd_sessionNotFound() { |
| setRTSPResponse("454 Session Not Found"); |
| } |
| |
| void RTSPServer::RTSPClientConnection::handleCmd_unsupportedTransport() { |
| setRTSPResponse("461 Unsupported Transport"); |
| } |
| |
| Boolean RTSPServer::RTSPClientConnection::parseHTTPRequestString(char* resultCmdName, unsigned resultCmdNameMaxSize, |
| char* urlSuffix, unsigned urlSuffixMaxSize, |
| char* sessionCookie, unsigned sessionCookieMaxSize, |
| char* acceptStr, unsigned acceptStrMaxSize) { |
| // Check for the limited HTTP requests that we expect for specifying RTSP-over-HTTP tunneling. |
| // This parser is currently rather dumb; it should be made smarter ##### |
| char const* reqStr = (char const*)fRequestBuffer; |
| unsigned const reqStrSize = fRequestBytesAlreadySeen; |
| |
| // Read everything up to the first space as the command name: |
| Boolean parseSucceeded = False; |
| unsigned i; |
| for (i = 0; i < resultCmdNameMaxSize-1 && i < reqStrSize; ++i) { |
| char c = reqStr[i]; |
| if (c == ' ' || c == '\t') { |
| parseSucceeded = True; |
| break; |
| } |
| |
| resultCmdName[i] = c; |
| } |
| resultCmdName[i] = '\0'; |
| if (!parseSucceeded) return False; |
| |
| // Look for the string "HTTP/", before the first \r or \n: |
| parseSucceeded = False; |
| for (; i < reqStrSize-5 && reqStr[i] != '\r' && reqStr[i] != '\n'; ++i) { |
| if (reqStr[i] == 'H' && reqStr[i+1] == 'T' && reqStr[i+2]== 'T' && reqStr[i+3]== 'P' && reqStr[i+4]== '/') { |
| i += 5; // to advance past the "HTTP/" |
| parseSucceeded = True; |
| break; |
| } |
| } |
| if (!parseSucceeded) return False; |
| |
| // Get the 'URL suffix' that occurred before this: |
| unsigned k = i-6; |
| while (k > 0 && reqStr[k] == ' ') --k; // back up over white space |
| unsigned j = k; |
| while (j > 0 && reqStr[j] != ' ' && reqStr[j] != '/') --j; |
| // The URL suffix is in position (j,k]: |
| if (k - j + 1 > urlSuffixMaxSize) return False; // there's no room> |
| unsigned n = 0; |
| while (++j <= k) urlSuffix[n++] = reqStr[j]; |
| urlSuffix[n] = '\0'; |
| |
| // Look for various headers that we're interested in: |
| lookForHeader("x-sessioncookie", &reqStr[i], reqStrSize-i, sessionCookie, sessionCookieMaxSize); |
| lookForHeader("Accept", &reqStr[i], reqStrSize-i, acceptStr, acceptStrMaxSize); |
| |
| return True; |
| } |
| |
| void RTSPServer::RTSPClientConnection::handleHTTPCmd_notSupported() { |
| snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, |
| "HTTP/1.1 405 Method Not Allowed\r\n%s\r\n\r\n", |
| dateHeader()); |
| } |
| |
| void RTSPServer::RTSPClientConnection::handleHTTPCmd_notFound() { |
| snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, |
| "HTTP/1.1 404 Not Found\r\n%s\r\n\r\n", |
| dateHeader()); |
| } |
| |
| void RTSPServer::RTSPClientConnection::handleHTTPCmd_OPTIONS() { |
| #ifdef DEBUG |
| fprintf(stderr, "Handled HTTP \"OPTIONS\" request\n"); |
| #endif |
| // Construct a response to the "OPTIONS" command that notes that our special headers (for RTSP-over-HTTP tunneling) are allowed: |
| snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, |
| "HTTP/1.1 200 OK\r\n" |
| "%s" |
| "Access-Control-Allow-Origin: *\r\n" |
| "Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n" |
| "Access-Control-Allow-Headers: x-sessioncookie, Pragma, Cache-Control\r\n" |
| "Access-Control-Max-Age: 1728000\r\n" |
| "\r\n", |
| dateHeader()); |
| } |
| |
| void RTSPServer::RTSPClientConnection::handleHTTPCmd_TunnelingGET(char const* sessionCookie) { |
| // Record ourself as having this 'session cookie', so that a subsequent HTTP "POST" command (with the same 'session cookie') |
| // can find us: |
| if (fOurRTSPServer.fClientConnectionsForHTTPTunneling == NULL) { |
| fOurRTSPServer.fClientConnectionsForHTTPTunneling = HashTable::create(STRING_HASH_KEYS); |
| } |
| delete[] fOurSessionCookie; fOurSessionCookie = strDup(sessionCookie); |
| fOurRTSPServer.fClientConnectionsForHTTPTunneling->Add(sessionCookie, (void*)this); |
| #ifdef DEBUG |
| fprintf(stderr, "Handled HTTP \"GET\" request (client output socket: %d)\n", fClientOutputSocket); |
| #endif |
| |
| // Construct our response: |
| snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, |
| "HTTP/1.1 200 OK\r\n" |
| "%s" |
| "Cache-Control: no-cache\r\n" |
| "Pragma: no-cache\r\n" |
| "Content-Type: application/x-rtsp-tunnelled\r\n" |
| "\r\n", |
| dateHeader()); |
| } |
| |
| Boolean RTSPServer::RTSPClientConnection |
| ::handleHTTPCmd_TunnelingPOST(char const* sessionCookie, unsigned char const* extraData, unsigned extraDataSize) { |
| // Use the "sessionCookie" string to look up the separate "RTSPClientConnection" object that should have been used to handle |
| // an earlier HTTP "GET" request: |
| if (fOurRTSPServer.fClientConnectionsForHTTPTunneling == NULL) { |
| fOurRTSPServer.fClientConnectionsForHTTPTunneling = HashTable::create(STRING_HASH_KEYS); |
| } |
| RTSPServer::RTSPClientConnection* prevClientConnection |
| = (RTSPServer::RTSPClientConnection*)(fOurRTSPServer.fClientConnectionsForHTTPTunneling->Lookup(sessionCookie)); |
| if (prevClientConnection == NULL || prevClientConnection == this) { |
| // Either there was no previous HTTP "GET" request, or it was on the same connection; treat this "POST" request as bad: |
| handleHTTPCmd_notSupported(); |
| fIsActive = False; // triggers deletion of ourself |
| return False; |
| } |
| #ifdef DEBUG |
| fprintf(stderr, "Handled HTTP \"POST\" request (client input socket: %d)\n", fClientInputSocket); |
| #endif |
| |
| // Change the previous "RTSPClientSession" object's input socket to ours. It will be used for subsequent requests: |
| prevClientConnection->changeClientInputSocket(fClientInputSocket, extraData, extraDataSize); |
| fClientInputSocket = fClientOutputSocket = -1; // so the socket doesn't get closed when we get deleted |
| return True; |
| } |
| |
| void RTSPServer::RTSPClientConnection::handleHTTPCmd_StreamingGET(char const* /*urlSuffix*/, char const* /*fullRequestStr*/) { |
| // By default, we don't support requests to access streams via HTTP: |
| handleHTTPCmd_notSupported(); |
| } |
| |
| void RTSPServer::RTSPClientConnection::resetRequestBuffer() { |
| ClientConnection::resetRequestBuffer(); |
| |
| fLastCRLF = &fRequestBuffer[-3]; // hack: Ensures that we don't think we have end-of-msg if the data starts with <CR><LF> |
| fBase64RemainderCount = 0; |
| } |
| |
| void RTSPServer::RTSPClientConnection::closeSocketsRTSP() { |
| // First, tell our server to stop any streaming that it might be doing over our output socket: |
| fOurRTSPServer.stopTCPStreamingOnSocket(fClientOutputSocket); |
| |
| // Turn off background handling on our input socket (and output socket, if different); then close it (or them): |
| if (fClientOutputSocket != fClientInputSocket) { |
| envir().taskScheduler().disableBackgroundHandling(fClientOutputSocket); |
| ::closeSocket(fClientOutputSocket); |
| } |
| fClientOutputSocket = -1; |
| |
| closeSockets(); // closes fClientInputSocket |
| } |
| |
| void RTSPServer::RTSPClientConnection::handleAlternativeRequestByte(void* instance, u_int8_t requestByte) { |
| RTSPClientConnection* connection = (RTSPClientConnection*)instance; |
| connection->handleAlternativeRequestByte1(requestByte); |
| } |
| |
| void RTSPServer::RTSPClientConnection::handleAlternativeRequestByte1(u_int8_t requestByte) { |
| if (requestByte == 0xFF) { |
| // Hack: The new handler of the input TCP socket encountered an error reading it. Indicate this: |
| handleRequestBytes(-1); |
| } else if (requestByte == 0xFE) { |
| // Another hack: The new handler of the input TCP socket no longer needs it, so take back control of it: |
| envir().taskScheduler().setBackgroundHandling(fClientInputSocket, SOCKET_READABLE|SOCKET_EXCEPTION, |
| incomingRequestHandler, this); |
| } else { |
| // Normal case: Add this character to our buffer; then try to handle the data that we have buffered so far: |
| if (fRequestBufferBytesLeft == 0 || fRequestBytesAlreadySeen >= REQUEST_BUFFER_SIZE) return; |
| fRequestBuffer[fRequestBytesAlreadySeen] = requestByte; |
| handleRequestBytes(1); |
| } |
| } |
| |
| void RTSPServer::RTSPClientConnection::handleRequestBytes(int newBytesRead) { |
| int numBytesRemaining = 0; |
| ++fRecursionCount; |
| |
| do { |
| RTSPServer::RTSPClientSession* clientSession = NULL; |
| |
| if (newBytesRead < 0 || (unsigned)newBytesRead >= fRequestBufferBytesLeft) { |
| // Either the client socket has died, or the request was too big for us. |
| // Terminate this connection: |
| #ifdef DEBUG |
| fprintf(stderr, "RTSPClientConnection[%p]::handleRequestBytes() read %d new bytes (of %d); terminating connection!\n", this, newBytesRead, fRequestBufferBytesLeft); |
| #endif |
| fIsActive = False; |
| break; |
| } |
| |
| Boolean endOfMsg = False; |
| unsigned char* ptr = &fRequestBuffer[fRequestBytesAlreadySeen]; |
| #ifdef DEBUG |
| ptr[newBytesRead] = '\0'; |
| fprintf(stderr, "RTSPClientConnection[%p]::handleRequestBytes() %s %d new bytes:%s\n", |
| this, numBytesRemaining > 0 ? "processing" : "read", newBytesRead, ptr); |
| #endif |
| |
| if (fClientOutputSocket != fClientInputSocket && numBytesRemaining == 0) { |
| // We're doing RTSP-over-HTTP tunneling, and input commands are assumed to have been Base64-encoded. |
| // We therefore Base64-decode as much of this new data as we can (i.e., up to a multiple of 4 bytes). |
| |
| // But first, we remove any whitespace that may be in the input data: |
| unsigned toIndex = 0; |
| for (int fromIndex = 0; fromIndex < newBytesRead; ++fromIndex) { |
| char c = ptr[fromIndex]; |
| if (!(c == ' ' || c == '\t' || c == '\r' || c == '\n')) { // not 'whitespace': space,tab,CR,NL |
| ptr[toIndex++] = c; |
| } |
| } |
| newBytesRead = toIndex; |
| |
| unsigned numBytesToDecode = fBase64RemainderCount + newBytesRead; |
| unsigned newBase64RemainderCount = numBytesToDecode%4; |
| numBytesToDecode -= newBase64RemainderCount; |
| if (numBytesToDecode > 0) { |
| ptr[newBytesRead] = '\0'; |
| unsigned decodedSize; |
| unsigned char* decodedBytes = base64Decode((char const*)(ptr-fBase64RemainderCount), numBytesToDecode, decodedSize); |
| #ifdef DEBUG |
| fprintf(stderr, "Base64-decoded %d input bytes into %d new bytes:", numBytesToDecode, decodedSize); |
| for (unsigned k = 0; k < decodedSize; ++k) fprintf(stderr, "%c", decodedBytes[k]); |
| fprintf(stderr, "\n"); |
| #endif |
| |
| // Copy the new decoded bytes in place of the old ones (we can do this because there are fewer decoded bytes than original): |
| unsigned char* to = ptr-fBase64RemainderCount; |
| for (unsigned i = 0; i < decodedSize; ++i) *to++ = decodedBytes[i]; |
| |
| // Then copy any remaining (undecoded) bytes to the end: |
| for (unsigned j = 0; j < newBase64RemainderCount; ++j) *to++ = (ptr-fBase64RemainderCount+numBytesToDecode)[j]; |
| |
| newBytesRead = decodedSize - fBase64RemainderCount + newBase64RemainderCount; |
| // adjust to allow for the size of the new decoded data (+ remainder) |
| delete[] decodedBytes; |
| } |
| fBase64RemainderCount = newBase64RemainderCount; |
| } |
| |
| unsigned char* tmpPtr = fLastCRLF + 2; |
| if (fBase64RemainderCount == 0) { // no more Base-64 bytes remain to be read/decoded |
| // Look for the end of the message: <CR><LF><CR><LF> |
| if (tmpPtr < fRequestBuffer) tmpPtr = fRequestBuffer; |
| while (tmpPtr < &ptr[newBytesRead-1]) { |
| if (*tmpPtr == '\r' && *(tmpPtr+1) == '\n') { |
| if (tmpPtr - fLastCRLF == 2) { // This is it: |
| endOfMsg = True; |
| break; |
| } |
| fLastCRLF = tmpPtr; |
| } |
| ++tmpPtr; |
| } |
| } |
| |
| fRequestBufferBytesLeft -= newBytesRead; |
| fRequestBytesAlreadySeen += newBytesRead; |
| |
| if (!endOfMsg) break; // subsequent reads will be needed to complete the request |
| |
| // Parse the request string into command name and 'CSeq', then handle the command: |
| fRequestBuffer[fRequestBytesAlreadySeen] = '\0'; |
| char cmdName[RTSP_PARAM_STRING_MAX]; |
| char urlPreSuffix[RTSP_PARAM_STRING_MAX]; |
| char urlSuffix[RTSP_PARAM_STRING_MAX]; |
| char cseq[RTSP_PARAM_STRING_MAX]; |
| char sessionIdStr[RTSP_PARAM_STRING_MAX]; |
| unsigned contentLength = 0; |
| Boolean playAfterSetup = False; |
| fLastCRLF[2] = '\0'; // temporarily, for parsing |
| Boolean parseSucceeded = parseRTSPRequestString((char*)fRequestBuffer, fLastCRLF+2 - fRequestBuffer, |
| cmdName, sizeof cmdName, |
| urlPreSuffix, sizeof urlPreSuffix, |
| urlSuffix, sizeof urlSuffix, |
| cseq, sizeof cseq, |
| sessionIdStr, sizeof sessionIdStr, |
| contentLength); |
| fLastCRLF[2] = '\r'; // restore its value |
| // Check first for a bogus "Content-Length" value that would cause a pointer wraparound: |
| if (tmpPtr + 2 + contentLength < tmpPtr + 2) { |
| #ifdef DEBUG |
| fprintf(stderr, "parseRTSPRequestString() returned a bogus \"Content-Length:\" value: 0x%x (%d)\n", contentLength, (int)contentLength); |
| #endif |
| contentLength = 0; |
| parseSucceeded = False; |
| } |
| if (parseSucceeded) { |
| #ifdef DEBUG |
| fprintf(stderr, "parseRTSPRequestString() succeeded, returning cmdName \"%s\", urlPreSuffix \"%s\", urlSuffix \"%s\", CSeq \"%s\", Content-Length %u, with %d bytes following the message.\n", cmdName, urlPreSuffix, urlSuffix, cseq, contentLength, ptr + newBytesRead - (tmpPtr + 2)); |
| #endif |
| // If there was a "Content-Length:" header, then make sure we've received all of the data that it specified: |
| if (ptr + newBytesRead < tmpPtr + 2 + contentLength) break; // we still need more data; subsequent reads will give it to us |
| |
| // If the request included a "Session:" id, and it refers to a client session that's |
| // current ongoing, then use this command to indicate 'liveness' on that client session: |
| Boolean const requestIncludedSessionId = sessionIdStr[0] != '\0'; |
| if (requestIncludedSessionId) { |
| clientSession |
| = (RTSPServer::RTSPClientSession*)(fOurRTSPServer.lookupClientSession(sessionIdStr)); |
| if (clientSession != NULL) clientSession->noteLiveness(); |
| } |
| |
| // We now have a complete RTSP request. |
| // Handle the specified command (beginning with commands that are session-independent): |
| fCurrentCSeq = cseq; |
| if (strcmp(cmdName, "OPTIONS") == 0) { |
| // If the "OPTIONS" command included a "Session:" id for a session that doesn't exist, |
| // then treat this as an error: |
| if (requestIncludedSessionId && clientSession == NULL) { |
| #ifdef DEBUG |
| fprintf(stderr, "Calling handleCmd_sessionNotFound() (case 1)\n"); |
| #endif |
| handleCmd_sessionNotFound(); |
| } else { |
| // Normal case: |
| handleCmd_OPTIONS(); |
| } |
| } else if (urlPreSuffix[0] == '\0' && urlSuffix[0] == '*' && urlSuffix[1] == '\0') { |
| // The special "*" URL means: an operation on the entire server. This works only for GET_PARAMETER and SET_PARAMETER: |
| if (strcmp(cmdName, "GET_PARAMETER") == 0) { |
| handleCmd_GET_PARAMETER((char const*)fRequestBuffer); |
| } else if (strcmp(cmdName, "SET_PARAMETER") == 0) { |
| handleCmd_SET_PARAMETER((char const*)fRequestBuffer); |
| } else { |
| handleCmd_notSupported(); |
| } |
| } else if (strcmp(cmdName, "DESCRIBE") == 0) { |
| handleCmd_DESCRIBE(urlPreSuffix, urlSuffix, (char const*)fRequestBuffer); |
| } else if (strcmp(cmdName, "SETUP") == 0) { |
| Boolean areAuthenticated = True; |
| |
| if (!requestIncludedSessionId) { |
| // No session id was present in the request. |
| // So create a new "RTSPClientSession" object for this request. |
| |
| // But first, make sure that we're authenticated to perform this command: |
| char urlTotalSuffix[2*RTSP_PARAM_STRING_MAX]; |
| // enough space for urlPreSuffix/urlSuffix'\0' |
| urlTotalSuffix[0] = '\0'; |
| if (urlPreSuffix[0] != '\0') { |
| strcat(urlTotalSuffix, urlPreSuffix); |
| strcat(urlTotalSuffix, "/"); |
| } |
| strcat(urlTotalSuffix, urlSuffix); |
| if (authenticationOK("SETUP", urlTotalSuffix, (char const*)fRequestBuffer)) { |
| clientSession |
| = (RTSPServer::RTSPClientSession*)fOurRTSPServer.createNewClientSessionWithId(); |
| } else { |
| areAuthenticated = False; |
| } |
| } |
| if (clientSession != NULL) { |
| clientSession->handleCmd_SETUP(this, urlPreSuffix, urlSuffix, (char const*)fRequestBuffer); |
| playAfterSetup = clientSession->fStreamAfterSETUP; |
| } else if (areAuthenticated) { |
| #ifdef DEBUG |
| fprintf(stderr, "Calling handleCmd_sessionNotFound() (case 2)\n"); |
| #endif |
| handleCmd_sessionNotFound(); |
| } |
| } else if (strcmp(cmdName, "TEARDOWN") == 0 |
| || strcmp(cmdName, "PLAY") == 0 |
| || strcmp(cmdName, "PAUSE") == 0 |
| || strcmp(cmdName, "GET_PARAMETER") == 0 |
| || strcmp(cmdName, "SET_PARAMETER") == 0) { |
| if (clientSession != NULL) { |
| clientSession->handleCmd_withinSession(this, cmdName, urlPreSuffix, urlSuffix, (char const*)fRequestBuffer); |
| } else { |
| #ifdef DEBUG |
| fprintf(stderr, "Calling handleCmd_sessionNotFound() (case 3)\n"); |
| #endif |
| handleCmd_sessionNotFound(); |
| } |
| } else if (strcmp(cmdName, "REGISTER") == 0 || strcmp(cmdName, "DEREGISTER") == 0) { |
| // Because - unlike other commands - an implementation of this command needs |
| // the entire URL, we re-parse the command to get it: |
| char* url = strDupSize((char*)fRequestBuffer); |
| if (sscanf((char*)fRequestBuffer, "%*s %s", url) == 1) { |
| // Check for special command-specific parameters in a "Transport:" header: |
| Boolean reuseConnection, deliverViaTCP; |
| char* proxyURLSuffix; |
| parseTransportHeaderForREGISTER((const char*)fRequestBuffer, reuseConnection, deliverViaTCP, proxyURLSuffix); |
| |
| handleCmd_REGISTER(cmdName, url, urlSuffix, (char const*)fRequestBuffer, reuseConnection, deliverViaTCP, proxyURLSuffix); |
| delete[] proxyURLSuffix; |
| } else { |
| handleCmd_bad(); |
| } |
| delete[] url; |
| } else { |
| // The command is one that we don't handle: |
| handleCmd_notSupported(); |
| } |
| } else { |
| #ifdef DEBUG |
| fprintf(stderr, "parseRTSPRequestString() failed; checking now for HTTP commands (for RTSP-over-HTTP tunneling)...\n"); |
| #endif |
| // The request was not (valid) RTSP, but check for a special case: HTTP commands (for setting up RTSP-over-HTTP tunneling): |
| char sessionCookie[RTSP_PARAM_STRING_MAX]; |
| char acceptStr[RTSP_PARAM_STRING_MAX]; |
| *fLastCRLF = '\0'; // temporarily, for parsing |
| parseSucceeded = parseHTTPRequestString(cmdName, sizeof cmdName, |
| urlSuffix, sizeof urlPreSuffix, |
| sessionCookie, sizeof sessionCookie, |
| acceptStr, sizeof acceptStr); |
| *fLastCRLF = '\r'; |
| if (parseSucceeded) { |
| #ifdef DEBUG |
| fprintf(stderr, "parseHTTPRequestString() succeeded, returning cmdName \"%s\", urlSuffix \"%s\", sessionCookie \"%s\", acceptStr \"%s\"\n", cmdName, urlSuffix, sessionCookie, acceptStr); |
| #endif |
| // Check that the HTTP command is valid for RTSP-over-HTTP tunneling: There must be a 'session cookie'. |
| Boolean isValidHTTPCmd = True; |
| if (strcmp(cmdName, "OPTIONS") == 0) { |
| handleHTTPCmd_OPTIONS(); |
| } else if (sessionCookie[0] == '\0') { |
| // There was no "x-sessioncookie:" header. If there was an "Accept: application/x-rtsp-tunnelled" header, |
| // then this is a bad tunneling request. Otherwise, assume that it's an attempt to access the stream via HTTP. |
| if (strcmp(acceptStr, "application/x-rtsp-tunnelled") == 0) { |
| isValidHTTPCmd = False; |
| } else { |
| handleHTTPCmd_StreamingGET(urlSuffix, (char const*)fRequestBuffer); |
| } |
| } else if (strcmp(cmdName, "GET") == 0) { |
| handleHTTPCmd_TunnelingGET(sessionCookie); |
| } else if (strcmp(cmdName, "POST") == 0) { |
| // We might have received additional data following the HTTP "POST" command - i.e., the first Base64-encoded RTSP command. |
| // Check for this, and handle it if it exists: |
| unsigned char const* extraData = fLastCRLF+4; |
| unsigned extraDataSize = &fRequestBuffer[fRequestBytesAlreadySeen] - extraData; |
| if (handleHTTPCmd_TunnelingPOST(sessionCookie, extraData, extraDataSize)) { |
| // We don't respond to the "POST" command, and we go away: |
| fIsActive = False; |
| break; |
| } |
| } else { |
| isValidHTTPCmd = False; |
| } |
| if (!isValidHTTPCmd) { |
| handleHTTPCmd_notSupported(); |
| } |
| } else { |
| #ifdef DEBUG |
| fprintf(stderr, "parseHTTPRequestString() failed!\n"); |
| #endif |
| handleCmd_bad(); |
| } |
| } |
| |
| #ifdef DEBUG |
| fprintf(stderr, "sending response: %s", fResponseBuffer); |
| #endif |
| send(fClientOutputSocket, (char const*)fResponseBuffer, strlen((char*)fResponseBuffer), 0); |
| |
| if (playAfterSetup) { |
| // The client has asked for streaming to commence now, rather than after a |
| // subsequent "PLAY" command. So, simulate the effect of a "PLAY" command: |
| clientSession->handleCmd_withinSession(this, "PLAY", urlPreSuffix, urlSuffix, (char const*)fRequestBuffer); |
| } |
| |
| // Check whether there are extra bytes remaining in the buffer, after the end of the request (a rare case). |
| // If so, move them to the front of our buffer, and keep processing it, because it might be a following, pipelined request. |
| unsigned requestSize = (fLastCRLF+4-fRequestBuffer) + contentLength; |
| numBytesRemaining = fRequestBytesAlreadySeen - requestSize; |
| resetRequestBuffer(); // to prepare for any subsequent request |
| |
| if (numBytesRemaining > 0) { |
| memmove(fRequestBuffer, &fRequestBuffer[requestSize], numBytesRemaining); |
| newBytesRead = numBytesRemaining; |
| } |
| } while (numBytesRemaining > 0); |
| |
| --fRecursionCount; |
| if (!fIsActive) { |
| if (fRecursionCount > 0) closeSockets(); else delete this; |
| // Note: The "fRecursionCount" test is for a pathological situation where we reenter the event loop and get called recursively |
| // while handling a command (e.g., while handling a "DESCRIBE", to get a SDP description). |
| // In such a case we don't want to actually delete ourself until we leave the outermost call. |
| } |
| } |
| |
| #define SKIP_WHITESPACE while (*fields != '\0' && (*fields == ' ' || *fields == '\t')) ++fields |
| |
| static Boolean parseAuthorizationHeader(char const* buf, |
| char const*& username, |
| char const*& realm, |
| char const*& nonce, char const*& uri, |
| char const*& response) { |
| // Initialize the result parameters to default values: |
| username = realm = nonce = uri = response = NULL; |
| |
| // First, find "Authorization:" |
| while (1) { |
| if (*buf == '\0') return False; // not found |
| if (_strncasecmp(buf, "Authorization: Digest ", 22) == 0) break; |
| ++buf; |
| } |
| |
| // Then, run through each of the fields, looking for ones we handle: |
| char const* fields = buf + 22; |
| char* parameter = strDupSize(fields); |
| char* value = strDupSize(fields); |
| char* p; |
| Boolean success; |
| do { |
| // Parse: <parameter>="<value>" |
| success = False; |
| parameter[0] = value[0] = '\0'; |
| SKIP_WHITESPACE; |
| for (p = parameter; *fields != '\0' && *fields != ' ' && *fields != '\t' && *fields != '='; ) *p++ = *fields++; |
| SKIP_WHITESPACE; |
| if (*fields++ != '=') break; // parsing failed |
| *p = '\0'; // complete parsing <parameter> |
| SKIP_WHITESPACE; |
| if (*fields++ != '"') break; // parsing failed |
| for (p = value; *fields != '\0' && *fields != '"'; ) *p++ = *fields++; |
| if (*fields++ != '"') break; // parsing failed |
| *p = '\0'; // complete parsing <value> |
| SKIP_WHITESPACE; |
| success = True; |
| |
| // Copy values for parameters that we understand: |
| if (strcmp(parameter, "username") == 0) { |
| username = strDup(value); |
| } else if (strcmp(parameter, "realm") == 0) { |
| realm = strDup(value); |
| } else if (strcmp(parameter, "nonce") == 0) { |
| nonce = strDup(value); |
| } else if (strcmp(parameter, "uri") == 0) { |
| uri = strDup(value); |
| } else if (strcmp(parameter, "response") == 0) { |
| response = strDup(value); |
| } |
| |
| // Check for a ',', indicating that more <parameter>="<value>" pairs follow: |
| } while (*fields++ == ','); |
| |
| delete[] parameter; delete[] value; |
| return success; |
| } |
| |
| Boolean RTSPServer::RTSPClientConnection |
| ::authenticationOK(char const* cmdName, char const* urlSuffix, char const* fullRequestStr) { |
| if (!fOurRTSPServer.specialClientAccessCheck(fClientInputSocket, fClientAddr, urlSuffix)) { |
| setRTSPResponse("401 Unauthorized"); |
| return False; |
| } |
| |
| // If we weren't set up with an authentication database, we're OK: |
| UserAuthenticationDatabase* authDB = fOurRTSPServer.getAuthenticationDatabaseForCommand(cmdName); |
| if (authDB == NULL) return True; |
| |
| char const* username = NULL; char const* realm = NULL; char const* nonce = NULL; |
| char const* uri = NULL; char const* response = NULL; |
| Boolean success = False; |
| |
| do { |
| // To authenticate, we first need to have a nonce set up |
| // from a previous attempt: |
| if (fCurrentAuthenticator.nonce() == NULL) break; |
| |
| // Next, the request needs to contain an "Authorization:" header, |
| // containing a username, (our) realm, (our) nonce, uri, |
| // and response string: |
| if (!parseAuthorizationHeader(fullRequestStr, |
| username, realm, nonce, uri, response) |
| || username == NULL |
| || realm == NULL || strcmp(realm, fCurrentAuthenticator.realm()) != 0 |
| || nonce == NULL || strcmp(nonce, fCurrentAuthenticator.nonce()) != 0 |
| || uri == NULL || response == NULL) { |
| break; |
| } |
| |
| // Next, the username has to be known to us: |
| char const* password = authDB->lookupPassword(username); |
| #ifdef DEBUG |
| fprintf(stderr, "lookupPassword(%s) returned password %s\n", username, password); |
| #endif |
| if (password == NULL) break; |
| fCurrentAuthenticator.setUsernameAndPassword(username, password, authDB->passwordsAreMD5()); |
| |
| // Finally, compute a digest response from the information that we have, |
| // and compare it to the one that we were given: |
| char const* ourResponse |
| = fCurrentAuthenticator.computeDigestResponse(cmdName, uri); |
| success = (strcmp(ourResponse, response) == 0); |
| fCurrentAuthenticator.reclaimDigestResponse(ourResponse); |
| } while (0); |
| |
| delete[] (char*)realm; delete[] (char*)nonce; |
| delete[] (char*)uri; delete[] (char*)response; |
| |
| if (success) { |
| // The user has been authenticated. |
| // Now allow subclasses a chance to validate the user against the IP address and/or URL suffix. |
| if (!fOurRTSPServer.specialClientUserAccessCheck(fClientInputSocket, fClientAddr, urlSuffix, username)) { |
| // Note: We don't return a "WWW-Authenticate" header here, because the user is valid, |
| // even though the server has decided that they should not have access. |
| setRTSPResponse("401 Unauthorized"); |
| delete[] (char*)username; |
| return False; |
| } |
| } |
| delete[] (char*)username; |
| if (success) return True; |
| |
| // If we get here, we failed to authenticate the user. |
| // Send back a "401 Unauthorized" response, with a new random nonce: |
| fCurrentAuthenticator.setRealmAndRandomNonce(authDB->realm()); |
| snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, |
| "RTSP/1.0 401 Unauthorized\r\n" |
| "CSeq: %s\r\n" |
| "%s" |
| "WWW-Authenticate: Digest realm=\"%s\", nonce=\"%s\"\r\n\r\n", |
| fCurrentCSeq, |
| dateHeader(), |
| fCurrentAuthenticator.realm(), fCurrentAuthenticator.nonce()); |
| return False; |
| } |
| |
| void RTSPServer::RTSPClientConnection |
| ::setRTSPResponse(char const* responseStr) { |
| snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, |
| "RTSP/1.0 %s\r\n" |
| "CSeq: %s\r\n" |
| "%s\r\n", |
| responseStr, |
| fCurrentCSeq, |
| dateHeader()); |
| } |
| |
| void RTSPServer::RTSPClientConnection |
| ::setRTSPResponse(char const* responseStr, u_int32_t sessionId) { |
| snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, |
| "RTSP/1.0 %s\r\n" |
| "CSeq: %s\r\n" |
| "%s" |
| "Session: %08X\r\n\r\n", |
| responseStr, |
| fCurrentCSeq, |
| dateHeader(), |
| sessionId); |
| } |
| |
| void RTSPServer::RTSPClientConnection |
| ::setRTSPResponse(char const* responseStr, char const* contentStr) { |
| if (contentStr == NULL) contentStr = ""; |
| unsigned const contentLen = strlen(contentStr); |
| |
| snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, |
| "RTSP/1.0 %s\r\n" |
| "CSeq: %s\r\n" |
| "%s" |
| "Content-Length: %d\r\n\r\n" |
| "%s", |
| responseStr, |
| fCurrentCSeq, |
| dateHeader(), |
| contentLen, |
| contentStr); |
| } |
| |
| void RTSPServer::RTSPClientConnection |
| ::setRTSPResponse(char const* responseStr, u_int32_t sessionId, char const* contentStr) { |
| if (contentStr == NULL) contentStr = ""; |
| unsigned const contentLen = strlen(contentStr); |
| |
| snprintf((char*)fResponseBuffer, sizeof fResponseBuffer, |
| "RTSP/1.0 %s\r\n" |
| "CSeq: %s\r\n" |
| "%s" |
| "Session: %08X\r\n" |
| "Content-Length: %d\r\n\r\n" |
| "%s", |
| responseStr, |
| fCurrentCSeq, |
| dateHeader(), |
| sessionId, |
| contentLen, |
| contentStr); |
| } |
| |
| void RTSPServer::RTSPClientConnection |
| ::changeClientInputSocket(int newSocketNum, unsigned char const* extraData, unsigned extraDataSize) { |
| envir().taskScheduler().disableBackgroundHandling(fClientInputSocket); |
| fClientInputSocket = newSocketNum; |
| envir().taskScheduler().setBackgroundHandling(fClientInputSocket, SOCKET_READABLE|SOCKET_EXCEPTION, |
| incomingRequestHandler, this); |
| |
| // Also write any extra data to our buffer, and handle it: |
| if (extraDataSize > 0 && extraDataSize <= fRequestBufferBytesLeft/*sanity check; should always be true*/) { |
| unsigned char* ptr = &fRequestBuffer[fRequestBytesAlreadySeen]; |
| for (unsigned i = 0; i < extraDataSize; ++i) { |
| ptr[i] = extraData[i]; |
| } |
| handleRequestBytes(extraDataSize); |
| } |
| } |
| |
| |
| ////////// RTSPServer::RTSPClientSession implementation ////////// |
| |
| RTSPServer::RTSPClientSession |
| ::RTSPClientSession(RTSPServer& ourServer, u_int32_t sessionId) |
| : GenericMediaServer::ClientSession(ourServer, sessionId), |
| fOurRTSPServer(ourServer), fIsMulticast(False), fStreamAfterSETUP(False), |
| fTCPStreamIdCount(0), fNumStreamStates(0), fStreamStates(NULL) { |
| } |
| |
| RTSPServer::RTSPClientSession::~RTSPClientSession() { |
| reclaimStreamStates(); |
| } |
| |
| void RTSPServer::RTSPClientSession::deleteStreamByTrack(unsigned trackNum) { |
| if (trackNum >= fNumStreamStates) return; // sanity check; shouldn't happen |
| if (fStreamStates[trackNum].subsession != NULL) { |
| fStreamStates[trackNum].subsession->deleteStream(fOurSessionId, fStreamStates[trackNum].streamToken); |
| fStreamStates[trackNum].subsession = NULL; |
| } |
| |
| // Optimization: If all subsessions have now been deleted, then we can delete ourself now: |
| Boolean noSubsessionsRemain = True; |
| for (unsigned i = 0; i < fNumStreamStates; ++i) { |
| if (fStreamStates[i].subsession != NULL) { |
| noSubsessionsRemain = False; |
| break; |
| } |
| } |
| if (noSubsessionsRemain) delete this; |
| } |
| |
| void RTSPServer::RTSPClientSession::reclaimStreamStates() { |
| for (unsigned i = 0; i < fNumStreamStates; ++i) { |
| if (fStreamStates[i].subsession != NULL) { |
| fOurRTSPServer.unnoteTCPStreamingOnSocket(fStreamStates[i].tcpSocketNum, this, i); |
| fStreamStates[i].subsession->deleteStream(fOurSessionId, fStreamStates[i].streamToken); |
| } |
| } |
| delete[] fStreamStates; fStreamStates = NULL; |
| fNumStreamStates = 0; |
| } |
| |
| typedef enum StreamingMode { |
| RTP_UDP, |
| RTP_TCP, |
| RAW_UDP |
| } StreamingMode; |
| |
| static void parseTransportHeader(char const* buf, |
| StreamingMode& streamingMode, |
| char*& streamingModeString, |
| char*& destinationAddressStr, |
| u_int8_t& destinationTTL, |
| portNumBits& clientRTPPortNum, // if UDP |
| portNumBits& clientRTCPPortNum, // if UDP |
| unsigned char& rtpChannelId, // if TCP |
| unsigned char& rtcpChannelId // if TCP |
| ) { |
| // Initialize the result parameters to default values: |
| streamingMode = RTP_UDP; |
| streamingModeString = NULL; |
| destinationAddressStr = NULL; |
| destinationTTL = 255; |
| clientRTPPortNum = 0; |
| clientRTCPPortNum = 1; |
| rtpChannelId = rtcpChannelId = 0xFF; |
| |
| portNumBits p1, p2; |
| unsigned ttl, rtpCid, rtcpCid; |
| |
| // First, find "Transport:" |
| while (1) { |
| if (*buf == '\0') return; // not found |
| if (*buf == '\r' && *(buf+1) == '\n' && *(buf+2) == '\r') return; // end of the headers => not found |
| if (_strncasecmp(buf, "Transport:", 10) == 0) break; |
| ++buf; |
| } |
| |
| // Then, run through each of the fields, looking for ones we handle: |
| char const* fields = buf + 10; |
| while (*fields == ' ') ++fields; |
| char* field = strDupSize(fields); |
| while (sscanf(fields, "%[^;\r\n]", field) == 1) { |
| if (strcmp(field, "RTP/AVP/TCP") == 0) { |
| streamingMode = RTP_TCP; |
| } else if (strcmp(field, "RAW/RAW/UDP") == 0 || |
| strcmp(field, "MP2T/H2221/UDP") == 0) { |
| streamingMode = RAW_UDP; |
| streamingModeString = strDup(field); |
| } else if (_strncasecmp(field, "destination=", 12) == 0) { |
| delete[] destinationAddressStr; |
| destinationAddressStr = strDup(field+12); |
| } else if (sscanf(field, "ttl%u", &ttl) == 1) { |
| destinationTTL = (u_int8_t)ttl; |
| } else if (sscanf(field, "client_port=%hu-%hu", &p1, &p2) == 2) { |
| clientRTPPortNum = p1; |
| clientRTCPPortNum = streamingMode == RAW_UDP ? 0 : p2; // ignore the second port number if the client asked for raw UDP |
| } else if (sscanf(field, "client_port=%hu", &p1) == 1) { |
| clientRTPPortNum = p1; |
| clientRTCPPortNum = streamingMode == RAW_UDP ? 0 : p1 + 1; |
| } else if (sscanf(field, "interleaved=%u-%u", &rtpCid, &rtcpCid) == 2) { |
| rtpChannelId = (unsigned char)rtpCid; |
| rtcpChannelId = (unsigned char)rtcpCid; |
| } |
| |
| fields += strlen(field); |
| while (*fields == ';' || *fields == ' ' || *fields == '\t') ++fields; // skip over separating ';' chars or whitespace |
| if (*fields == '\0' || *fields == '\r' || *fields == '\n') break; |
| } |
| delete[] field; |
| } |
| |
| static Boolean parsePlayNowHeader(char const* buf) { |
| // Find "x-playNow:" header, if present |
| while (1) { |
| if (*buf == '\0') return False; // not found |
| if (_strncasecmp(buf, "x-playNow:", 10) == 0) break; |
| ++buf; |
| } |
| |
| return True; |
| } |
| |
| void RTSPServer::RTSPClientSession |
| ::handleCmd_SETUP(RTSPServer::RTSPClientConnection* ourClientConnection, |
| char const* urlPreSuffix, char const* urlSuffix, char const* fullRequestStr) { |
| // Normally, "urlPreSuffix" should be the session (stream) name, and "urlSuffix" should be the subsession (track) name. |
| // However (being "liberal in what we accept"), we also handle 'aggregate' SETUP requests (i.e., without a track name), |
| // in the special case where we have only a single track. I.e., in this case, we also handle: |
| // "urlPreSuffix" is empty and "urlSuffix" is the session (stream) name, or |
| // "urlPreSuffix" concatenated with "urlSuffix" (with "/" inbetween) is the session (stream) name. |
| char const* streamName = urlPreSuffix; // in the normal case |
| char const* trackId = urlSuffix; // in the normal case |
| char* concatenatedStreamName = NULL; // in the normal case |
| |
| do { |
| // First, make sure the specified stream name exists: |
| ServerMediaSession* sms |
| = fOurServer.lookupServerMediaSession(streamName, fOurServerMediaSession == NULL); |
| if (sms == NULL) { |
| // Check for the special case (noted above), before we give up: |
| if (urlPreSuffix[0] == '\0') { |
| streamName = urlSuffix; |
| } else { |
| concatenatedStreamName = new char[strlen(urlPreSuffix) + strlen(urlSuffix) + 2]; // allow for the "/" and the trailing '\0' |
| sprintf(concatenatedStreamName, "%s/%s", urlPreSuffix, urlSuffix); |
| streamName = concatenatedStreamName; |
| } |
| trackId = NULL; |
| |
| // Check again: |
| sms = fOurServer.lookupServerMediaSession(streamName, fOurServerMediaSession == NULL); |
| } |
| if (sms == NULL) { |
| if (fOurServerMediaSession == NULL) { |
| // The client asked for a stream that doesn't exist (and this session descriptor has not been used before): |
| ourClientConnection->handleCmd_notFound(); |
| } else { |
| // The client asked for a stream that doesn't exist, but using a stream id for a stream that does exist. Bad request: |
| ourClientConnection->handleCmd_bad(); |
| } |
| break; |
| } else { |
| if (fOurServerMediaSession == NULL) { |
| // We're accessing the "ServerMediaSession" for the first time. |
| fOurServerMediaSession = sms; |
| fOurServerMediaSession->incrementReferenceCount(); |
| } else if (sms != fOurServerMediaSession) { |
| // The client asked for a stream that's different from the one originally requested for this stream id. Bad request: |
| ourClientConnection->handleCmd_bad(); |
| break; |
| } |
| } |
| |
| if (fStreamStates == NULL) { |
| // This is the first "SETUP" for this session. Set up our array of states for all of this session's subsessions (tracks): |
| fNumStreamStates = fOurServerMediaSession->numSubsessions(); |
| fStreamStates = new struct streamState[fNumStreamStates]; |
| |
| ServerMediaSubsessionIterator iter(*fOurServerMediaSession); |
| ServerMediaSubsession* subsession; |
| for (unsigned i = 0; i < fNumStreamStates; ++i) { |
| subsession = iter.next(); |
| fStreamStates[i].subsession = subsession; |
| fStreamStates[i].tcpSocketNum = -1; // for now; may get set for RTP-over-TCP streaming |
| fStreamStates[i].streamToken = NULL; // for now; it may be changed by the "getStreamParameters()" call that comes later |
| } |
| } |
| |
| // Look up information for the specified subsession (track): |
| ServerMediaSubsession* subsession = NULL; |
| unsigned trackNum; |
| if (trackId != NULL && trackId[0] != '\0') { // normal case |
| for (trackNum = 0; trackNum < fNumStreamStates; ++trackNum) { |
| subsession = fStreamStates[trackNum].subsession; |
| if (subsession != NULL && strcmp(trackId, subsession->trackId()) == 0) break; |
| } |
| if (trackNum >= fNumStreamStates) { |
| // The specified track id doesn't exist, so this request fails: |
| ourClientConnection->handleCmd_notFound(); |
| break; |
| } |
| } else { |
| // Weird case: there was no track id in the URL. |
| // This works only if we have only one subsession: |
| if (fNumStreamStates != 1 || fStreamStates[0].subsession == NULL) { |
| ourClientConnection->handleCmd_bad(); |
| break; |
| } |
| trackNum = 0; |
| subsession = fStreamStates[trackNum].subsession; |
| } |
| // ASSERT: subsession != NULL |
| |
| void*& token = fStreamStates[trackNum].streamToken; // alias |
| if (token != NULL) { |
| // We already handled a "SETUP" for this track (to the same client), |
| // so stop any existing streaming of it, before we set it up again: |
| subsession->pauseStream(fOurSessionId, token); |
| fOurRTSPServer.unnoteTCPStreamingOnSocket(fStreamStates[trackNum].tcpSocketNum, this, trackNum); |
| subsession->deleteStream(fOurSessionId, token); |
| } |
| |
| // Look for a "Transport:" header in the request string, to extract client parameters: |
| StreamingMode streamingMode; |
| char* streamingModeString = NULL; // set when RAW_UDP streaming is specified |
| char* clientsDestinationAddressStr; |
| u_int8_t clientsDestinationTTL; |
| portNumBits clientRTPPortNum, clientRTCPPortNum; |
| unsigned char rtpChannelId, rtcpChannelId; |
| parseTransportHeader(fullRequestStr, streamingMode, streamingModeString, |
| clientsDestinationAddressStr, clientsDestinationTTL, |
| clientRTPPortNum, clientRTCPPortNum, |
| rtpChannelId, rtcpChannelId); |
| if ((streamingMode == RTP_TCP && rtpChannelId == 0xFF) || |
| (streamingMode != RTP_TCP && ourClientConnection->fClientOutputSocket != ourClientConnection->fClientInputSocket)) { |
| // An anomolous situation, caused by a buggy client. Either: |
| // 1/ TCP streaming was requested, but with no "interleaving=" fields. (QuickTime Player sometimes does this.), or |
| // 2/ TCP streaming was not requested, but we're doing RTSP-over-HTTP tunneling (which implies TCP streaming). |
| // In either case, we assume TCP streaming, and set the RTP and RTCP channel ids to proper values: |
| streamingMode = RTP_TCP; |
| rtpChannelId = fTCPStreamIdCount; rtcpChannelId = fTCPStreamIdCount+1; |
| } |
| if (streamingMode == RTP_TCP) fTCPStreamIdCount += 2; |
| |
| Port clientRTPPort(clientRTPPortNum); |
| Port clientRTCPPort(clientRTCPPortNum); |
| |
| // Next, check whether a "Range:" or "x-playNow:" header is present in the request. |
| // This isn't legal, but some clients do this to combine "SETUP" and "PLAY": |
| double rangeStart = 0.0, rangeEnd = 0.0; |
| char* absStart = NULL; char* absEnd = NULL; |
| Boolean startTimeIsNow; |
| if (parseRangeHeader(fullRequestStr, rangeStart, rangeEnd, absStart, absEnd, startTimeIsNow)) { |
| delete[] absStart; delete[] absEnd; |
| fStreamAfterSETUP = True; |
| } else if (parsePlayNowHeader(fullRequestStr)) { |
| fStreamAfterSETUP = True; |
| } else { |
| fStreamAfterSETUP = False; |
| } |
| |
| // Then, get server parameters from the 'subsession': |
| if (streamingMode == RTP_TCP) { |
| // Note that we'll be streaming over the RTSP TCP connection: |
| fStreamStates[trackNum].tcpSocketNum = ourClientConnection->fClientOutputSocket; |
| fOurRTSPServer.noteTCPStreamingOnSocket(fStreamStates[trackNum].tcpSocketNum, this, trackNum); |
| } |
| netAddressBits destinationAddress = 0; |
| u_int8_t destinationTTL = 255; |
| #ifdef RTSP_ALLOW_CLIENT_DESTINATION_SETTING |
| if (clientsDestinationAddressStr != NULL) { |
| // Use the client-provided "destination" address. |
| // Note: This potentially allows the server to be used in denial-of-service |
| // attacks, so don't enable this code unless you're sure that clients are |
| // trusted. |
| destinationAddress = our_inet_addr(clientsDestinationAddressStr); |
| } |
| // Also use the client-provided TTL. |
| destinationTTL = clientsDestinationTTL; |
| #endif |
| delete[] clientsDestinationAddressStr; |
| Port serverRTPPort(0); |
| Port serverRTCPPort(0); |
| |
| // Make sure that we transmit on the same interface that's used by the client (in case we're a multi-homed server): |
| struct sockaddr_in sourceAddr; SOCKLEN_T namelen = sizeof sourceAddr; |
| getsockname(ourClientConnection->fClientInputSocket, (struct sockaddr*)&sourceAddr, &namelen); |
| netAddressBits origSendingInterfaceAddr = SendingInterfaceAddr; |
| netAddressBits origReceivingInterfaceAddr = ReceivingInterfaceAddr; |
| // NOTE: The following might not work properly, so we ifdef it out for now: |
| #ifdef HACK_FOR_MULTIHOMED_SERVERS |
| ReceivingInterfaceAddr = SendingInterfaceAddr = sourceAddr.sin_addr.s_addr; |
| #endif |
| |
| subsession->getStreamParameters(fOurSessionId, ourClientConnection->fClientAddr.sin_addr.s_addr, |
| clientRTPPort, clientRTCPPort, |
| fStreamStates[trackNum].tcpSocketNum, rtpChannelId, rtcpChannelId, |
| destinationAddress, destinationTTL, fIsMulticast, |
| serverRTPPort, serverRTCPPort, |
| fStreamStates[trackNum].streamToken); |
| SendingInterfaceAddr = origSendingInterfaceAddr; |
| ReceivingInterfaceAddr = origReceivingInterfaceAddr; |
| |
| AddressString destAddrStr(destinationAddress); |
| AddressString sourceAddrStr(sourceAddr); |
| char timeoutParameterString[100]; |
| if (fOurRTSPServer.fReclamationSeconds > 0) { |
| sprintf(timeoutParameterString, ";timeout=%u", fOurRTSPServer.fReclamationSeconds); |
| } else { |
| timeoutParameterString[0] = '\0'; |
| } |
| if (fIsMulticast) { |
| switch (streamingMode) { |
| case RTP_UDP: { |
| snprintf((char*)ourClientConnection->fResponseBuffer, sizeof ourClientConnection->fResponseBuffer, |
| "RTSP/1.0 200 OK\r\n" |
| "CSeq: %s\r\n" |
| "%s" |
| "Transport: RTP/AVP;multicast;destination=%s;source=%s;port=%d-%d;ttl=%d\r\n" |
| "Session: %08X%s\r\n\r\n", |
| ourClientConnection->fCurrentCSeq, |
| dateHeader(), |
| destAddrStr.val(), sourceAddrStr.val(), ntohs(serverRTPPort.num()), ntohs(serverRTCPPort.num()), destinationTTL, |
| fOurSessionId, timeoutParameterString); |
| break; |
| } |
| case RTP_TCP: { |
| // multicast streams can't be sent via TCP |
| ourClientConnection->handleCmd_unsupportedTransport(); |
| break; |
| } |
| case RAW_UDP: { |
| snprintf((char*)ourClientConnection->fResponseBuffer, sizeof ourClientConnection->fResponseBuffer, |
| "RTSP/1.0 200 OK\r\n" |
| "CSeq: %s\r\n" |
| "%s" |
| "Transport: %s;multicast;destination=%s;source=%s;port=%d;ttl=%d\r\n" |
| "Session: %08X%s\r\n\r\n", |
| ourClientConnection->fCurrentCSeq, |
| dateHeader(), |
| streamingModeString, destAddrStr.val(), sourceAddrStr.val(), ntohs(serverRTPPort.num()), destinationTTL, |
| fOurSessionId, timeoutParameterString); |
| break; |
| } |
| } |
| } else { |
| switch (streamingMode) { |
| case RTP_UDP: { |
| snprintf((char*)ourClientConnection->fResponseBuffer, sizeof ourClientConnection->fResponseBuffer, |
| "RTSP/1.0 200 OK\r\n" |
| "CSeq: %s\r\n" |
| "%s" |
| "Transport: RTP/AVP;unicast;destination=%s;source=%s;client_port=%d-%d;server_port=%d-%d\r\n" |
| "Session: %08X%s\r\n\r\n", |
| ourClientConnection->fCurrentCSeq, |
| dateHeader(), |
| destAddrStr.val(), sourceAddrStr.val(), ntohs(clientRTPPort.num()), ntohs(clientRTCPPort.num()), ntohs(serverRTPPort.num()), ntohs(serverRTCPPort.num()), |
| fOurSessionId, timeoutParameterString); |
| break; |
| } |
| case RTP_TCP: { |
| if (!fOurRTSPServer.fAllowStreamingRTPOverTCP) { |
| ourClientConnection->handleCmd_unsupportedTransport(); |
| } else { |
| snprintf((char*)ourClientConnection->fResponseBuffer, sizeof ourClientConnection->fResponseBuffer, |
| "RTSP/1.0 200 OK\r\n" |
| "CSeq: %s\r\n" |
| "%s" |
| "Transport: RTP/AVP/TCP;unicast;destination=%s;source=%s;interleaved=%d-%d\r\n" |
| "Session: %08X%s\r\n\r\n", |
| ourClientConnection->fCurrentCSeq, |
| dateHeader(), |
| destAddrStr.val(), sourceAddrStr.val(), rtpChannelId, rtcpChannelId, |
| fOurSessionId, timeoutParameterString); |
| } |
| break; |
| } |
| case RAW_UDP: { |
| snprintf((char*)ourClientConnection->fResponseBuffer, sizeof ourClientConnection->fResponseBuffer, |
| "RTSP/1.0 200 OK\r\n" |
| "CSeq: %s\r\n" |
| "%s" |
| "Transport: %s;unicast;destination=%s;source=%s;client_port=%d;server_port=%d\r\n" |
| "Session: %08X%s\r\n\r\n", |
| ourClientConnection->fCurrentCSeq, |
| dateHeader(), |
| streamingModeString, destAddrStr.val(), sourceAddrStr.val(), ntohs(clientRTPPort.num()), ntohs(serverRTPPort.num()), |
| fOurSessionId, timeoutParameterString); |
| break; |
| } |
| } |
| } |
| delete[] streamingModeString; |
| } while (0); |
| |
| delete[] concatenatedStreamName; |
| } |
| |
| void RTSPServer::RTSPClientSession |
| ::handleCmd_withinSession(RTSPServer::RTSPClientConnection* ourClientConnection, |
| char const* cmdName, |
| char const* urlPreSuffix, char const* urlSuffix, |
| char const* fullRequestStr) { |
| // This will either be: |
| // - a non-aggregated operation, if "urlPreSuffix" is the session (stream) |
| // name and "urlSuffix" is the subsession (track) name, or |
| // - an aggregated operation, if "urlSuffix" is the session (stream) name, |
| // or "urlPreSuffix" is the session (stream) name, and "urlSuffix" is empty, |
| // or "urlPreSuffix" and "urlSuffix" are both nonempty, but when concatenated, (with "/") form the session (stream) name. |
| // Begin by figuring out which of these it is: |
| ServerMediaSubsession* subsession; |
| |
| if (fOurServerMediaSession == NULL) { // There wasn't a previous SETUP! |
| ourClientConnection->handleCmd_notSupported(); |
| return; |
| } else if (urlSuffix[0] != '\0' && strcmp(fOurServerMediaSession->streamName(), urlPreSuffix) == 0) { |
| // Non-aggregated operation. |
| // Look up the media subsession whose track id is "urlSuffix": |
| ServerMediaSubsessionIterator iter(*fOurServerMediaSession); |
| while ((subsession = iter.next()) != NULL) { |
| if (strcmp(subsession->trackId(), urlSuffix) == 0) break; // success |
| } |
| if (subsession == NULL) { // no such track! |
| ourClientConnection->handleCmd_notFound(); |
| return; |
| } |
| } else if (strcmp(fOurServerMediaSession->streamName(), urlSuffix) == 0 || |
| (urlSuffix[0] == '\0' && strcmp(fOurServerMediaSession->streamName(), urlPreSuffix) == 0)) { |
| // Aggregated operation |
| subsession = NULL; |
| } else if (urlPreSuffix[0] != '\0' && urlSuffix[0] != '\0') { |
| // Aggregated operation, if <urlPreSuffix>/<urlSuffix> is the session (stream) name: |
| unsigned const urlPreSuffixLen = strlen(urlPreSuffix); |
| if (strncmp(fOurServerMediaSession->streamName(), urlPreSuffix, urlPreSuffixLen) == 0 && |
| fOurServerMediaSession->streamName()[urlPreSuffixLen] == '/' && |
| strcmp(&(fOurServerMediaSession->streamName())[urlPreSuffixLen+1], urlSuffix) == 0) { |
| subsession = NULL; |
| } else { |
| ourClientConnection->handleCmd_notFound(); |
| return; |
| } |
| } else { // the request doesn't match a known stream and/or track at all! |
| ourClientConnection->handleCmd_notFound(); |
| return; |
| } |
| |
| if (strcmp(cmdName, "TEARDOWN") == 0) { |
| handleCmd_TEARDOWN(ourClientConnection, subsession); |
| } else if (strcmp(cmdName, "PLAY") == 0) { |
| handleCmd_PLAY(ourClientConnection, subsession, fullRequestStr); |
| } else if (strcmp(cmdName, "PAUSE") == 0) { |
| handleCmd_PAUSE(ourClientConnection, subsession); |
| } else if (strcmp(cmdName, "GET_PARAMETER") == 0) { |
| handleCmd_GET_PARAMETER(ourClientConnection, subsession, fullRequestStr); |
| } else if (strcmp(cmdName, "SET_PARAMETER") == 0) { |
| handleCmd_SET_PARAMETER(ourClientConnection, subsession, fullRequestStr); |
| } |
| } |
| |
| void RTSPServer::RTSPClientSession |
| ::handleCmd_TEARDOWN(RTSPServer::RTSPClientConnection* ourClientConnection, |
| ServerMediaSubsession* subsession) { |
| unsigned i; |
| for (i = 0; i < fNumStreamStates; ++i) { |
| if (subsession == NULL /* means: aggregated operation */ |
| || subsession == fStreamStates[i].subsession) { |
| if (fStreamStates[i].subsession != NULL) { |
| fOurRTSPServer.unnoteTCPStreamingOnSocket(fStreamStates[i].tcpSocketNum, this, i); |
| fStreamStates[i].subsession->deleteStream(fOurSessionId, fStreamStates[i].streamToken); |
| fStreamStates[i].subsession = NULL; |
| } |
| } |
| } |
| |
| setRTSPResponse(ourClientConnection, "200 OK"); |
| |
| // Optimization: If all subsessions have now been torn down, then we know that we can reclaim our object now. |
| // (Without this optimization, however, this object would still get reclaimed later, as a result of a 'liveness' timeout.) |
| Boolean noSubsessionsRemain = True; |
| for (i = 0; i < fNumStreamStates; ++i) { |
| if (fStreamStates[i].subsession != NULL) { |
| noSubsessionsRemain = False; |
| break; |
| } |
| } |
| if (noSubsessionsRemain) delete this; |
| } |
| |
| void RTSPServer::RTSPClientSession |
| ::handleCmd_PLAY(RTSPServer::RTSPClientConnection* ourClientConnection, |
| ServerMediaSubsession* subsession, char const* fullRequestStr) { |
| char* rtspURL |
| = fOurRTSPServer.rtspURL(fOurServerMediaSession, ourClientConnection->fClientInputSocket); |
| unsigned rtspURLSize = strlen(rtspURL); |
| |
| // Parse the client's "Scale:" header, if any: |
| float scale; |
| Boolean sawScaleHeader = parseScaleHeader(fullRequestStr, scale); |
| |
| // Try to set the stream's scale factor to this value: |
| if (subsession == NULL /*aggregate op*/) { |
| fOurServerMediaSession->testScaleFactor(scale); |
| } else { |
| subsession->testScaleFactor(scale); |
| } |
| |
| char buf[100]; |
| char* scaleHeader; |
| if (!sawScaleHeader) { |
| buf[0] = '\0'; // Because we didn't see a Scale: header, don't send one back |
| } else { |
| sprintf(buf, "Scale: %f\r\n", scale); |
| } |
| scaleHeader = strDup(buf); |
| |
| // Parse the client's "Range:" header, if any: |
| float duration = 0.0; |
| double rangeStart = 0.0, rangeEnd = 0.0; |
| char* absStart = NULL; char* absEnd = NULL; |
| Boolean startTimeIsNow; |
| Boolean sawRangeHeader |
| = parseRangeHeader(fullRequestStr, rangeStart, rangeEnd, absStart, absEnd, startTimeIsNow); |
| |
| if (sawRangeHeader && absStart == NULL/*not seeking by 'absolute' time*/) { |
| // Use this information, plus the stream's duration (if known), to create our own "Range:" header, for the response: |
| duration = subsession == NULL /*aggregate op*/ |
| ? fOurServerMediaSession->duration() : subsession->duration(); |
| if (duration < 0.0) { |
| // We're an aggregate PLAY, but the subsessions have different durations. |
| // Use the largest of these durations in our header |
| duration = -duration; |
| } |
| |
| // Make sure that "rangeStart" and "rangeEnd" (from the client's "Range:" header) |
| // have sane values, before we send back our own "Range:" header in our response: |
| if (rangeStart < 0.0) rangeStart = 0.0; |
| else if (rangeStart > duration) rangeStart = duration; |
| if (rangeEnd < 0.0) rangeEnd = 0.0; |
| else if (rangeEnd > duration) rangeEnd = duration; |
| if ((scale > 0.0 && rangeStart > rangeEnd && rangeEnd > 0.0) || |
| (scale < 0.0 && rangeStart < rangeEnd)) { |
| // "rangeStart" and "rangeEnd" were the wrong way around; swap them: |
| double tmp = rangeStart; |
| rangeStart = rangeEnd; |
| rangeEnd = tmp; |
| } |
| } |
| |
| // Create a "RTP-Info:" line. It will get filled in from each subsession's state: |
| char const* rtpInfoFmt = |
| "%s" // "RTP-Info:", plus any preceding rtpInfo items |
| "%s" // comma separator, if needed |
| "url=%s/%s" |
| ";seq=%d" |
| ";rtptime=%u" |
| ; |
| unsigned rtpInfoFmtSize = strlen(rtpInfoFmt); |
| char* rtpInfo = strDup("RTP-Info: "); |
| unsigned i, numRTPInfoItems = 0; |
| |
| // Do any required seeking/scaling on each subsession, before starting streaming. |
| // (However, we don't do this if the "PLAY" request was for just a single subsession |
| // of a multiple-subsession stream; for such streams, seeking/scaling can be done |
| // only with an aggregate "PLAY".) |
| for (i = 0; i < fNumStreamStates; ++i) { |
| if (subsession == NULL /* means: aggregated operation */ || fNumStreamStates == 1) { |
| if (fStreamStates[i].subsession != NULL) { |
| if (sawScaleHeader) { |
| fStreamStates[i].subsession->setStreamScale(fOurSessionId, fStreamStates[i].streamToken, scale); |
| } |
| if (absStart != NULL) { |
| // Special case handling for seeking by 'absolute' time: |
| |
| fStreamStates[i].subsession->seekStream(fOurSessionId, fStreamStates[i].streamToken, absStart, absEnd); |
| } else { |
| // Seeking by relative (NPT) time: |
| |
| u_int64_t numBytes; |
| if (!sawRangeHeader || startTimeIsNow) { |
| // We're resuming streaming without seeking, so we just do a 'null' seek |
| // (to get our NPT, and to specify when to end streaming): |
| fStreamStates[i].subsession->nullSeekStream(fOurSessionId, fStreamStates[i].streamToken, |
| rangeEnd, numBytes); |
| } else { |
| // We do a real 'seek': |
| double streamDuration = 0.0; // by default; means: stream until the end of the media |
| if (rangeEnd > 0.0 && (rangeEnd+0.001) < duration) { |
| // the 0.001 is because we limited the values to 3 decimal places |
| // We want the stream to end early. Set the duration we want: |
| streamDuration = rangeEnd - rangeStart; |
| if (streamDuration < 0.0) streamDuration = -streamDuration; |
| // should happen only if scale < 0.0 |
| } |
| fStreamStates[i].subsession->seekStream(fOurSessionId, fStreamStates[i].streamToken, |
| rangeStart, streamDuration, numBytes); |
| } |
| } |
| } |
| } |
| } |
| |
| // Create the "Range:" header that we'll send back in our response. |
| // (Note that we do this after seeking, in case the seeking operation changed the range start time.) |
| if (absStart != NULL) { |
| // We're seeking by 'absolute' time: |
| if (absEnd == NULL) { |
| sprintf(buf, "Range: clock=%s-\r\n", absStart); |
| } else { |
| sprintf(buf, "Range: clock=%s-%s\r\n", absStart, absEnd); |
| } |
| delete[] absStart; delete[] absEnd; |
| } else { |
| // We're seeking by relative (NPT) time: |
| if (!sawRangeHeader || startTimeIsNow) { |
| // We didn't seek, so in our response, begin the range with the current NPT (normal play time): |
| float curNPT = 0.0; |
| for (i = 0; i < fNumStreamStates; ++i) { |
| if (subsession == NULL /* means: aggregated operation */ |
| || subsession == fStreamStates[i].subsession) { |
| if (fStreamStates[i].subsession == NULL) continue; |
| float npt = fStreamStates[i].subsession->getCurrentNPT(fStreamStates[i].streamToken); |
| if (npt > curNPT) curNPT = npt; |
| // Note: If this is an aggregate "PLAY" on a multi-subsession stream, |
| // then it's conceivable that the NPTs of each subsession may differ |
| // (if there has been a previous seek on just one subsession). |
| // In this (unusual) case, we just return the largest NPT; I hope that turns out OK... |
| } |
| } |
| rangeStart = curNPT; |
| } |
| |
| if (rangeEnd == 0.0 && scale >= 0.0) { |
| sprintf(buf, "Range: npt=%.3f-\r\n", rangeStart); |
| } else { |
| sprintf(buf, "Range: npt=%.3f-%.3f\r\n", rangeStart, rangeEnd); |
| } |
| } |
| char* rangeHeader = strDup(buf); |
| |
| // Now, start streaming: |
| for (i = 0; i < fNumStreamStates; ++i) { |
| if (subsession == NULL /* means: aggregated operation */ |
| || subsession == fStreamStates[i].subsession) { |
| unsigned short rtpSeqNum = 0; |
| unsigned rtpTimestamp = 0; |
| if (fStreamStates[i].subsession == NULL) continue; |
| fStreamStates[i].subsession->startStream(fOurSessionId, |
| fStreamStates[i].streamToken, |
| (TaskFunc*)noteClientLiveness, this, |
| rtpSeqNum, rtpTimestamp, |
| RTSPServer::RTSPClientConnection::handleAlternativeRequestByte, ourClientConnection); |
| const char *urlSuffix = fStreamStates[i].subsession->trackId(); |
| char* prevRTPInfo = rtpInfo; |
| unsigned rtpInfoSize = rtpInfoFmtSize |
| + strlen(prevRTPInfo) |
| + 1 |
| + rtspURLSize + strlen(urlSuffix) |
| + 5 /*max unsigned short len*/ |
| + 10 /*max unsigned (32-bit) len*/ |
| + 2 /*allows for trailing \r\n at final end of string*/; |
| rtpInfo = new char[rtpInfoSize]; |
| sprintf(rtpInfo, rtpInfoFmt, |
| prevRTPInfo, |
| numRTPInfoItems++ == 0 ? "" : ",", |
| rtspURL, urlSuffix, |
| rtpSeqNum, |
| rtpTimestamp |
| ); |
| delete[] prevRTPInfo; |
| } |
| } |
| if (numRTPInfoItems == 0) { |
| rtpInfo[0] = '\0'; |
| } else { |
| unsigned rtpInfoLen = strlen(rtpInfo); |
| rtpInfo[rtpInfoLen] = '\r'; |
| rtpInfo[rtpInfoLen+1] = '\n'; |
| rtpInfo[rtpInfoLen+2] = '\0'; |
| } |
| |
| // Fill in the response: |
| snprintf((char*)ourClientConnection->fResponseBuffer, sizeof ourClientConnection->fResponseBuffer, |
| "RTSP/1.0 200 OK\r\n" |
| "CSeq: %s\r\n" |
| "%s" |
| "%s" |
| "%s" |
| "Session: %08X\r\n" |
| "%s\r\n", |
| ourClientConnection->fCurrentCSeq, |
| dateHeader(), |
| scaleHeader, |
| rangeHeader, |
| fOurSessionId, |
| rtpInfo); |
| delete[] rtpInfo; delete[] rangeHeader; |
| delete[] scaleHeader; delete[] rtspURL; |
| } |
| |
| void RTSPServer::RTSPClientSession |
| ::handleCmd_PAUSE(RTSPServer::RTSPClientConnection* ourClientConnection, |
| ServerMediaSubsession* subsession) { |
| for (unsigned i = 0; i < fNumStreamStates; ++i) { |
| if (subsession == NULL /* means: aggregated operation */ |
| || subsession == fStreamStates[i].subsession) { |
| if (fStreamStates[i].subsession != NULL) { |
| fStreamStates[i].subsession->pauseStream(fOurSessionId, fStreamStates[i].streamToken); |
| } |
| } |
| } |
| |
| setRTSPResponse(ourClientConnection, "200 OK", fOurSessionId); |
| } |
| |
| void RTSPServer::RTSPClientSession |
| ::handleCmd_GET_PARAMETER(RTSPServer::RTSPClientConnection* ourClientConnection, |
| ServerMediaSubsession* /*subsession*/, char const* /*fullRequestStr*/) { |
| // By default, we implement "GET_PARAMETER" just as a 'keep alive', and send back a dummy response. |
| // (If you want to handle "GET_PARAMETER" properly, you can do so by defining a subclass of "RTSPServer" |
| // and "RTSPServer::RTSPClientSession", and then reimplement this virtual function in your subclass.) |
| setRTSPResponse(ourClientConnection, "200 OK", fOurSessionId, LIVEMEDIA_LIBRARY_VERSION_STRING); |
| } |
| |
| void RTSPServer::RTSPClientSession |
| ::handleCmd_SET_PARAMETER(RTSPServer::RTSPClientConnection* ourClientConnection, |
| ServerMediaSubsession* /*subsession*/, char const* /*fullRequestStr*/) { |
| // By default, we implement "SET_PARAMETER" just as a 'keep alive', and send back an empty response. |
| // (If you want to handle "SET_PARAMETER" properly, you can do so by defining a subclass of "RTSPServer" |
| // and "RTSPServer::RTSPClientSession", and then reimplement this virtual function in your subclass.) |
| setRTSPResponse(ourClientConnection, "200 OK", fOurSessionId); |
| } |
| |
| GenericMediaServer::ClientConnection* |
| RTSPServer::createNewClientConnection(int clientSocket, struct sockaddr_in clientAddr) { |
| return new RTSPClientConnection(*this, clientSocket, clientAddr); |
| } |
| |
| GenericMediaServer::ClientSession* |
| RTSPServer::createNewClientSession(u_int32_t sessionId) { |
| return new RTSPClientSession(*this, sessionId); |
| } |