Only use one thread for the webserver to save resources.
[xbmc:jgildays-xbmc.git] / xbmc / utils / WebServer.cpp
1 /*
2  *      Copyright (C) 2005-2010 Team XBMC
3  *      http://www.xbmc.org
4  *
5  *  This Program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2, or (at your option)
8  *  any later version.
9  *
10  *  This Program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with XBMC; see the file COPYING.  If not, write to
17  *  the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
18  *  http://www.gnu.org/copyleft/gpl.html
19  *
20  */
21
22 #include "WebServer.h"
23 #ifdef HAS_WEB_SERVER
24 #include "../lib/libjsonrpc/JSONRPC.h"
25 #include "../lib/libhttpapi/HttpApi.h"
26 #include "../FileSystem/File.h"
27 #include "../FileSystem/Directory.h"
28 #include "../Util.h"
29 #include "log.h"
30 #include "SingleLock.h"
31 #include "DateTime.h"
32 #include "addons/AddonManager.h"
33
34 #ifdef _WIN32
35 #pragma comment(lib, "../../lib/libmicrohttpd_win32/lib/libmicrohttpd.dll.lib")
36 #endif
37
38 #define MAX_STRING_POST_SIZE 20000
39 #define PAGE_FILE_NOT_FOUND "<html><head><title>File not found</title></head><body>File not found</body></html>"
40 #define PAGE_JSONRPC_INFO   "<html><head><title>JSONRPC</title></head><body>JSONRPC active and working</body></html>"
41 #define NOT_SUPPORTED       "<html><head><title>Not Supported</title></head><body>The method you are trying to use is not supported by this server</body></html>"
42 #define DEFAULT_PAGE        "index.html"
43
44 using namespace ADDON;
45 using namespace XFILE;
46 using namespace std;
47 using namespace JSONRPC;
48
49 CWebServer::CWebServer()
50 {
51   m_running = false;
52   m_daemon = NULL;
53   m_needcredentials = true;
54   m_Credentials64Encoded = "eGJtYzp4Ym1j"; // xbmc:xbmc
55 }
56
57 int CWebServer::FillArgumentMap(void *cls, enum MHD_ValueKind kind, const char *key, const char *value) 
58 {
59   map<CStdString, CStdString> *arguments = (map<CStdString, CStdString> *)cls;
60   arguments->insert( pair<CStdString,CStdString>(key,value) );
61   return MHD_YES; 
62 }
63
64 int CWebServer::AskForAuthentication(struct MHD_Connection *connection)
65 {
66   int ret;
67   struct MHD_Response *response;
68
69   response = MHD_create_response_from_data (0, NULL, MHD_NO, MHD_NO);
70   if (!response)
71     return MHD_NO;
72
73   ret = MHD_add_response_header (response, "WWW-Authenticate", "Basic realm=XBMC");
74   if (!ret)
75   {
76     MHD_destroy_response (response);
77     return MHD_NO;
78   }
79
80   ret = MHD_queue_response (connection, MHD_HTTP_UNAUTHORIZED, response);
81
82   MHD_destroy_response (response);
83
84   return ret;
85 }
86
87 bool CWebServer::IsAuthenticated(CWebServer *server, struct MHD_Connection *connection)
88 {
89   CSingleLock lock (server->m_critSection);
90   if (!server->m_needcredentials)
91     return true;
92
93   const char *strbase = "Basic ";
94   const char *headervalue = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "Authorization");
95   if (NULL == headervalue)
96     return false;
97   if (strncmp (headervalue, strbase, strlen(strbase)))
98     return false;
99
100   return server->m_Credentials64Encoded.Equals(headervalue + strlen(strbase));
101 }
102
103 #if (MHD_VERSION >= 0x00040001)
104 int CWebServer::AnswerToConnection(void *cls, struct MHD_Connection *connection,
105                       const char *url, const char *method,
106                       const char *version, const char *upload_data,
107                       size_t *upload_data_size, void **con_cls)
108 #else
109 int CWebServer::AnswerToConnection(void *cls, struct MHD_Connection *connection,
110                       const char *url, const char *method,
111                       const char *version, const char *upload_data,
112                       unsigned int *upload_data_size, void **con_cls)
113 #endif
114 {
115   CWebServer *server = (CWebServer *)cls;
116   CStdString strURL = url;
117   CStdString originalURL = url;
118   HTTPMethod methodType = GetMethod(method);
119   
120   if (!IsAuthenticated(server, connection)) 
121     return AskForAuthentication(connection);
122
123   if (methodType != GET && methodType != POST) /* Only GET and POST supported, catch other method types here to avoid continual checking later on */
124     return CreateErrorResponse(connection, MHD_HTTP_NOT_IMPLEMENTED, methodType);
125
126 #ifdef HAS_JSONRPC
127   if (strURL.Equals("/jsonrpc"))
128   {
129     if (methodType == POST)
130       return JSONRPC(server, con_cls, connection, upload_data, upload_data_size);
131     else
132       return CreateMemoryDownloadResponse(connection, (void *)PAGE_JSONRPC_INFO, strlen(PAGE_JSONRPC_INFO));
133   }
134 #endif
135
136 #ifdef HAS_HTTPAPI
137   if ((methodType == GET || methodType == POST) && strURL.Left(18).Equals("/xbmcCmds/xbmcHttp"))
138     return HttpApi(connection);
139 #endif
140
141   if (strURL.Left(4).Equals("/vfs"))
142   {
143     strURL = strURL.Right(strURL.length() - 5);
144     CUtil::URLDecode(strURL);
145     return CreateFileDownloadResponse(connection, strURL);
146   }
147
148 #ifdef HAS_WEB_INTERFACE
149   if (strURL.Equals("/"))
150     strURL.Format("/%s", DEFAULT_PAGE);
151
152   AddonPtr addon;
153   CAddonMgr::Get().GetDefault(ADDON_WEB_INTERFACE,addon);
154   if (addon)
155     strURL = CUtil::AddFileToFolder(addon->Path(),strURL);
156   if (CDirectory::Exists(strURL))
157   {
158     if (strURL.Right(1).Equals("/"))
159       strURL += DEFAULT_PAGE;
160     else
161       return CreateRedirect(connection, originalURL += "/");
162   }
163   return CreateFileDownloadResponse(connection, strURL);
164
165 #endif
166
167   return MHD_NO;
168 }
169
170 CWebServer::HTTPMethod CWebServer::GetMethod(const char *method)
171 {
172   if (strcmp(method, "GET") == 0)
173     return GET;
174   if (strcmp(method, "POST") == 0)
175     return POST;
176   if (strcmp(method, "HEAD") == 0)
177     return HEAD;
178
179   return UNKNOWN;
180 }
181
182 #if (MHD_VERSION >= 0x00040001)
183 int CWebServer::JSONRPC(CWebServer *server, void **con_cls, struct MHD_Connection *connection, const char *upload_data, size_t *upload_data_size)
184 #else
185 int CWebServer::JSONRPC(CWebServer *server, void **con_cls, struct MHD_Connection *connection, const char *upload_data, unsigned int *upload_data_size)
186 #endif
187 {
188 #ifdef HAS_JSONRPC
189   if ((*con_cls) == NULL)
190   {
191     *con_cls = new CStdString();
192
193     return MHD_YES;
194   }
195   if (*upload_data_size) 
196   {
197     CStdString *post = (CStdString *)(*con_cls);
198     if (*upload_data_size + post->size() > MAX_STRING_POST_SIZE)
199     {
200       CLog::Log(LOGERROR, "WebServer: Stopped uploading post since it exceeded size limitations");
201       return MHD_NO;
202     }
203     else
204     {
205       post->append(upload_data, *upload_data_size);
206       *upload_data_size = 0;
207       return MHD_YES;
208     }
209   }
210   else
211   {
212     CStdString *jsoncall = (CStdString *)(*con_cls);
213
214     CHTTPClient client;
215     CStdString jsonresponse = CJSONRPC::MethodCall(*jsoncall, server, &client);
216
217     struct MHD_Response *response = MHD_create_response_from_data(jsonresponse.length(), (void *) jsonresponse.c_str(), MHD_NO, MHD_YES);
218     int ret = MHD_queue_response(connection, MHD_HTTP_OK, response);
219     MHD_add_response_header(response, "Content-Type", "application/json");
220     MHD_destroy_response(response);
221
222     delete jsoncall;
223     return ret;
224   }
225 #else
226   return MHD_NO;
227 #endif
228 }
229
230 int CWebServer::HttpApi(struct MHD_Connection *connection)
231 {
232 #ifdef HAS_HTTPAPI
233   map<CStdString, CStdString> arguments;
234   if (MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, FillArgumentMap, &arguments) > 0)
235   {
236     CStdString httpapiresponse = CHttpApi::WebMethodCall(arguments["command"], arguments["parameter"]);
237
238     struct MHD_Response *response = MHD_create_response_from_data(httpapiresponse.length(), (void *) httpapiresponse.c_str(), MHD_NO, MHD_YES);
239     int ret = MHD_queue_response(connection, MHD_HTTP_OK, response);
240     MHD_destroy_response(response);
241
242     return ret;
243   }
244 #endif
245   return MHD_NO;
246 }
247
248 int CWebServer::CreateRedirect(struct MHD_Connection *connection, const CStdString &strURL)
249 {
250   struct MHD_Response *response = MHD_create_response_from_data (0, NULL, MHD_NO, MHD_NO);
251   int ret = MHD_queue_response (connection, MHD_HTTP_FOUND, response);
252   MHD_add_response_header(response, "Location", strURL);
253   MHD_destroy_response (response);
254   return ret;
255 }
256
257 int CWebServer::CreateFileDownloadResponse(struct MHD_Connection *connection, const CStdString &strURL)
258 {
259   int ret = MHD_NO;
260   CFile *file = new CFile();
261
262   if (file->Open(strURL, READ_NO_CACHE))
263   {
264     struct MHD_Response *response;
265     response = MHD_create_response_from_callback ( file->GetLength(),
266                                                    2048,
267                                                    &CWebServer::ContentReaderCallback, file,
268                                                    &CWebServer::ContentReaderFreeCallback); 
269
270     CStdString ext = CUtil::GetExtension(strURL);
271     ext = ext.ToLower();
272     const char *mime = CreateMimeTypeFromExtension(ext.c_str());
273     if (mime)
274       MHD_add_response_header(response, "Content-Type", mime);
275
276     CDateTime expiryTime = CDateTime::GetCurrentDateTime();
277     expiryTime += CDateTimeSpan(1, 0, 0, 0);
278     MHD_add_response_header(response, "Expires", expiryTime.GetAsRFC1123DateTime());
279
280     ret = MHD_queue_response(connection, MHD_HTTP_OK, response);
281
282     MHD_destroy_response(response);
283   }
284   else
285   {
286     delete file;
287     CLog::Log(LOGERROR, "WebServer: Failed to open %s", strURL.c_str());
288     return CreateErrorResponse(connection, MHD_HTTP_NOT_FOUND, GET); /* GET Assumed Temporarily */
289   }
290   return ret;
291 }
292
293 int CWebServer::CreateErrorResponse(struct MHD_Connection *connection, int responseType, HTTPMethod method)
294 {
295   int ret = MHD_NO;
296   size_t payloadSize = 0;
297   void *payload = NULL;
298
299   if (method != HEAD)
300   {
301     switch (responseType)
302     {
303       case MHD_HTTP_NOT_FOUND:
304         payloadSize = strlen(PAGE_FILE_NOT_FOUND);
305         payload = (void *)PAGE_FILE_NOT_FOUND;
306         break;
307       case MHD_HTTP_NOT_IMPLEMENTED:
308         payloadSize = strlen(NOT_SUPPORTED);
309         payload = (void *)NOT_SUPPORTED;
310         break;
311     }
312   }
313
314   struct MHD_Response *response = MHD_create_response_from_data (payloadSize, payload, MHD_NO, MHD_NO);
315   ret = MHD_queue_response (connection, MHD_HTTP_NOT_FOUND, response);
316   MHD_destroy_response (response);
317   return ret;
318 }
319
320 int CWebServer::CreateMemoryDownloadResponse(struct MHD_Connection *connection, void *data, size_t size)
321 {
322   struct MHD_Response *response = MHD_create_response_from_data (size, data, MHD_NO, MHD_NO);
323   int ret = MHD_queue_response (connection, MHD_HTTP_OK, response);
324   MHD_destroy_response (response);
325   return ret;
326 }
327
328 #if (MHD_VERSION >= 0x00090200)
329 ssize_t CWebServer::ContentReaderCallback (void *cls, uint64_t pos, char *buf, size_t max)
330 #elif (MHD_VERSION >= 0x00040001)
331 int CWebServer::ContentReaderCallback(void *cls, uint64_t pos, char *buf, int max)
332 #else   //libmicrohttpd < 0.4.0
333 int CWebServer::ContentReaderCallback(void *cls, size_t pos, char *buf, int max)
334 #endif
335 {
336   CFile *file = (CFile *)cls;
337   if((unsigned int)pos != file->GetPosition())
338     file->Seek(pos);
339   unsigned res = file->Read(buf, max);
340   if(res == 0)
341     return -1;
342   return res;
343 }
344
345 void CWebServer::ContentReaderFreeCallback(void *cls)
346 {
347   CFile *file = (CFile *)cls;
348   file->Close();
349
350   delete file;
351 }
352
353 struct MHD_Daemon* CWebServer::StartMHD(unsigned int flags, int port)
354 {
355   // WARNING: when using MHD_USE_THREAD_PER_CONNECTION, set MHD_OPTION_CONNECTION_TIMEOUT to something higher than 1
356   // otherwise on libmicrohttpd 0.4.4-1 it spins a busy loop
357
358   unsigned int timeout = 60 * 60 * 24;
359   // MHD_USE_THREAD_PER_CONNECTION = one thread per connection
360   // MHD_USE_SELECT_INTERNALLY = use main thread for each connection, can only handle one request at a time [unless you set the thread pool size]
361
362   return MHD_start_daemon(flags,
363                           port,
364                           NULL,
365                           NULL,
366                           &CWebServer::AnswerToConnection,
367                           this,
368 #if (MHD_VERSION >= 0x00040002)
369                           MHD_OPTION_THREAD_POOL_SIZE, 1,
370 #endif
371                           MHD_OPTION_CONNECTION_LIMIT, 512,
372                           MHD_OPTION_CONNECTION_TIMEOUT, timeout,
373                           MHD_OPTION_END);
374 }
375
376 bool CWebServer::Start(int port, const CStdString &username, const CStdString &password)
377 {
378   SetCredentials(username, password);
379   if (!m_running)
380   {
381     m_daemon = StartMHD(MHD_USE_SELECT_INTERNALLY, port);
382
383     m_running = m_daemon != NULL;
384     if (m_running)
385       CLog::Log(LOGNOTICE, "WebServer: Started the webserver");
386     else
387       CLog::Log(LOGERROR, "WebServer: Failed to start the webserver");
388   }
389   return m_running;
390 }
391
392 bool CWebServer::Stop()
393 {
394   if (m_running)
395   {
396     MHD_stop_daemon(m_daemon);
397     m_running = false;
398     CLog::Log(LOGNOTICE, "WebServer: Stopped the webserver");
399   } else 
400     CLog::Log(LOGNOTICE, "WebServer: Stopped failed because its not running");
401
402   return !m_running;
403 }
404
405 bool CWebServer::IsStarted()
406 {
407   return m_running;
408 }
409
410 void CWebServer::StringToBase64(const char *input, CStdString &output)
411 {
412   const char *lookup = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
413   unsigned long l;
414   size_t length = strlen (input);
415   output = "";
416
417   for (unsigned int i = 0; i < length; i += 3)
418   {
419     l = (((unsigned long) input[i]) << 16)
420       | (((i + 1) < length) ? (((unsigned long) input[i + 1]) << 8) : 0)
421       | (((i + 2) < length) ? ((unsigned long) input[i + 2]) : 0);
422
423
424     output.push_back(lookup[(l >> 18) & 0x3F]);
425     output.push_back(lookup[(l >> 12) & 0x3F]);
426
427     if (i + 1 < length)
428       output.push_back(lookup[(l >> 6) & 0x3F]);
429     if (i + 2 < length)
430       output.push_back(lookup[l & 0x3F]);
431   }
432
433   int left = 3 - (length % 3);
434
435   if (length % 3)
436   {
437     for (int i = 0; i < left; i++)
438       output.push_back('=');
439   }
440 }
441
442 void CWebServer::SetCredentials(const CStdString &username, const CStdString &password)
443 {
444   CSingleLock lock (m_critSection);
445   CStdString str = username + ":" + password;
446
447   StringToBase64(str.c_str(), m_Credentials64Encoded);
448   m_needcredentials = !password.IsEmpty();
449 }
450
451 bool CWebServer::Download(const char *path, Json::Value *result)
452 {
453   bool exists = false;
454   CFile *file = new CFile();
455   if (file->Open(path))
456   {
457     exists = true;
458     file->Close();
459   }
460
461   delete file;
462
463   if (exists)
464   {
465     string str = "vfs/";
466     str += path;
467     (*result)["path"] = str;
468   }
469
470   return exists;
471 }
472
473 int CWebServer::GetCapabilities()
474 {
475   return Response | FileDownload;
476 }
477
478 const char *CWebServer::CreateMimeTypeFromExtension(const char *ext)
479 {
480   if (strcmp(ext, ".aif") == 0)   return "audio/aiff";
481   if (strcmp(ext, ".aiff") == 0)  return "audio/aiff";
482   if (strcmp(ext, ".asf") == 0)   return "video/x-ms-asf";
483   if (strcmp(ext, ".asx") == 0)   return "video/x-ms-asf";
484   if (strcmp(ext, ".avi") == 0)   return "video/avi";
485   if (strcmp(ext, ".avs") == 0)   return "video/avs-video";
486   if (strcmp(ext, ".bin") == 0)   return "application/octet-stream";
487   if (strcmp(ext, ".bmp") == 0)   return "image/bmp";
488   if (strcmp(ext, ".dv") == 0)    return "video/x-dv";
489   if (strcmp(ext, ".fli") == 0)   return "video/fli";
490   if (strcmp(ext, ".gif") == 0)   return "image/gif";
491   if (strcmp(ext, ".htm") == 0)   return "text/html";
492   if (strcmp(ext, ".html") == 0)  return "text/html";
493   if (strcmp(ext, ".htmls") == 0) return "text/html";
494   if (strcmp(ext, ".ico") == 0)   return "image/x-icon";
495   if (strcmp(ext, ".it") == 0)    return "audio/it";
496   if (strcmp(ext, ".jpeg") == 0)  return "image/jpeg";
497   if (strcmp(ext, ".jpg") == 0)   return "image/jpeg";
498   if (strcmp(ext, ".json") == 0)  return "application/json";
499   if (strcmp(ext, ".kar") == 0)   return "audio/midi";
500   if (strcmp(ext, ".list") == 0)  return "text/plain";
501   if (strcmp(ext, ".log") == 0)   return "text/plain";
502   if (strcmp(ext, ".lst") == 0)   return "text/plain";
503   if (strcmp(ext, ".m2v") == 0)   return "video/mpeg";
504   if (strcmp(ext, ".m3u") == 0)   return "audio/x-mpequrl";
505   if (strcmp(ext, ".mid") == 0)   return "audio/midi";
506   if (strcmp(ext, ".midi") == 0)  return "audio/midi";
507   if (strcmp(ext, ".mod") == 0)   return "audio/mod";
508   if (strcmp(ext, ".mov") == 0)   return "video/quicktime";
509   if (strcmp(ext, ".mp2") == 0)   return "audio/mpeg";
510   if (strcmp(ext, ".mp3") == 0)   return "audio/mpeg3";
511   if (strcmp(ext, ".mpa") == 0)   return "audio/mpeg";
512   if (strcmp(ext, ".mpeg") == 0)  return "video/mpeg";
513   if (strcmp(ext, ".mpg") == 0)   return "video/mpeg";
514   if (strcmp(ext, ".mpga") == 0)  return "audio/mpeg";
515   if (strcmp(ext, ".pcx") == 0)   return "image/x-pcx";
516   if (strcmp(ext, ".png") == 0)   return "image/png";
517   if (strcmp(ext, ".rm") == 0)    return "audio/x-pn-realaudio";
518   if (strcmp(ext, ".s3m") == 0)   return "audio/s3m";
519   if (strcmp(ext, ".sid") == 0)   return "audio/x-psid";
520   if (strcmp(ext, ".tif") == 0)   return "image/tiff";
521   if (strcmp(ext, ".tiff") == 0)  return "image/tiff";
522   if (strcmp(ext, ".txt") == 0)   return "text/plain";
523   if (strcmp(ext, ".uni") == 0)   return "text/uri-list";
524   if (strcmp(ext, ".viv") == 0)   return "video/vivo";
525   if (strcmp(ext, ".wav") == 0)   return "audio/wav";
526   if (strcmp(ext, ".xm") == 0)    return "audio/xm";
527   if (strcmp(ext, ".xml") == 0)   return "text/xml";
528   if (strcmp(ext, ".zip") == 0)   return "application/zip";
529   if (strcmp(ext, ".tbn") == 0)   return "image/jpeg";
530   if (strcmp(ext, ".js") == 0)    return "application/javascript";
531   if (strcmp(ext, ".css") == 0)   return "text/css";
532   return NULL;
533 }
534
535 int CWebServer::CHTTPClient::GetPermissionFlags()
536 {
537   return OPERATION_PERMISSION_ALL;
538 }
539
540 int CWebServer::CHTTPClient::GetAnnouncementFlags()
541 {
542   // Does not support broadcast
543   return 0;
544 }
545
546 bool CWebServer::CHTTPClient::SetAnnouncementFlags(int flags)
547 {
548   return false;
549 }
550 #endif