|
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 } |