michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim:set ts=2 sw=2 sts=2 et cindent: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: #include "mozilla/Assertions.h" michael@0: #include "mozilla/Base64.h" michael@0: #include "nsThreadUtils.h" michael@0: #include "nsIServiceManager.h" michael@0: #include "nsISocketTransport.h" michael@0: #include "nsIOutputStream.h" michael@0: #include "nsIInputStream.h" michael@0: #include "nsIRandomGenerator.h" michael@0: #include "nsReadLine.h" michael@0: #include "nsNetCID.h" michael@0: #include "VideoUtils.h" michael@0: #include "MediaResource.h" michael@0: #include "MediaResourceServer.h" michael@0: michael@0: #if defined(_MSC_VER) michael@0: #define strtoll _strtoi64 michael@0: #define snprintf _snprintf_s michael@0: #endif michael@0: michael@0: using namespace mozilla; michael@0: michael@0: /* michael@0: ReadCRLF is a variant of NS_ReadLine from nsReadLine.h that deals michael@0: with the carriage return/line feed requirements of HTTP requests. michael@0: */ michael@0: template michael@0: nsresult michael@0: ReadCRLF (StreamType* aStream, nsLineBuffer * aBuffer, michael@0: StringType & aLine, bool *aMore) michael@0: { michael@0: // eollast is true if the last character in the buffer is a '\r', michael@0: // signaling a potential '\r\n' sequence split between reads. michael@0: bool eollast = false; michael@0: michael@0: aLine.Truncate(); michael@0: michael@0: while (1) { // will be returning out of this loop on eol or eof michael@0: if (aBuffer->start == aBuffer->end) { // buffer is empty. Read into it. michael@0: uint32_t bytesRead; michael@0: nsresult rv = aStream->Read(aBuffer->buf, kLineBufferSize, &bytesRead); michael@0: if (NS_FAILED(rv) || bytesRead == 0) { michael@0: *aMore = false; michael@0: return rv; michael@0: } michael@0: aBuffer->start = aBuffer->buf; michael@0: aBuffer->end = aBuffer->buf + bytesRead; michael@0: *(aBuffer->end) = '\0'; michael@0: } michael@0: michael@0: /* michael@0: * Walk the buffer looking for an end-of-line. michael@0: * There are 4 cases to consider: michael@0: * 1. the CR char is the last char in the buffer michael@0: * 2. the CRLF sequence are the last characters in the buffer michael@0: * 3. the CRLF sequence + one or more chars at the end of the buffer michael@0: * we need at least one char after the first CRLF sequence to michael@0: * set |aMore| correctly. michael@0: * 4. The LF character is the first char in the buffer when eollast is michael@0: * true. michael@0: */ michael@0: CharT* current = aBuffer->start; michael@0: if (eollast) { // Case 4 michael@0: if (*current == '\n') { michael@0: aBuffer->start = ++current; michael@0: *aMore = true; michael@0: return NS_OK; michael@0: } michael@0: else { michael@0: eollast = false; michael@0: aLine.Append('\r'); michael@0: } michael@0: } michael@0: // Cases 2 and 3 michael@0: for ( ; current < aBuffer->end-1; ++current) { michael@0: if (*current == '\r' && *(current+1) == '\n') { michael@0: *current++ = '\0'; michael@0: *current++ = '\0'; michael@0: aLine.Append(aBuffer->start); michael@0: aBuffer->start = current; michael@0: *aMore = true; michael@0: return NS_OK; michael@0: } michael@0: } michael@0: // Case 1 michael@0: if (*current == '\r') { michael@0: eollast = true; michael@0: *current++ = '\0'; michael@0: } michael@0: michael@0: aLine.Append(aBuffer->start); michael@0: aBuffer->start = aBuffer->end; // mark the buffer empty michael@0: } michael@0: } michael@0: michael@0: // Each client HTTP request results in a thread being spawned to process it. michael@0: // That thread has a single event dispatched to it which handles the HTTP michael@0: // protocol. It parses the headers and forwards data from the MediaResource michael@0: // associated with the URL back to client. When the request is complete it will michael@0: // shutdown the thread. michael@0: class ServeResourceEvent : public nsRunnable { michael@0: private: michael@0: // Reading from this reads the data sent from the client. michael@0: nsCOMPtr mInput; michael@0: michael@0: // Writing to this sends data to the client. michael@0: nsCOMPtr mOutput; michael@0: michael@0: // The MediaResourceServer that owns the MediaResource instances michael@0: // served. This is used to lookup the MediaResource from the URL. michael@0: nsRefPtr mServer; michael@0: michael@0: // Write 'aBufferLength' bytes from 'aBuffer' to 'mOutput'. This michael@0: // method ensures all the data is written by checking the number michael@0: // of bytes returned from the output streams 'Write' method and michael@0: // looping until done. michael@0: nsresult WriteAll(char const* aBuffer, int32_t aBufferLength); michael@0: michael@0: public: michael@0: ServeResourceEvent(nsIInputStream* aInput, nsIOutputStream* aOutput, michael@0: MediaResourceServer* aServer) michael@0: : mInput(aInput), mOutput(aOutput), mServer(aServer) {} michael@0: michael@0: // This method runs on the thread and exits when it has completed the michael@0: // HTTP request. michael@0: NS_IMETHOD Run(); michael@0: michael@0: // Given the first line of an HTTP request, parse the URL requested and michael@0: // return the MediaResource for that URL. michael@0: already_AddRefed GetMediaResource(nsCString const& aHTTPRequest); michael@0: michael@0: // Gracefully shutdown the thread and cleanup resources michael@0: void Shutdown(); michael@0: }; michael@0: michael@0: nsresult michael@0: ServeResourceEvent::WriteAll(char const* aBuffer, int32_t aBufferLength) michael@0: { michael@0: while (aBufferLength > 0) { michael@0: uint32_t written = 0; michael@0: nsresult rv = mOutput->Write(aBuffer, aBufferLength, &written); michael@0: if (NS_FAILED (rv)) return rv; michael@0: michael@0: aBufferLength -= written; michael@0: aBuffer += written; michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: already_AddRefed michael@0: ServeResourceEvent::GetMediaResource(nsCString const& aHTTPRequest) michael@0: { michael@0: // Check that the HTTP method is GET michael@0: const char* HTTP_METHOD = "GET "; michael@0: if (strncmp(aHTTPRequest.get(), HTTP_METHOD, strlen(HTTP_METHOD)) != 0) { michael@0: return nullptr; michael@0: } michael@0: michael@0: const char* url_start = strchr(aHTTPRequest.get(), ' '); michael@0: if (!url_start) { michael@0: return nullptr; michael@0: } michael@0: michael@0: const char* url_end = strrchr(++url_start, ' '); michael@0: if (!url_end) { michael@0: return nullptr; michael@0: } michael@0: michael@0: // The path extracted from the HTTP request is used as a key in hash michael@0: // table. It is not related to retrieving data from the filesystem so michael@0: // we don't need to do any sanity checking on ".." paths and similar michael@0: // exploits. michael@0: nsCString relative(url_start, url_end - url_start); michael@0: nsRefPtr resource = michael@0: mServer->GetResource(mServer->GetURLPrefix() + relative); michael@0: return resource.forget(); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: ServeResourceEvent::Run() { michael@0: bool more = false; // Are there HTTP headers to read after the first line michael@0: nsCString line; // Contains the current line read from input stream michael@0: nsLineBuffer* buffer = new nsLineBuffer(); michael@0: nsresult rv = ReadCRLF(mInput.get(), buffer, line, &more); michael@0: if (NS_FAILED(rv)) { Shutdown(); return rv; } michael@0: michael@0: // First line contains the HTTP GET request. Extract the URL and obtain michael@0: // the MediaResource for it. michael@0: nsRefPtr resource = GetMediaResource(line); michael@0: if (!resource) { michael@0: const char* response_404 = "HTTP/1.1 404 Not Found\r\n" michael@0: "Content-Length: 0\r\n\r\n"; michael@0: rv = WriteAll(response_404, strlen(response_404)); michael@0: Shutdown(); michael@0: return rv; michael@0: } michael@0: michael@0: // Offset in bytes to start reading from resource. michael@0: // This is zero by default but can be set to another starting value if michael@0: // this HTTP request includes a byte range request header. michael@0: int64_t start = 0; michael@0: michael@0: // Keep reading lines until we get a zero length line, which is the HTTP michael@0: // protocol's way of signifying the end of headers and start of body, or michael@0: // until we have no more data to read. michael@0: while (more && line.Length() > 0) { michael@0: rv = ReadCRLF(mInput.get(), buffer, line, &more); michael@0: if (NS_FAILED(rv)) { Shutdown(); return rv; } michael@0: michael@0: // Look for a byte range request header. If there is one, set the michael@0: // media resource offset to start from to that requested. Here we michael@0: // only check for the range request format used by Android rather michael@0: // than implementing all possibilities in the HTTP specification. michael@0: // That is, the range request is of the form: michael@0: // Range: bytes=nnnn- michael@0: // Were 'nnnn' is an integer number. michael@0: // The end of the range is not checked, instead we return up to michael@0: // the end of the resource and the client is informed of this via michael@0: // the content-range header. michael@0: NS_NAMED_LITERAL_CSTRING(byteRange, "Range: bytes="); michael@0: const char* s = strstr(line.get(), byteRange.get()); michael@0: if (s) { michael@0: start = strtoll(s+byteRange.Length(), nullptr, 10); michael@0: michael@0: // Clamp 'start' to be between 0 and the resource length. michael@0: start = std::max(0ll, std::min(resource->GetLength(), start)); michael@0: } michael@0: } michael@0: michael@0: // HTTP response to use if this is a non byte range request michael@0: const char* response_normal = "HTTP/1.1 200 OK\r\n"; michael@0: michael@0: // HTTP response to use if this is a byte range request michael@0: const char* response_range = "HTTP/1.1 206 Partial Content\r\n"; michael@0: michael@0: // End of HTTP reponse headers is indicated by an empty line. michael@0: const char* response_end = "\r\n"; michael@0: michael@0: // If the request was a byte range request, we need to read from the michael@0: // requested offset. If the resource is non-seekable, or the seek michael@0: // fails, then the start offset is set back to zero. This results in all michael@0: // HTTP response data being as if the byte range request was not made. michael@0: if (start > 0 && !resource->IsTransportSeekable()) { michael@0: start = 0; michael@0: } michael@0: michael@0: const char* response_line = start > 0 ? michael@0: response_range : michael@0: response_normal; michael@0: rv = WriteAll(response_line, strlen(response_line)); michael@0: if (NS_FAILED(rv)) { Shutdown(); return NS_OK; } michael@0: michael@0: // Buffer used for reading from the input stream and writing to michael@0: // the output stream. The buffer size should be big enough for the michael@0: // HTTP response headers sent below. A static_assert ensures michael@0: // this where the buffer is used. michael@0: const int buffer_size = 32768; michael@0: nsAutoArrayPtr b(new char[buffer_size]); michael@0: michael@0: // If we know the length of the resource, send a Content-Length header. michael@0: int64_t contentlength = resource->GetLength() - start; michael@0: if (contentlength > 0) { michael@0: static_assert (buffer_size > 1024, michael@0: "buffer_size must be large enough " michael@0: "to hold response headers"); michael@0: snprintf(b, buffer_size, "Content-Length: %lld\r\n", contentlength); michael@0: rv = WriteAll(b, strlen(b)); michael@0: if (NS_FAILED(rv)) { Shutdown(); return NS_OK; } michael@0: } michael@0: michael@0: // If the request was a byte range request, respond with a Content-Range michael@0: // header which details the extent of the data returned. michael@0: if (start > 0) { michael@0: static_assert (buffer_size > 1024, michael@0: "buffer_size must be large enough " michael@0: "to hold response headers"); michael@0: snprintf(b, buffer_size, "Content-Range: bytes %lld-%lld/%lld\r\n", michael@0: start, resource->GetLength() - 1, resource->GetLength()); michael@0: rv = WriteAll(b, strlen(b)); michael@0: if (NS_FAILED(rv)) { Shutdown(); return NS_OK; } michael@0: } michael@0: michael@0: rv = WriteAll(response_end, strlen(response_end)); michael@0: if (NS_FAILED(rv)) { Shutdown(); return NS_OK; } michael@0: michael@0: rv = mOutput->Flush(); michael@0: if (NS_FAILED(rv)) { Shutdown(); return NS_OK; } michael@0: michael@0: // Read data from media resource michael@0: uint32_t bytesRead = 0; // Number of bytes read/written to streams michael@0: rv = resource->ReadAt(start, b, buffer_size, &bytesRead); michael@0: while (NS_SUCCEEDED(rv) && bytesRead != 0) { michael@0: // Keep track of what we think the starting position for the next read michael@0: // is. This is used in subsequent ReadAt calls to ensure we are reading michael@0: // from the correct offset in the case where another thread is reading michael@0: // from th same MediaResource. michael@0: start += bytesRead; michael@0: michael@0: // Write data obtained from media resource to output stream michael@0: rv = WriteAll(b, bytesRead); michael@0: if (NS_FAILED (rv)) break; michael@0: michael@0: rv = resource->ReadAt(start, b, 32768, &bytesRead); michael@0: } michael@0: michael@0: Shutdown(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: void michael@0: ServeResourceEvent::Shutdown() michael@0: { michael@0: // Cleanup resources and exit. michael@0: mInput->Close(); michael@0: mOutput->Close(); michael@0: michael@0: // To shutdown the current thread we need to first exit this event. michael@0: // The Shutdown event below is posted to the main thread to do this. michael@0: nsCOMPtr event = new ShutdownThreadEvent(NS_GetCurrentThread()); michael@0: NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); michael@0: } michael@0: michael@0: /* michael@0: This is the listener attached to the server socket. When an HTTP michael@0: request is made by the client the OnSocketAccepted method is michael@0: called. This method will spawn a thread to process the request. michael@0: The thread receives a single event which does the parsing of michael@0: the HTTP request and forwarding the data from the MediaResource michael@0: to the output stream of the request. michael@0: michael@0: The MediaResource used for providing the request data is obtained michael@0: from the MediaResourceServer that created this listener, using the michael@0: URL the client requested. michael@0: */ michael@0: class ResourceSocketListener : public nsIServerSocketListener michael@0: { michael@0: public: michael@0: // The MediaResourceServer used to look up the MediaResource michael@0: // on requests. michael@0: nsRefPtr mServer; michael@0: michael@0: public: michael@0: NS_DECL_THREADSAFE_ISUPPORTS michael@0: NS_DECL_NSISERVERSOCKETLISTENER michael@0: michael@0: ResourceSocketListener(MediaResourceServer* aServer) : michael@0: mServer(aServer) michael@0: { michael@0: } michael@0: michael@0: virtual ~ResourceSocketListener() { } michael@0: }; michael@0: michael@0: NS_IMPL_ISUPPORTS(ResourceSocketListener, nsIServerSocketListener) michael@0: michael@0: NS_IMETHODIMP michael@0: ResourceSocketListener::OnSocketAccepted(nsIServerSocket* aServ, michael@0: nsISocketTransport* aTrans) michael@0: { michael@0: nsCOMPtr input; michael@0: nsCOMPtr output; michael@0: nsresult rv; michael@0: michael@0: rv = aTrans->OpenInputStream(nsITransport::OPEN_BLOCKING, 0, 0, getter_AddRefs(input)); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: rv = aTrans->OpenOutputStream(nsITransport::OPEN_BLOCKING, 0, 0, getter_AddRefs(output)); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: nsCOMPtr thread; michael@0: rv = NS_NewThread(getter_AddRefs(thread)); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: nsCOMPtr event = new ServeResourceEvent(input.get(), output.get(), mServer); michael@0: return thread->Dispatch(event, NS_DISPATCH_NORMAL); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: ResourceSocketListener::OnStopListening(nsIServerSocket* aServ, nsresult aStatus) michael@0: { michael@0: return NS_OK; michael@0: } michael@0: michael@0: MediaResourceServer::MediaResourceServer() : michael@0: mMutex("MediaResourceServer") michael@0: { michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: MediaResourceServer::Run() michael@0: { michael@0: MutexAutoLock lock(mMutex); michael@0: michael@0: nsresult rv; michael@0: mSocket = do_CreateInstance(NS_SERVERSOCKET_CONTRACTID, &rv); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: rv = mSocket->InitSpecialConnection(-1, michael@0: nsIServerSocket::LoopbackOnly michael@0: | nsIServerSocket::KeepWhenOffline, michael@0: -1); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: rv = mSocket->AsyncListen(new ResourceSocketListener(this)); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: /* static */ michael@0: already_AddRefed michael@0: MediaResourceServer::Start() michael@0: { michael@0: nsRefPtr server = new MediaResourceServer(); michael@0: NS_DispatchToMainThread(server, NS_DISPATCH_SYNC); michael@0: return server.forget(); michael@0: } michael@0: michael@0: void michael@0: MediaResourceServer::Stop() michael@0: { michael@0: MutexAutoLock lock(mMutex); michael@0: mSocket->Close(); michael@0: mSocket = nullptr; michael@0: } michael@0: michael@0: nsresult michael@0: MediaResourceServer::AppendRandomPath(nsCString& aUrl) michael@0: { michael@0: // Use a cryptographic quality PRNG to generate raw random bytes michael@0: // and convert that to a base64 string for use as an URL path. This michael@0: // is based on code from nsExternalAppHandler::SetUpTempFile. michael@0: nsresult rv; michael@0: nsCOMPtr rg = michael@0: do_GetService("@mozilla.org/security/random-generator;1", &rv); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: // For each three bytes of random data we will get four bytes of michael@0: // ASCII. Request a bit more to be safe and truncate to the length michael@0: // we want at the end. michael@0: const uint32_t wantedFileNameLength = 16; michael@0: const uint32_t requiredBytesLength = michael@0: static_cast((wantedFileNameLength + 1) / 4 * 3); michael@0: michael@0: uint8_t* buffer; michael@0: rv = rg->GenerateRandomBytes(requiredBytesLength, &buffer); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: nsAutoCString tempLeafName; michael@0: nsDependentCSubstring randomData(reinterpret_cast(buffer), michael@0: requiredBytesLength); michael@0: rv = Base64Encode(randomData, tempLeafName); michael@0: NS_Free(buffer); michael@0: buffer = nullptr; michael@0: if (NS_FAILED (rv)) return rv; michael@0: michael@0: tempLeafName.Truncate(wantedFileNameLength); michael@0: michael@0: // Base64 characters are alphanumeric (a-zA-Z0-9) and '+' and '/', so we need michael@0: // to replace illegal characters -- notably '/' michael@0: tempLeafName.ReplaceChar(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, '_'); michael@0: michael@0: aUrl += "/"; michael@0: aUrl += tempLeafName; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: MediaResourceServer::AddResource(mozilla::MediaResource* aResource, nsCString& aUrl) michael@0: { michael@0: nsCString url = GetURLPrefix(); michael@0: nsresult rv = AppendRandomPath(url); michael@0: if (NS_FAILED (rv)) return rv; michael@0: michael@0: { michael@0: MutexAutoLock lock(mMutex); michael@0: michael@0: // Adding a resource URL that already exists is considered an error. michael@0: if (mResources.find(aUrl) != mResources.end()) return NS_ERROR_FAILURE; michael@0: mResources[url] = aResource; michael@0: } michael@0: michael@0: aUrl = url; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: void michael@0: MediaResourceServer::RemoveResource(nsCString const& aUrl) michael@0: { michael@0: MutexAutoLock lock(mMutex); michael@0: mResources.erase(aUrl); michael@0: } michael@0: michael@0: nsCString michael@0: MediaResourceServer::GetURLPrefix() michael@0: { michael@0: MutexAutoLock lock(mMutex); michael@0: michael@0: int32_t port = 0; michael@0: nsresult rv = mSocket->GetPort(&port); michael@0: if (NS_FAILED (rv) || port < 0) { michael@0: return nsCString(""); michael@0: } michael@0: michael@0: char buffer[256]; michael@0: snprintf(buffer, sizeof(buffer), "http://127.0.0.1:%d", port >= 0 ? port : 0); michael@0: return nsCString(buffer); michael@0: } michael@0: michael@0: already_AddRefed michael@0: MediaResourceServer::GetResource(nsCString const& aUrl) michael@0: { michael@0: MutexAutoLock lock(mMutex); michael@0: ResourceMap::const_iterator it = mResources.find(aUrl); michael@0: if (it == mResources.end()) return nullptr; michael@0: michael@0: nsRefPtr resource = it->second; michael@0: return resource.forget(); michael@0: }