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