content/media/plugins/MediaResourceServer.cpp

branch
TOR_BUG_9701
changeset 8
97036ab72558
equal deleted inserted replaced
-1:000000000000 0:f2b19e4573ea
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et cindent: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #include "mozilla/Assertions.h"
7 #include "mozilla/Base64.h"
8 #include "nsThreadUtils.h"
9 #include "nsIServiceManager.h"
10 #include "nsISocketTransport.h"
11 #include "nsIOutputStream.h"
12 #include "nsIInputStream.h"
13 #include "nsIRandomGenerator.h"
14 #include "nsReadLine.h"
15 #include "nsNetCID.h"
16 #include "VideoUtils.h"
17 #include "MediaResource.h"
18 #include "MediaResourceServer.h"
19
20 #if defined(_MSC_VER)
21 #define strtoll _strtoi64
22 #define snprintf _snprintf_s
23 #endif
24
25 using namespace mozilla;
26
27 /*
28 ReadCRLF is a variant of NS_ReadLine from nsReadLine.h that deals
29 with the carriage return/line feed requirements of HTTP requests.
30 */
31 template<typename CharT, class StreamType, class StringType>
32 nsresult
33 ReadCRLF (StreamType* aStream, nsLineBuffer<CharT> * aBuffer,
34 StringType & aLine, bool *aMore)
35 {
36 // eollast is true if the last character in the buffer is a '\r',
37 // signaling a potential '\r\n' sequence split between reads.
38 bool eollast = false;
39
40 aLine.Truncate();
41
42 while (1) { // will be returning out of this loop on eol or eof
43 if (aBuffer->start == aBuffer->end) { // buffer is empty. Read into it.
44 uint32_t bytesRead;
45 nsresult rv = aStream->Read(aBuffer->buf, kLineBufferSize, &bytesRead);
46 if (NS_FAILED(rv) || bytesRead == 0) {
47 *aMore = false;
48 return rv;
49 }
50 aBuffer->start = aBuffer->buf;
51 aBuffer->end = aBuffer->buf + bytesRead;
52 *(aBuffer->end) = '\0';
53 }
54
55 /*
56 * Walk the buffer looking for an end-of-line.
57 * There are 4 cases to consider:
58 * 1. the CR char is the last char in the buffer
59 * 2. the CRLF sequence are the last characters in the buffer
60 * 3. the CRLF sequence + one or more chars at the end of the buffer
61 * we need at least one char after the first CRLF sequence to
62 * set |aMore| correctly.
63 * 4. The LF character is the first char in the buffer when eollast is
64 * true.
65 */
66 CharT* current = aBuffer->start;
67 if (eollast) { // Case 4
68 if (*current == '\n') {
69 aBuffer->start = ++current;
70 *aMore = true;
71 return NS_OK;
72 }
73 else {
74 eollast = false;
75 aLine.Append('\r');
76 }
77 }
78 // Cases 2 and 3
79 for ( ; current < aBuffer->end-1; ++current) {
80 if (*current == '\r' && *(current+1) == '\n') {
81 *current++ = '\0';
82 *current++ = '\0';
83 aLine.Append(aBuffer->start);
84 aBuffer->start = current;
85 *aMore = true;
86 return NS_OK;
87 }
88 }
89 // Case 1
90 if (*current == '\r') {
91 eollast = true;
92 *current++ = '\0';
93 }
94
95 aLine.Append(aBuffer->start);
96 aBuffer->start = aBuffer->end; // mark the buffer empty
97 }
98 }
99
100 // Each client HTTP request results in a thread being spawned to process it.
101 // That thread has a single event dispatched to it which handles the HTTP
102 // protocol. It parses the headers and forwards data from the MediaResource
103 // associated with the URL back to client. When the request is complete it will
104 // shutdown the thread.
105 class ServeResourceEvent : public nsRunnable {
106 private:
107 // Reading from this reads the data sent from the client.
108 nsCOMPtr<nsIInputStream> mInput;
109
110 // Writing to this sends data to the client.
111 nsCOMPtr<nsIOutputStream> mOutput;
112
113 // The MediaResourceServer that owns the MediaResource instances
114 // served. This is used to lookup the MediaResource from the URL.
115 nsRefPtr<MediaResourceServer> mServer;
116
117 // Write 'aBufferLength' bytes from 'aBuffer' to 'mOutput'. This
118 // method ensures all the data is written by checking the number
119 // of bytes returned from the output streams 'Write' method and
120 // looping until done.
121 nsresult WriteAll(char const* aBuffer, int32_t aBufferLength);
122
123 public:
124 ServeResourceEvent(nsIInputStream* aInput, nsIOutputStream* aOutput,
125 MediaResourceServer* aServer)
126 : mInput(aInput), mOutput(aOutput), mServer(aServer) {}
127
128 // This method runs on the thread and exits when it has completed the
129 // HTTP request.
130 NS_IMETHOD Run();
131
132 // Given the first line of an HTTP request, parse the URL requested and
133 // return the MediaResource for that URL.
134 already_AddRefed<MediaResource> GetMediaResource(nsCString const& aHTTPRequest);
135
136 // Gracefully shutdown the thread and cleanup resources
137 void Shutdown();
138 };
139
140 nsresult
141 ServeResourceEvent::WriteAll(char const* aBuffer, int32_t aBufferLength)
142 {
143 while (aBufferLength > 0) {
144 uint32_t written = 0;
145 nsresult rv = mOutput->Write(aBuffer, aBufferLength, &written);
146 if (NS_FAILED (rv)) return rv;
147
148 aBufferLength -= written;
149 aBuffer += written;
150 }
151
152 return NS_OK;
153 }
154
155 already_AddRefed<MediaResource>
156 ServeResourceEvent::GetMediaResource(nsCString const& aHTTPRequest)
157 {
158 // Check that the HTTP method is GET
159 const char* HTTP_METHOD = "GET ";
160 if (strncmp(aHTTPRequest.get(), HTTP_METHOD, strlen(HTTP_METHOD)) != 0) {
161 return nullptr;
162 }
163
164 const char* url_start = strchr(aHTTPRequest.get(), ' ');
165 if (!url_start) {
166 return nullptr;
167 }
168
169 const char* url_end = strrchr(++url_start, ' ');
170 if (!url_end) {
171 return nullptr;
172 }
173
174 // The path extracted from the HTTP request is used as a key in hash
175 // table. It is not related to retrieving data from the filesystem so
176 // we don't need to do any sanity checking on ".." paths and similar
177 // exploits.
178 nsCString relative(url_start, url_end - url_start);
179 nsRefPtr<MediaResource> resource =
180 mServer->GetResource(mServer->GetURLPrefix() + relative);
181 return resource.forget();
182 }
183
184 NS_IMETHODIMP
185 ServeResourceEvent::Run() {
186 bool more = false; // Are there HTTP headers to read after the first line
187 nsCString line; // Contains the current line read from input stream
188 nsLineBuffer<char>* buffer = new nsLineBuffer<char>();
189 nsresult rv = ReadCRLF(mInput.get(), buffer, line, &more);
190 if (NS_FAILED(rv)) { Shutdown(); return rv; }
191
192 // First line contains the HTTP GET request. Extract the URL and obtain
193 // the MediaResource for it.
194 nsRefPtr<MediaResource> resource = GetMediaResource(line);
195 if (!resource) {
196 const char* response_404 = "HTTP/1.1 404 Not Found\r\n"
197 "Content-Length: 0\r\n\r\n";
198 rv = WriteAll(response_404, strlen(response_404));
199 Shutdown();
200 return rv;
201 }
202
203 // Offset in bytes to start reading from resource.
204 // This is zero by default but can be set to another starting value if
205 // this HTTP request includes a byte range request header.
206 int64_t start = 0;
207
208 // Keep reading lines until we get a zero length line, which is the HTTP
209 // protocol's way of signifying the end of headers and start of body, or
210 // until we have no more data to read.
211 while (more && line.Length() > 0) {
212 rv = ReadCRLF(mInput.get(), buffer, line, &more);
213 if (NS_FAILED(rv)) { Shutdown(); return rv; }
214
215 // Look for a byte range request header. If there is one, set the
216 // media resource offset to start from to that requested. Here we
217 // only check for the range request format used by Android rather
218 // than implementing all possibilities in the HTTP specification.
219 // That is, the range request is of the form:
220 // Range: bytes=nnnn-
221 // Were 'nnnn' is an integer number.
222 // The end of the range is not checked, instead we return up to
223 // the end of the resource and the client is informed of this via
224 // the content-range header.
225 NS_NAMED_LITERAL_CSTRING(byteRange, "Range: bytes=");
226 const char* s = strstr(line.get(), byteRange.get());
227 if (s) {
228 start = strtoll(s+byteRange.Length(), nullptr, 10);
229
230 // Clamp 'start' to be between 0 and the resource length.
231 start = std::max(0ll, std::min(resource->GetLength(), start));
232 }
233 }
234
235 // HTTP response to use if this is a non byte range request
236 const char* response_normal = "HTTP/1.1 200 OK\r\n";
237
238 // HTTP response to use if this is a byte range request
239 const char* response_range = "HTTP/1.1 206 Partial Content\r\n";
240
241 // End of HTTP reponse headers is indicated by an empty line.
242 const char* response_end = "\r\n";
243
244 // If the request was a byte range request, we need to read from the
245 // requested offset. If the resource is non-seekable, or the seek
246 // fails, then the start offset is set back to zero. This results in all
247 // HTTP response data being as if the byte range request was not made.
248 if (start > 0 && !resource->IsTransportSeekable()) {
249 start = 0;
250 }
251
252 const char* response_line = start > 0 ?
253 response_range :
254 response_normal;
255 rv = WriteAll(response_line, strlen(response_line));
256 if (NS_FAILED(rv)) { Shutdown(); return NS_OK; }
257
258 // Buffer used for reading from the input stream and writing to
259 // the output stream. The buffer size should be big enough for the
260 // HTTP response headers sent below. A static_assert ensures
261 // this where the buffer is used.
262 const int buffer_size = 32768;
263 nsAutoArrayPtr<char> b(new char[buffer_size]);
264
265 // If we know the length of the resource, send a Content-Length header.
266 int64_t contentlength = resource->GetLength() - start;
267 if (contentlength > 0) {
268 static_assert (buffer_size > 1024,
269 "buffer_size must be large enough "
270 "to hold response headers");
271 snprintf(b, buffer_size, "Content-Length: %lld\r\n", contentlength);
272 rv = WriteAll(b, strlen(b));
273 if (NS_FAILED(rv)) { Shutdown(); return NS_OK; }
274 }
275
276 // If the request was a byte range request, respond with a Content-Range
277 // header which details the extent of the data returned.
278 if (start > 0) {
279 static_assert (buffer_size > 1024,
280 "buffer_size must be large enough "
281 "to hold response headers");
282 snprintf(b, buffer_size, "Content-Range: bytes %lld-%lld/%lld\r\n",
283 start, resource->GetLength() - 1, resource->GetLength());
284 rv = WriteAll(b, strlen(b));
285 if (NS_FAILED(rv)) { Shutdown(); return NS_OK; }
286 }
287
288 rv = WriteAll(response_end, strlen(response_end));
289 if (NS_FAILED(rv)) { Shutdown(); return NS_OK; }
290
291 rv = mOutput->Flush();
292 if (NS_FAILED(rv)) { Shutdown(); return NS_OK; }
293
294 // Read data from media resource
295 uint32_t bytesRead = 0; // Number of bytes read/written to streams
296 rv = resource->ReadAt(start, b, buffer_size, &bytesRead);
297 while (NS_SUCCEEDED(rv) && bytesRead != 0) {
298 // Keep track of what we think the starting position for the next read
299 // is. This is used in subsequent ReadAt calls to ensure we are reading
300 // from the correct offset in the case where another thread is reading
301 // from th same MediaResource.
302 start += bytesRead;
303
304 // Write data obtained from media resource to output stream
305 rv = WriteAll(b, bytesRead);
306 if (NS_FAILED (rv)) break;
307
308 rv = resource->ReadAt(start, b, 32768, &bytesRead);
309 }
310
311 Shutdown();
312 return NS_OK;
313 }
314
315 void
316 ServeResourceEvent::Shutdown()
317 {
318 // Cleanup resources and exit.
319 mInput->Close();
320 mOutput->Close();
321
322 // To shutdown the current thread we need to first exit this event.
323 // The Shutdown event below is posted to the main thread to do this.
324 nsCOMPtr<nsIRunnable> event = new ShutdownThreadEvent(NS_GetCurrentThread());
325 NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
326 }
327
328 /*
329 This is the listener attached to the server socket. When an HTTP
330 request is made by the client the OnSocketAccepted method is
331 called. This method will spawn a thread to process the request.
332 The thread receives a single event which does the parsing of
333 the HTTP request and forwarding the data from the MediaResource
334 to the output stream of the request.
335
336 The MediaResource used for providing the request data is obtained
337 from the MediaResourceServer that created this listener, using the
338 URL the client requested.
339 */
340 class ResourceSocketListener : public nsIServerSocketListener
341 {
342 public:
343 // The MediaResourceServer used to look up the MediaResource
344 // on requests.
345 nsRefPtr<MediaResourceServer> mServer;
346
347 public:
348 NS_DECL_THREADSAFE_ISUPPORTS
349 NS_DECL_NSISERVERSOCKETLISTENER
350
351 ResourceSocketListener(MediaResourceServer* aServer) :
352 mServer(aServer)
353 {
354 }
355
356 virtual ~ResourceSocketListener() { }
357 };
358
359 NS_IMPL_ISUPPORTS(ResourceSocketListener, nsIServerSocketListener)
360
361 NS_IMETHODIMP
362 ResourceSocketListener::OnSocketAccepted(nsIServerSocket* aServ,
363 nsISocketTransport* aTrans)
364 {
365 nsCOMPtr<nsIInputStream> input;
366 nsCOMPtr<nsIOutputStream> output;
367 nsresult rv;
368
369 rv = aTrans->OpenInputStream(nsITransport::OPEN_BLOCKING, 0, 0, getter_AddRefs(input));
370 if (NS_FAILED(rv)) return rv;
371
372 rv = aTrans->OpenOutputStream(nsITransport::OPEN_BLOCKING, 0, 0, getter_AddRefs(output));
373 if (NS_FAILED(rv)) return rv;
374
375 nsCOMPtr<nsIThread> thread;
376 rv = NS_NewThread(getter_AddRefs(thread));
377 if (NS_FAILED(rv)) return rv;
378
379 nsCOMPtr<nsIRunnable> event = new ServeResourceEvent(input.get(), output.get(), mServer);
380 return thread->Dispatch(event, NS_DISPATCH_NORMAL);
381 }
382
383 NS_IMETHODIMP
384 ResourceSocketListener::OnStopListening(nsIServerSocket* aServ, nsresult aStatus)
385 {
386 return NS_OK;
387 }
388
389 MediaResourceServer::MediaResourceServer() :
390 mMutex("MediaResourceServer")
391 {
392 }
393
394 NS_IMETHODIMP
395 MediaResourceServer::Run()
396 {
397 MutexAutoLock lock(mMutex);
398
399 nsresult rv;
400 mSocket = do_CreateInstance(NS_SERVERSOCKET_CONTRACTID, &rv);
401 if (NS_FAILED(rv)) return rv;
402
403 rv = mSocket->InitSpecialConnection(-1,
404 nsIServerSocket::LoopbackOnly
405 | nsIServerSocket::KeepWhenOffline,
406 -1);
407 if (NS_FAILED(rv)) return rv;
408
409 rv = mSocket->AsyncListen(new ResourceSocketListener(this));
410 if (NS_FAILED(rv)) return rv;
411
412 return NS_OK;
413 }
414
415 /* static */
416 already_AddRefed<MediaResourceServer>
417 MediaResourceServer::Start()
418 {
419 nsRefPtr<MediaResourceServer> server = new MediaResourceServer();
420 NS_DispatchToMainThread(server, NS_DISPATCH_SYNC);
421 return server.forget();
422 }
423
424 void
425 MediaResourceServer::Stop()
426 {
427 MutexAutoLock lock(mMutex);
428 mSocket->Close();
429 mSocket = nullptr;
430 }
431
432 nsresult
433 MediaResourceServer::AppendRandomPath(nsCString& aUrl)
434 {
435 // Use a cryptographic quality PRNG to generate raw random bytes
436 // and convert that to a base64 string for use as an URL path. This
437 // is based on code from nsExternalAppHandler::SetUpTempFile.
438 nsresult rv;
439 nsCOMPtr<nsIRandomGenerator> rg =
440 do_GetService("@mozilla.org/security/random-generator;1", &rv);
441 if (NS_FAILED(rv)) return rv;
442
443 // For each three bytes of random data we will get four bytes of
444 // ASCII. Request a bit more to be safe and truncate to the length
445 // we want at the end.
446 const uint32_t wantedFileNameLength = 16;
447 const uint32_t requiredBytesLength =
448 static_cast<uint32_t>((wantedFileNameLength + 1) / 4 * 3);
449
450 uint8_t* buffer;
451 rv = rg->GenerateRandomBytes(requiredBytesLength, &buffer);
452 if (NS_FAILED(rv)) return rv;
453
454 nsAutoCString tempLeafName;
455 nsDependentCSubstring randomData(reinterpret_cast<const char*>(buffer),
456 requiredBytesLength);
457 rv = Base64Encode(randomData, tempLeafName);
458 NS_Free(buffer);
459 buffer = nullptr;
460 if (NS_FAILED (rv)) return rv;
461
462 tempLeafName.Truncate(wantedFileNameLength);
463
464 // Base64 characters are alphanumeric (a-zA-Z0-9) and '+' and '/', so we need
465 // to replace illegal characters -- notably '/'
466 tempLeafName.ReplaceChar(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, '_');
467
468 aUrl += "/";
469 aUrl += tempLeafName;
470
471 return NS_OK;
472 }
473
474 nsresult
475 MediaResourceServer::AddResource(mozilla::MediaResource* aResource, nsCString& aUrl)
476 {
477 nsCString url = GetURLPrefix();
478 nsresult rv = AppendRandomPath(url);
479 if (NS_FAILED (rv)) return rv;
480
481 {
482 MutexAutoLock lock(mMutex);
483
484 // Adding a resource URL that already exists is considered an error.
485 if (mResources.find(aUrl) != mResources.end()) return NS_ERROR_FAILURE;
486 mResources[url] = aResource;
487 }
488
489 aUrl = url;
490
491 return NS_OK;
492 }
493
494 void
495 MediaResourceServer::RemoveResource(nsCString const& aUrl)
496 {
497 MutexAutoLock lock(mMutex);
498 mResources.erase(aUrl);
499 }
500
501 nsCString
502 MediaResourceServer::GetURLPrefix()
503 {
504 MutexAutoLock lock(mMutex);
505
506 int32_t port = 0;
507 nsresult rv = mSocket->GetPort(&port);
508 if (NS_FAILED (rv) || port < 0) {
509 return nsCString("");
510 }
511
512 char buffer[256];
513 snprintf(buffer, sizeof(buffer), "http://127.0.0.1:%d", port >= 0 ? port : 0);
514 return nsCString(buffer);
515 }
516
517 already_AddRefed<MediaResource>
518 MediaResourceServer::GetResource(nsCString const& aUrl)
519 {
520 MutexAutoLock lock(mMutex);
521 ResourceMap::const_iterator it = mResources.find(aUrl);
522 if (it == mResources.end()) return nullptr;
523
524 nsRefPtr<MediaResource> resource = it->second;
525 return resource.forget();
526 }

mercurial