1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/content/media/plugins/MediaResourceServer.cpp Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,526 @@ 1.4 +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim:set ts=2 sw=2 sts=2 et cindent: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.8 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 +#include "mozilla/Assertions.h" 1.10 +#include "mozilla/Base64.h" 1.11 +#include "nsThreadUtils.h" 1.12 +#include "nsIServiceManager.h" 1.13 +#include "nsISocketTransport.h" 1.14 +#include "nsIOutputStream.h" 1.15 +#include "nsIInputStream.h" 1.16 +#include "nsIRandomGenerator.h" 1.17 +#include "nsReadLine.h" 1.18 +#include "nsNetCID.h" 1.19 +#include "VideoUtils.h" 1.20 +#include "MediaResource.h" 1.21 +#include "MediaResourceServer.h" 1.22 + 1.23 +#if defined(_MSC_VER) 1.24 +#define strtoll _strtoi64 1.25 +#define snprintf _snprintf_s 1.26 +#endif 1.27 + 1.28 +using namespace mozilla; 1.29 + 1.30 +/* 1.31 + ReadCRLF is a variant of NS_ReadLine from nsReadLine.h that deals 1.32 + with the carriage return/line feed requirements of HTTP requests. 1.33 +*/ 1.34 +template<typename CharT, class StreamType, class StringType> 1.35 +nsresult 1.36 +ReadCRLF (StreamType* aStream, nsLineBuffer<CharT> * aBuffer, 1.37 + StringType & aLine, bool *aMore) 1.38 +{ 1.39 + // eollast is true if the last character in the buffer is a '\r', 1.40 + // signaling a potential '\r\n' sequence split between reads. 1.41 + bool eollast = false; 1.42 + 1.43 + aLine.Truncate(); 1.44 + 1.45 + while (1) { // will be returning out of this loop on eol or eof 1.46 + if (aBuffer->start == aBuffer->end) { // buffer is empty. Read into it. 1.47 + uint32_t bytesRead; 1.48 + nsresult rv = aStream->Read(aBuffer->buf, kLineBufferSize, &bytesRead); 1.49 + if (NS_FAILED(rv) || bytesRead == 0) { 1.50 + *aMore = false; 1.51 + return rv; 1.52 + } 1.53 + aBuffer->start = aBuffer->buf; 1.54 + aBuffer->end = aBuffer->buf + bytesRead; 1.55 + *(aBuffer->end) = '\0'; 1.56 + } 1.57 + 1.58 + /* 1.59 + * Walk the buffer looking for an end-of-line. 1.60 + * There are 4 cases to consider: 1.61 + * 1. the CR char is the last char in the buffer 1.62 + * 2. the CRLF sequence are the last characters in the buffer 1.63 + * 3. the CRLF sequence + one or more chars at the end of the buffer 1.64 + * we need at least one char after the first CRLF sequence to 1.65 + * set |aMore| correctly. 1.66 + * 4. The LF character is the first char in the buffer when eollast is 1.67 + * true. 1.68 + */ 1.69 + CharT* current = aBuffer->start; 1.70 + if (eollast) { // Case 4 1.71 + if (*current == '\n') { 1.72 + aBuffer->start = ++current; 1.73 + *aMore = true; 1.74 + return NS_OK; 1.75 + } 1.76 + else { 1.77 + eollast = false; 1.78 + aLine.Append('\r'); 1.79 + } 1.80 + } 1.81 + // Cases 2 and 3 1.82 + for ( ; current < aBuffer->end-1; ++current) { 1.83 + if (*current == '\r' && *(current+1) == '\n') { 1.84 + *current++ = '\0'; 1.85 + *current++ = '\0'; 1.86 + aLine.Append(aBuffer->start); 1.87 + aBuffer->start = current; 1.88 + *aMore = true; 1.89 + return NS_OK; 1.90 + } 1.91 + } 1.92 + // Case 1 1.93 + if (*current == '\r') { 1.94 + eollast = true; 1.95 + *current++ = '\0'; 1.96 + } 1.97 + 1.98 + aLine.Append(aBuffer->start); 1.99 + aBuffer->start = aBuffer->end; // mark the buffer empty 1.100 + } 1.101 +} 1.102 + 1.103 +// Each client HTTP request results in a thread being spawned to process it. 1.104 +// That thread has a single event dispatched to it which handles the HTTP 1.105 +// protocol. It parses the headers and forwards data from the MediaResource 1.106 +// associated with the URL back to client. When the request is complete it will 1.107 +// shutdown the thread. 1.108 +class ServeResourceEvent : public nsRunnable { 1.109 +private: 1.110 + // Reading from this reads the data sent from the client. 1.111 + nsCOMPtr<nsIInputStream> mInput; 1.112 + 1.113 + // Writing to this sends data to the client. 1.114 + nsCOMPtr<nsIOutputStream> mOutput; 1.115 + 1.116 + // The MediaResourceServer that owns the MediaResource instances 1.117 + // served. This is used to lookup the MediaResource from the URL. 1.118 + nsRefPtr<MediaResourceServer> mServer; 1.119 + 1.120 + // Write 'aBufferLength' bytes from 'aBuffer' to 'mOutput'. This 1.121 + // method ensures all the data is written by checking the number 1.122 + // of bytes returned from the output streams 'Write' method and 1.123 + // looping until done. 1.124 + nsresult WriteAll(char const* aBuffer, int32_t aBufferLength); 1.125 + 1.126 +public: 1.127 + ServeResourceEvent(nsIInputStream* aInput, nsIOutputStream* aOutput, 1.128 + MediaResourceServer* aServer) 1.129 + : mInput(aInput), mOutput(aOutput), mServer(aServer) {} 1.130 + 1.131 + // This method runs on the thread and exits when it has completed the 1.132 + // HTTP request. 1.133 + NS_IMETHOD Run(); 1.134 + 1.135 + // Given the first line of an HTTP request, parse the URL requested and 1.136 + // return the MediaResource for that URL. 1.137 + already_AddRefed<MediaResource> GetMediaResource(nsCString const& aHTTPRequest); 1.138 + 1.139 + // Gracefully shutdown the thread and cleanup resources 1.140 + void Shutdown(); 1.141 +}; 1.142 + 1.143 +nsresult 1.144 +ServeResourceEvent::WriteAll(char const* aBuffer, int32_t aBufferLength) 1.145 +{ 1.146 + while (aBufferLength > 0) { 1.147 + uint32_t written = 0; 1.148 + nsresult rv = mOutput->Write(aBuffer, aBufferLength, &written); 1.149 + if (NS_FAILED (rv)) return rv; 1.150 + 1.151 + aBufferLength -= written; 1.152 + aBuffer += written; 1.153 + } 1.154 + 1.155 + return NS_OK; 1.156 +} 1.157 + 1.158 +already_AddRefed<MediaResource> 1.159 +ServeResourceEvent::GetMediaResource(nsCString const& aHTTPRequest) 1.160 +{ 1.161 + // Check that the HTTP method is GET 1.162 + const char* HTTP_METHOD = "GET "; 1.163 + if (strncmp(aHTTPRequest.get(), HTTP_METHOD, strlen(HTTP_METHOD)) != 0) { 1.164 + return nullptr; 1.165 + } 1.166 + 1.167 + const char* url_start = strchr(aHTTPRequest.get(), ' '); 1.168 + if (!url_start) { 1.169 + return nullptr; 1.170 + } 1.171 + 1.172 + const char* url_end = strrchr(++url_start, ' '); 1.173 + if (!url_end) { 1.174 + return nullptr; 1.175 + } 1.176 + 1.177 + // The path extracted from the HTTP request is used as a key in hash 1.178 + // table. It is not related to retrieving data from the filesystem so 1.179 + // we don't need to do any sanity checking on ".." paths and similar 1.180 + // exploits. 1.181 + nsCString relative(url_start, url_end - url_start); 1.182 + nsRefPtr<MediaResource> resource = 1.183 + mServer->GetResource(mServer->GetURLPrefix() + relative); 1.184 + return resource.forget(); 1.185 +} 1.186 + 1.187 +NS_IMETHODIMP 1.188 +ServeResourceEvent::Run() { 1.189 + bool more = false; // Are there HTTP headers to read after the first line 1.190 + nsCString line; // Contains the current line read from input stream 1.191 + nsLineBuffer<char>* buffer = new nsLineBuffer<char>(); 1.192 + nsresult rv = ReadCRLF(mInput.get(), buffer, line, &more); 1.193 + if (NS_FAILED(rv)) { Shutdown(); return rv; } 1.194 + 1.195 + // First line contains the HTTP GET request. Extract the URL and obtain 1.196 + // the MediaResource for it. 1.197 + nsRefPtr<MediaResource> resource = GetMediaResource(line); 1.198 + if (!resource) { 1.199 + const char* response_404 = "HTTP/1.1 404 Not Found\r\n" 1.200 + "Content-Length: 0\r\n\r\n"; 1.201 + rv = WriteAll(response_404, strlen(response_404)); 1.202 + Shutdown(); 1.203 + return rv; 1.204 + } 1.205 + 1.206 + // Offset in bytes to start reading from resource. 1.207 + // This is zero by default but can be set to another starting value if 1.208 + // this HTTP request includes a byte range request header. 1.209 + int64_t start = 0; 1.210 + 1.211 + // Keep reading lines until we get a zero length line, which is the HTTP 1.212 + // protocol's way of signifying the end of headers and start of body, or 1.213 + // until we have no more data to read. 1.214 + while (more && line.Length() > 0) { 1.215 + rv = ReadCRLF(mInput.get(), buffer, line, &more); 1.216 + if (NS_FAILED(rv)) { Shutdown(); return rv; } 1.217 + 1.218 + // Look for a byte range request header. If there is one, set the 1.219 + // media resource offset to start from to that requested. Here we 1.220 + // only check for the range request format used by Android rather 1.221 + // than implementing all possibilities in the HTTP specification. 1.222 + // That is, the range request is of the form: 1.223 + // Range: bytes=nnnn- 1.224 + // Were 'nnnn' is an integer number. 1.225 + // The end of the range is not checked, instead we return up to 1.226 + // the end of the resource and the client is informed of this via 1.227 + // the content-range header. 1.228 + NS_NAMED_LITERAL_CSTRING(byteRange, "Range: bytes="); 1.229 + const char* s = strstr(line.get(), byteRange.get()); 1.230 + if (s) { 1.231 + start = strtoll(s+byteRange.Length(), nullptr, 10); 1.232 + 1.233 + // Clamp 'start' to be between 0 and the resource length. 1.234 + start = std::max(0ll, std::min(resource->GetLength(), start)); 1.235 + } 1.236 + } 1.237 + 1.238 + // HTTP response to use if this is a non byte range request 1.239 + const char* response_normal = "HTTP/1.1 200 OK\r\n"; 1.240 + 1.241 + // HTTP response to use if this is a byte range request 1.242 + const char* response_range = "HTTP/1.1 206 Partial Content\r\n"; 1.243 + 1.244 + // End of HTTP reponse headers is indicated by an empty line. 1.245 + const char* response_end = "\r\n"; 1.246 + 1.247 + // If the request was a byte range request, we need to read from the 1.248 + // requested offset. If the resource is non-seekable, or the seek 1.249 + // fails, then the start offset is set back to zero. This results in all 1.250 + // HTTP response data being as if the byte range request was not made. 1.251 + if (start > 0 && !resource->IsTransportSeekable()) { 1.252 + start = 0; 1.253 + } 1.254 + 1.255 + const char* response_line = start > 0 ? 1.256 + response_range : 1.257 + response_normal; 1.258 + rv = WriteAll(response_line, strlen(response_line)); 1.259 + if (NS_FAILED(rv)) { Shutdown(); return NS_OK; } 1.260 + 1.261 + // Buffer used for reading from the input stream and writing to 1.262 + // the output stream. The buffer size should be big enough for the 1.263 + // HTTP response headers sent below. A static_assert ensures 1.264 + // this where the buffer is used. 1.265 + const int buffer_size = 32768; 1.266 + nsAutoArrayPtr<char> b(new char[buffer_size]); 1.267 + 1.268 + // If we know the length of the resource, send a Content-Length header. 1.269 + int64_t contentlength = resource->GetLength() - start; 1.270 + if (contentlength > 0) { 1.271 + static_assert (buffer_size > 1024, 1.272 + "buffer_size must be large enough " 1.273 + "to hold response headers"); 1.274 + snprintf(b, buffer_size, "Content-Length: %lld\r\n", contentlength); 1.275 + rv = WriteAll(b, strlen(b)); 1.276 + if (NS_FAILED(rv)) { Shutdown(); return NS_OK; } 1.277 + } 1.278 + 1.279 + // If the request was a byte range request, respond with a Content-Range 1.280 + // header which details the extent of the data returned. 1.281 + if (start > 0) { 1.282 + static_assert (buffer_size > 1024, 1.283 + "buffer_size must be large enough " 1.284 + "to hold response headers"); 1.285 + snprintf(b, buffer_size, "Content-Range: bytes %lld-%lld/%lld\r\n", 1.286 + start, resource->GetLength() - 1, resource->GetLength()); 1.287 + rv = WriteAll(b, strlen(b)); 1.288 + if (NS_FAILED(rv)) { Shutdown(); return NS_OK; } 1.289 + } 1.290 + 1.291 + rv = WriteAll(response_end, strlen(response_end)); 1.292 + if (NS_FAILED(rv)) { Shutdown(); return NS_OK; } 1.293 + 1.294 + rv = mOutput->Flush(); 1.295 + if (NS_FAILED(rv)) { Shutdown(); return NS_OK; } 1.296 + 1.297 + // Read data from media resource 1.298 + uint32_t bytesRead = 0; // Number of bytes read/written to streams 1.299 + rv = resource->ReadAt(start, b, buffer_size, &bytesRead); 1.300 + while (NS_SUCCEEDED(rv) && bytesRead != 0) { 1.301 + // Keep track of what we think the starting position for the next read 1.302 + // is. This is used in subsequent ReadAt calls to ensure we are reading 1.303 + // from the correct offset in the case where another thread is reading 1.304 + // from th same MediaResource. 1.305 + start += bytesRead; 1.306 + 1.307 + // Write data obtained from media resource to output stream 1.308 + rv = WriteAll(b, bytesRead); 1.309 + if (NS_FAILED (rv)) break; 1.310 + 1.311 + rv = resource->ReadAt(start, b, 32768, &bytesRead); 1.312 + } 1.313 + 1.314 + Shutdown(); 1.315 + return NS_OK; 1.316 +} 1.317 + 1.318 +void 1.319 +ServeResourceEvent::Shutdown() 1.320 +{ 1.321 + // Cleanup resources and exit. 1.322 + mInput->Close(); 1.323 + mOutput->Close(); 1.324 + 1.325 + // To shutdown the current thread we need to first exit this event. 1.326 + // The Shutdown event below is posted to the main thread to do this. 1.327 + nsCOMPtr<nsIRunnable> event = new ShutdownThreadEvent(NS_GetCurrentThread()); 1.328 + NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); 1.329 +} 1.330 + 1.331 +/* 1.332 + This is the listener attached to the server socket. When an HTTP 1.333 + request is made by the client the OnSocketAccepted method is 1.334 + called. This method will spawn a thread to process the request. 1.335 + The thread receives a single event which does the parsing of 1.336 + the HTTP request and forwarding the data from the MediaResource 1.337 + to the output stream of the request. 1.338 + 1.339 + The MediaResource used for providing the request data is obtained 1.340 + from the MediaResourceServer that created this listener, using the 1.341 + URL the client requested. 1.342 +*/ 1.343 +class ResourceSocketListener : public nsIServerSocketListener 1.344 +{ 1.345 +public: 1.346 + // The MediaResourceServer used to look up the MediaResource 1.347 + // on requests. 1.348 + nsRefPtr<MediaResourceServer> mServer; 1.349 + 1.350 +public: 1.351 + NS_DECL_THREADSAFE_ISUPPORTS 1.352 + NS_DECL_NSISERVERSOCKETLISTENER 1.353 + 1.354 + ResourceSocketListener(MediaResourceServer* aServer) : 1.355 + mServer(aServer) 1.356 + { 1.357 + } 1.358 + 1.359 + virtual ~ResourceSocketListener() { } 1.360 +}; 1.361 + 1.362 +NS_IMPL_ISUPPORTS(ResourceSocketListener, nsIServerSocketListener) 1.363 + 1.364 +NS_IMETHODIMP 1.365 +ResourceSocketListener::OnSocketAccepted(nsIServerSocket* aServ, 1.366 + nsISocketTransport* aTrans) 1.367 +{ 1.368 + nsCOMPtr<nsIInputStream> input; 1.369 + nsCOMPtr<nsIOutputStream> output; 1.370 + nsresult rv; 1.371 + 1.372 + rv = aTrans->OpenInputStream(nsITransport::OPEN_BLOCKING, 0, 0, getter_AddRefs(input)); 1.373 + if (NS_FAILED(rv)) return rv; 1.374 + 1.375 + rv = aTrans->OpenOutputStream(nsITransport::OPEN_BLOCKING, 0, 0, getter_AddRefs(output)); 1.376 + if (NS_FAILED(rv)) return rv; 1.377 + 1.378 + nsCOMPtr<nsIThread> thread; 1.379 + rv = NS_NewThread(getter_AddRefs(thread)); 1.380 + if (NS_FAILED(rv)) return rv; 1.381 + 1.382 + nsCOMPtr<nsIRunnable> event = new ServeResourceEvent(input.get(), output.get(), mServer); 1.383 + return thread->Dispatch(event, NS_DISPATCH_NORMAL); 1.384 +} 1.385 + 1.386 +NS_IMETHODIMP 1.387 +ResourceSocketListener::OnStopListening(nsIServerSocket* aServ, nsresult aStatus) 1.388 +{ 1.389 + return NS_OK; 1.390 +} 1.391 + 1.392 +MediaResourceServer::MediaResourceServer() : 1.393 + mMutex("MediaResourceServer") 1.394 +{ 1.395 +} 1.396 + 1.397 +NS_IMETHODIMP 1.398 +MediaResourceServer::Run() 1.399 +{ 1.400 + MutexAutoLock lock(mMutex); 1.401 + 1.402 + nsresult rv; 1.403 + mSocket = do_CreateInstance(NS_SERVERSOCKET_CONTRACTID, &rv); 1.404 + if (NS_FAILED(rv)) return rv; 1.405 + 1.406 + rv = mSocket->InitSpecialConnection(-1, 1.407 + nsIServerSocket::LoopbackOnly 1.408 + | nsIServerSocket::KeepWhenOffline, 1.409 + -1); 1.410 + if (NS_FAILED(rv)) return rv; 1.411 + 1.412 + rv = mSocket->AsyncListen(new ResourceSocketListener(this)); 1.413 + if (NS_FAILED(rv)) return rv; 1.414 + 1.415 + return NS_OK; 1.416 +} 1.417 + 1.418 +/* static */ 1.419 +already_AddRefed<MediaResourceServer> 1.420 +MediaResourceServer::Start() 1.421 +{ 1.422 + nsRefPtr<MediaResourceServer> server = new MediaResourceServer(); 1.423 + NS_DispatchToMainThread(server, NS_DISPATCH_SYNC); 1.424 + return server.forget(); 1.425 +} 1.426 + 1.427 +void 1.428 +MediaResourceServer::Stop() 1.429 +{ 1.430 + MutexAutoLock lock(mMutex); 1.431 + mSocket->Close(); 1.432 + mSocket = nullptr; 1.433 +} 1.434 + 1.435 +nsresult 1.436 +MediaResourceServer::AppendRandomPath(nsCString& aUrl) 1.437 +{ 1.438 + // Use a cryptographic quality PRNG to generate raw random bytes 1.439 + // and convert that to a base64 string for use as an URL path. This 1.440 + // is based on code from nsExternalAppHandler::SetUpTempFile. 1.441 + nsresult rv; 1.442 + nsCOMPtr<nsIRandomGenerator> rg = 1.443 + do_GetService("@mozilla.org/security/random-generator;1", &rv); 1.444 + if (NS_FAILED(rv)) return rv; 1.445 + 1.446 + // For each three bytes of random data we will get four bytes of 1.447 + // ASCII. Request a bit more to be safe and truncate to the length 1.448 + // we want at the end. 1.449 + const uint32_t wantedFileNameLength = 16; 1.450 + const uint32_t requiredBytesLength = 1.451 + static_cast<uint32_t>((wantedFileNameLength + 1) / 4 * 3); 1.452 + 1.453 + uint8_t* buffer; 1.454 + rv = rg->GenerateRandomBytes(requiredBytesLength, &buffer); 1.455 + if (NS_FAILED(rv)) return rv; 1.456 + 1.457 + nsAutoCString tempLeafName; 1.458 + nsDependentCSubstring randomData(reinterpret_cast<const char*>(buffer), 1.459 + requiredBytesLength); 1.460 + rv = Base64Encode(randomData, tempLeafName); 1.461 + NS_Free(buffer); 1.462 + buffer = nullptr; 1.463 + if (NS_FAILED (rv)) return rv; 1.464 + 1.465 + tempLeafName.Truncate(wantedFileNameLength); 1.466 + 1.467 + // Base64 characters are alphanumeric (a-zA-Z0-9) and '+' and '/', so we need 1.468 + // to replace illegal characters -- notably '/' 1.469 + tempLeafName.ReplaceChar(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, '_'); 1.470 + 1.471 + aUrl += "/"; 1.472 + aUrl += tempLeafName; 1.473 + 1.474 + return NS_OK; 1.475 +} 1.476 + 1.477 +nsresult 1.478 +MediaResourceServer::AddResource(mozilla::MediaResource* aResource, nsCString& aUrl) 1.479 +{ 1.480 + nsCString url = GetURLPrefix(); 1.481 + nsresult rv = AppendRandomPath(url); 1.482 + if (NS_FAILED (rv)) return rv; 1.483 + 1.484 + { 1.485 + MutexAutoLock lock(mMutex); 1.486 + 1.487 + // Adding a resource URL that already exists is considered an error. 1.488 + if (mResources.find(aUrl) != mResources.end()) return NS_ERROR_FAILURE; 1.489 + mResources[url] = aResource; 1.490 + } 1.491 + 1.492 + aUrl = url; 1.493 + 1.494 + return NS_OK; 1.495 +} 1.496 + 1.497 +void 1.498 +MediaResourceServer::RemoveResource(nsCString const& aUrl) 1.499 +{ 1.500 + MutexAutoLock lock(mMutex); 1.501 + mResources.erase(aUrl); 1.502 +} 1.503 + 1.504 +nsCString 1.505 +MediaResourceServer::GetURLPrefix() 1.506 +{ 1.507 + MutexAutoLock lock(mMutex); 1.508 + 1.509 + int32_t port = 0; 1.510 + nsresult rv = mSocket->GetPort(&port); 1.511 + if (NS_FAILED (rv) || port < 0) { 1.512 + return nsCString(""); 1.513 + } 1.514 + 1.515 + char buffer[256]; 1.516 + snprintf(buffer, sizeof(buffer), "http://127.0.0.1:%d", port >= 0 ? port : 0); 1.517 + return nsCString(buffer); 1.518 +} 1.519 + 1.520 +already_AddRefed<MediaResource> 1.521 +MediaResourceServer::GetResource(nsCString const& aUrl) 1.522 +{ 1.523 + MutexAutoLock lock(mMutex); 1.524 + ResourceMap::const_iterator it = mResources.find(aUrl); 1.525 + if (it == mResources.end()) return nullptr; 1.526 + 1.527 + nsRefPtr<MediaResource> resource = it->second; 1.528 + return resource.forget(); 1.529 +}