Merge remote branch 'mine/ext-python'
[vuplus_xbmc] / xbmc / network / 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 "interfaces/http-api/HttpApi.h"
25 #include "interfaces/json-rpc/JSONRPC.h"
26 #include "filesystem/File.h"
27 #include "filesystem/Directory.h"
28 #include "URL.h"
29 #include "utils/log.h"
30 #include "utils/URIUtils.h"
31 #include "threads/SingleLock.h"
32 #include "XBDateTime.h"
33 #include "addons/AddonManager.h"
34
35 #ifdef _WIN32
36 #pragma comment(lib, "../../lib/win32/libmicrohttpd_win32/lib/libmicrohttpd.dll.lib")
37 #endif
38
39 #define MAX_STRING_POST_SIZE 20000
40 #define PAGE_FILE_NOT_FOUND "<html><head><title>File not found</title></head><body>File not found</body></html>"
41 #define PAGE_JSONRPC_INFO   "<html><head><title>JSONRPC</title></head><body>JSONRPC active and working</body></html>"
42 #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>"
43 #define DEFAULT_PAGE        "index.html"
44
45 using namespace ADDON;
46 using namespace XFILE;
47 using namespace std;
48 using namespace JSONRPC;
49
50 CWebServer::CWebServer()
51 {
52   m_running = false;
53   m_daemon = NULL;
54   m_needcredentials = true;
55   m_Credentials64Encoded = "eGJtYzp4Ym1j"; // xbmc:xbmc
56 }
57
58 int CWebServer::FillArgumentMap(void *cls, enum MHD_ValueKind kind, const char *key, const char *value) 
59 {
60   map<CStdString, CStdString> *arguments = (map<CStdString, CStdString> *)cls;
61   arguments->insert( pair<CStdString,CStdString>(key,value) );
62   return MHD_YES; 
63 }
64
65 int CWebServer::AskForAuthentication(struct MHD_Connection *connection)
66 {
67   int ret;
68   struct MHD_Response *response;
69
70   response = MHD_create_response_from_data (0, NULL, MHD_NO, MHD_NO);
71   if (!response)
72     return MHD_NO;
73
74   ret = MHD_add_response_header (response, "WWW-Authenticate", "Basic realm=XBMC");
75   if (!ret)
76   {
77     MHD_destroy_response (response);
78     return MHD_NO;
79   }
80
81   ret = MHD_queue_response (connection, MHD_HTTP_UNAUTHORIZED, response);
82
83   MHD_destroy_response (response);
84
85   return ret;
86 }
87
88 bool CWebServer::IsAuthenticated(CWebServer *server, struct MHD_Connection *connection)
89 {
90   CSingleLock lock (server->m_critSection);
91   if (!server->m_needcredentials)
92     return true;
93
94   const char *strbase = "Basic ";
95   const char *headervalue = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, "Authorization");
96   if (NULL == headervalue)
97     return false;
98   if (strncmp (headervalue, strbase, strlen(strbase)))
99     return false;
100
101   return server->m_Credentials64Encoded.Equals(headervalue + strlen(strbase));
102 }
103
104 #if (MHD_VERSION >= 0x00040001)
105 int CWebServer::AnswerToConnection(void *cls, struct MHD_Connection *connection,
106                       const char *url, const char *method,
107                       const char *version, const char *upload_data,
108                       size_t *upload_data_size, void **con_cls)
109 #else
110 int CWebServer::AnswerToConnection(void *cls, struct MHD_Connection *connection,
111                       const char *url, const char *method,
112                       const char *version, const char *upload_data,
113                       unsigned int *upload_data_size, void **con_cls)
114 #endif
115 {
116   CWebServer *server = (CWebServer *)cls;
117   CStdString strURL = url;
118   CStdString originalURL = url;
119   HTTPMethod methodType = GetMethod(method);
120   
121   if (!IsAuthenticated(server, connection)) 
122     return AskForAuthentication(connection);
123
124 //  if (methodType != GET && methodType != POST) /* Only GET and POST supported, catch other method types here to avoid continual checking later on */
125 //    return CreateErrorResponse(connection, MHD_HTTP_NOT_IMPLEMENTED, methodType);
126
127 #ifdef HAS_JSONRPC
128   if (strURL.Equals("/jsonrpc"))
129   {
130     if (methodType == POST)
131       return JSONRPC(server, con_cls, connection, upload_data, upload_data_size);
132     else
133       return CreateMemoryDownloadResponse(connection, (void *)PAGE_JSONRPC_INFO, strlen(PAGE_JSONRPC_INFO));
134   }
135 #endif
136
137 #ifdef HAS_HTTPAPI
138   if ((methodType == GET || methodType == POST) && strURL.Left(18).Equals("/xbmcCmds/xbmcHttp"))
139     return HttpApi(connection);
140 #endif
141
142   if (strURL.Left(4).Equals("/vfs"))
143   {
144     strURL = strURL.Right(strURL.length() - 5);
145     CURL::Decode(strURL);
146     return CreateFileDownloadResponse(connection, strURL, methodType);
147   }
148
149 #ifdef HAS_WEB_INTERFACE
150   AddonPtr addon;
151   CStdString addonPath;
152   bool useDefaultWebInterface = true;
153   if (strURL.Left(8).Equals("/addons/") || (strURL == "/addons"))
154   {
155     CStdStringArray components;
156     CUtil::Tokenize(strURL,components,"/");
157     if (components.size() > 1)
158     {
159       CAddonMgr::Get().GetAddon(components.at(1),addon);
160       if (addon)
161       {
162         size_t pos;
163         pos = strURL.find('/', 8); // /addons/ = 8 characters +1 to start behind the last slash
164         if (pos != CStdString::npos)
165           strURL = strURL.substr(pos);
166         else // missing trailing slash
167           return CreateRedirect(connection, originalURL += "/");
168
169         useDefaultWebInterface = false;
170         addonPath = addon->Path();
171         if (addon->Type() != ADDON_WEB_INTERFACE) // No need to append /htdocs for web interfaces
172           addonPath = URIUtils::AddFileToFolder(addonPath, "/htdocs/");
173       }
174     }
175     else
176     {
177       if (strURL.length() < 8) // missing trailing slash
178         return CreateRedirect(connection, originalURL += "/");
179       else
180         return CreateAddonsListResponse(connection);
181     }
182   }
183
184   if (strURL.Equals("/"))
185     strURL.Format("/%s", DEFAULT_PAGE);
186
187   if (useDefaultWebInterface)
188   {
189     CAddonMgr::Get().GetDefault(ADDON_WEB_INTERFACE,addon);
190     if (addon)
191       addonPath = addon->Path();
192   }
193
194   if (addon)
195     strURL = URIUtils::AddFileToFolder(addon->Path(),strURL);
196   if (CDirectory::Exists(strURL))
197   {
198     if (strURL.Right(1).Equals("/"))
199       strURL += DEFAULT_PAGE;
200     else
201       return CreateRedirect(connection, originalURL += "/");
202   }
203   return CreateFileDownloadResponse(connection, strURL, methodType);
204
205 #endif
206
207   return MHD_NO;
208 }
209
210 CWebServer::HTTPMethod CWebServer::GetMethod(const char *method)
211 {
212   if (strcmp(method, "GET") == 0)
213     return GET;
214   if (strcmp(method, "POST") == 0)
215     return POST;
216   if (strcmp(method, "HEAD") == 0)
217     return HEAD;
218
219   return UNKNOWN;
220 }
221
222 #if (MHD_VERSION >= 0x00040001)
223 int CWebServer::JSONRPC(CWebServer *server, void **con_cls, struct MHD_Connection *connection, const char *upload_data, size_t *upload_data_size)
224 #else
225 int CWebServer::JSONRPC(CWebServer *server, void **con_cls, struct MHD_Connection *connection, const char *upload_data, unsigned int *upload_data_size)
226 #endif
227 {
228 #ifdef HAS_JSONRPC
229   if ((*con_cls) == NULL)
230   {
231     *con_cls = new CStdString();
232
233     return MHD_YES;
234   }
235   if (*upload_data_size) 
236   {
237     CStdString *post = (CStdString *)(*con_cls);
238     if (*upload_data_size + post->size() > MAX_STRING_POST_SIZE)
239     {
240       CLog::Log(LOGERROR, "WebServer: Stopped uploading post since it exceeded size limitations");
241       return MHD_NO;
242     }
243     else
244     {
245       post->append(upload_data, *upload_data_size);
246       *upload_data_size = 0;
247       return MHD_YES;
248     }
249   }
250   else
251   {
252     CStdString *jsoncall = (CStdString *)(*con_cls);
253
254     CHTTPClient client;
255     CStdString jsonresponse = CJSONRPC::MethodCall(*jsoncall, server, &client);
256
257     struct MHD_Response *response = MHD_create_response_from_data(jsonresponse.length(), (void *) jsonresponse.c_str(), MHD_NO, MHD_YES);
258     int ret = MHD_queue_response(connection, MHD_HTTP_OK, response);
259     MHD_add_response_header(response, "Content-Type", "application/json");
260     MHD_destroy_response(response);
261
262     delete jsoncall;
263     return ret;
264   }
265 #else
266   return MHD_NO;
267 #endif
268 }
269
270 int CWebServer::HttpApi(struct MHD_Connection *connection)
271 {
272 #ifdef HAS_HTTPAPI
273   map<CStdString, CStdString> arguments;
274   if (MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, FillArgumentMap, &arguments) > 0)
275   {
276     CStdString httpapiresponse = CHttpApi::WebMethodCall(arguments["command"], arguments["parameter"]);
277
278     struct MHD_Response *response = MHD_create_response_from_data(httpapiresponse.length(), (void *) httpapiresponse.c_str(), MHD_NO, MHD_YES);
279     int ret = MHD_queue_response(connection, MHD_HTTP_OK, response);
280     MHD_destroy_response(response);
281
282     return ret;
283   }
284 #endif
285   return MHD_NO;
286 }
287
288 int CWebServer::CreateRedirect(struct MHD_Connection *connection, const CStdString &strURL)
289 {
290   struct MHD_Response *response = MHD_create_response_from_data (0, NULL, MHD_NO, MHD_NO);
291   int ret = MHD_queue_response (connection, MHD_HTTP_FOUND, response);
292   MHD_add_response_header(response, "Location", strURL);
293   MHD_destroy_response (response);
294   return ret;
295 }
296
297 int CWebServer::CreateFileDownloadResponse(struct MHD_Connection *connection, const CStdString &strURL, HTTPMethod methodType)
298 {
299   int ret = MHD_NO;
300   CFile *file = new CFile();
301
302   if (file->Open(strURL, READ_NO_CACHE))
303   {
304     struct MHD_Response *response;
305     if (methodType != HEAD)
306     {
307       response = MHD_create_response_from_callback ( file->GetLength(),
308                                                      2048,
309                                                      &CWebServer::ContentReaderCallback, file,
310                                                      &CWebServer::ContentReaderFreeCallback); 
311     } else {
312       file->Close();
313       delete file;
314       response = MHD_create_response_from_data (0, NULL, MHD_NO, MHD_NO);
315     }
316
317     CStdString ext = URIUtils::GetExtension(strURL);
318     ext = ext.ToLower();
319     const char *mime = CreateMimeTypeFromExtension(ext.c_str());
320     if (mime)
321       MHD_add_response_header(response, "Content-Type", mime);
322
323     CDateTime expiryTime = CDateTime::GetCurrentDateTime();
324     expiryTime += CDateTimeSpan(1, 0, 0, 0);
325     MHD_add_response_header(response, "Expires", expiryTime.GetAsRFC1123DateTime());
326
327     ret = MHD_queue_response(connection, MHD_HTTP_OK, response);
328
329     MHD_destroy_response(response);
330   }
331   else
332   {
333     delete file;
334     CLog::Log(LOGERROR, "WebServer: Failed to open %s", strURL.c_str());
335     return CreateErrorResponse(connection, MHD_HTTP_NOT_FOUND, GET); /* GET Assumed Temporarily */
336   }
337   return ret;
338 }
339
340 int CWebServer::CreateErrorResponse(struct MHD_Connection *connection, int responseType, HTTPMethod method)
341 {
342   int ret = MHD_NO;
343   size_t payloadSize = 0;
344   void *payload = NULL;
345
346   if (method != HEAD)
347   {
348     switch (responseType)
349     {
350       case MHD_HTTP_NOT_FOUND:
351         payloadSize = strlen(PAGE_FILE_NOT_FOUND);
352         payload = (void *)PAGE_FILE_NOT_FOUND;
353         break;
354       case MHD_HTTP_NOT_IMPLEMENTED:
355         payloadSize = strlen(NOT_SUPPORTED);
356         payload = (void *)NOT_SUPPORTED;
357         break;
358     }
359   }
360
361   struct MHD_Response *response = MHD_create_response_from_data (payloadSize, payload, MHD_NO, MHD_NO);
362   ret = MHD_queue_response (connection, MHD_HTTP_NOT_FOUND, response);
363   MHD_destroy_response (response);
364   return ret;
365 }
366
367 int CWebServer::CreateMemoryDownloadResponse(struct MHD_Connection *connection, void *data, size_t size)
368 {
369   struct MHD_Response *response = MHD_create_response_from_data (size, data, MHD_NO, MHD_NO);
370   int ret = MHD_queue_response (connection, MHD_HTTP_OK, response);
371   MHD_destroy_response (response);
372   return ret;
373 }
374
375 int CWebServer::CreateAddonsListResponse(struct MHD_Connection *connection)
376 {
377   CStdString responseData = "<html><head><title>Add-on List</title></head><body>\n<h1>Available web interfaces:</h1>\n<ul>\n";
378   VECADDONS addons;
379   CAddonMgr::Get().GetAddons(ADDON_WEB_INTERFACE, addons);
380   IVECADDONS addons_it;
381   for (addons_it=addons.begin(); addons_it!=addons.end(); addons_it++)
382     responseData += "<li><a href=/addons/"+ (*addons_it)->ID() + "/>" + (*addons_it)->Name() + "</a></li>\n";
383
384   responseData += "</ul>\n</body></html>";
385
386   struct MHD_Response *response = MHD_create_response_from_data (responseData.length(), (void *)responseData.c_str(), MHD_NO, MHD_YES);
387   if (!response)
388     return MHD_NO;
389
390   int ret = MHD_queue_response (connection, MHD_HTTP_OK, response);
391   MHD_destroy_response (response);
392   return ret;
393 }
394
395 #if (MHD_VERSION >= 0x00090200)
396 ssize_t CWebServer::ContentReaderCallback (void *cls, uint64_t pos, char *buf, size_t max)
397 #elif (MHD_VERSION >= 0x00040001)
398 int CWebServer::ContentReaderCallback(void *cls, uint64_t pos, char *buf, int max)
399 #else   //libmicrohttpd < 0.4.0
400 int CWebServer::ContentReaderCallback(void *cls, size_t pos, char *buf, int max)
401 #endif
402 {
403   CFile *file = (CFile *)cls;
404   if((unsigned int)pos != file->GetPosition())
405     file->Seek(pos);
406   unsigned res = file->Read(buf, max);
407   if(res == 0)
408     return -1;
409   return res;
410 }
411
412 void CWebServer::ContentReaderFreeCallback(void *cls)
413 {
414   CFile *file = (CFile *)cls;
415   file->Close();
416
417   delete file;
418 }
419
420 struct MHD_Daemon* CWebServer::StartMHD(unsigned int flags, int port)
421 {
422   // WARNING: when using MHD_USE_THREAD_PER_CONNECTION, set MHD_OPTION_CONNECTION_TIMEOUT to something higher than 1
423   // otherwise on libmicrohttpd 0.4.4-1 it spins a busy loop
424
425   unsigned int timeout = 60 * 60 * 24;
426   // MHD_USE_THREAD_PER_CONNECTION = one thread per connection
427   // 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]
428
429   return MHD_start_daemon(flags,
430                           port,
431                           NULL,
432                           NULL,
433                           &CWebServer::AnswerToConnection,
434                           this,
435 #if (MHD_VERSION >= 0x00040002)
436                           MHD_OPTION_THREAD_POOL_SIZE, 1,
437 #endif
438                           MHD_OPTION_CONNECTION_LIMIT, 512,
439                           MHD_OPTION_CONNECTION_TIMEOUT, timeout,
440                           MHD_OPTION_END);
441 }
442
443 bool CWebServer::Start(int port, const CStdString &username, const CStdString &password)
444 {
445   SetCredentials(username, password);
446   if (!m_running)
447   {
448     m_daemon = StartMHD(MHD_USE_SELECT_INTERNALLY, port);
449
450     m_running = m_daemon != NULL;
451     if (m_running)
452       CLog::Log(LOGNOTICE, "WebServer: Started the webserver");
453     else
454       CLog::Log(LOGERROR, "WebServer: Failed to start the webserver");
455   }
456   return m_running;
457 }
458
459 bool CWebServer::Stop()
460 {
461   if (m_running)
462   {
463     MHD_stop_daemon(m_daemon);
464     m_running = false;
465     CLog::Log(LOGNOTICE, "WebServer: Stopped the webserver");
466   } else 
467     CLog::Log(LOGNOTICE, "WebServer: Stopped failed because its not running");
468
469   return !m_running;
470 }
471
472 bool CWebServer::IsStarted()
473 {
474   return m_running;
475 }
476
477 void CWebServer::StringToBase64(const char *input, CStdString &output)
478 {
479   const char *lookup = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
480   unsigned long l;
481   size_t length = strlen (input);
482   output = "";
483
484   for (unsigned int i = 0; i < length; i += 3)
485   {
486     l = (((unsigned long) input[i]) << 16)
487       | (((i + 1) < length) ? (((unsigned long) input[i + 1]) << 8) : 0)
488       | (((i + 2) < length) ? ((unsigned long) input[i + 2]) : 0);
489
490
491     output.push_back(lookup[(l >> 18) & 0x3F]);
492     output.push_back(lookup[(l >> 12) & 0x3F]);
493
494     if (i + 1 < length)
495       output.push_back(lookup[(l >> 6) & 0x3F]);
496     if (i + 2 < length)
497       output.push_back(lookup[l & 0x3F]);
498   }
499
500   int left = 3 - (length % 3);
501
502   if (length % 3)
503   {
504     for (int i = 0; i < left; i++)
505       output.push_back('=');
506   }
507 }
508
509 void CWebServer::SetCredentials(const CStdString &username, const CStdString &password)
510 {
511   CSingleLock lock (m_critSection);
512   CStdString str = username + ":" + password;
513
514   StringToBase64(str.c_str(), m_Credentials64Encoded);
515   m_needcredentials = !password.IsEmpty();
516 }
517
518 bool CWebServer::Download(const char *path, Json::Value *result)
519 {
520   bool exists = false;
521   CFile *file = new CFile();
522   if (file->Open(path))
523   {
524     exists = true;
525     file->Close();
526   }
527
528   delete file;
529
530   if (exists)
531   {
532     string str = "vfs/";
533     str += path;
534     (*result)["path"] = str;
535   }
536
537   return exists;
538 }
539
540 int CWebServer::GetCapabilities()
541 {
542   return Response | FileDownload;
543 }
544
545 const char *CWebServer::CreateMimeTypeFromExtension(const char *ext)
546 {
547   if (strcmp(ext, ".aif") == 0)   return "audio/aiff";
548   if (strcmp(ext, ".aiff") == 0)  return "audio/aiff";
549   if (strcmp(ext, ".asf") == 0)   return "video/x-ms-asf";
550   if (strcmp(ext, ".asx") == 0)   return "video/x-ms-asf";
551   if (strcmp(ext, ".avi") == 0)   return "video/avi";
552   if (strcmp(ext, ".avs") == 0)   return "video/avs-video";
553   if (strcmp(ext, ".bin") == 0)   return "application/octet-stream";
554   if (strcmp(ext, ".bmp") == 0)   return "image/bmp";
555   if (strcmp(ext, ".dv") == 0)    return "video/x-dv";
556   if (strcmp(ext, ".fli") == 0)   return "video/fli";
557   if (strcmp(ext, ".gif") == 0)   return "image/gif";
558   if (strcmp(ext, ".htm") == 0)   return "text/html";
559   if (strcmp(ext, ".html") == 0)  return "text/html";
560   if (strcmp(ext, ".htmls") == 0) return "text/html";
561   if (strcmp(ext, ".ico") == 0)   return "image/x-icon";
562   if (strcmp(ext, ".it") == 0)    return "audio/it";
563   if (strcmp(ext, ".jpeg") == 0)  return "image/jpeg";
564   if (strcmp(ext, ".jpg") == 0)   return "image/jpeg";
565   if (strcmp(ext, ".json") == 0)  return "application/json";
566   if (strcmp(ext, ".kar") == 0)   return "audio/midi";
567   if (strcmp(ext, ".list") == 0)  return "text/plain";
568   if (strcmp(ext, ".log") == 0)   return "text/plain";
569   if (strcmp(ext, ".lst") == 0)   return "text/plain";
570   if (strcmp(ext, ".m2v") == 0)   return "video/mpeg";
571   if (strcmp(ext, ".m3u") == 0)   return "audio/x-mpequrl";
572   if (strcmp(ext, ".mid") == 0)   return "audio/midi";
573   if (strcmp(ext, ".midi") == 0)  return "audio/midi";
574   if (strcmp(ext, ".mod") == 0)   return "audio/mod";
575   if (strcmp(ext, ".mov") == 0)   return "video/quicktime";
576   if (strcmp(ext, ".mp2") == 0)   return "audio/mpeg";
577   if (strcmp(ext, ".mp3") == 0)   return "audio/mpeg3";
578   if (strcmp(ext, ".mpa") == 0)   return "audio/mpeg";
579   if (strcmp(ext, ".mpeg") == 0)  return "video/mpeg";
580   if (strcmp(ext, ".mpg") == 0)   return "video/mpeg";
581   if (strcmp(ext, ".mpga") == 0)  return "audio/mpeg";
582   if (strcmp(ext, ".pcx") == 0)   return "image/x-pcx";
583   if (strcmp(ext, ".png") == 0)   return "image/png";
584   if (strcmp(ext, ".rm") == 0)    return "audio/x-pn-realaudio";
585   if (strcmp(ext, ".s3m") == 0)   return "audio/s3m";
586   if (strcmp(ext, ".sid") == 0)   return "audio/x-psid";
587   if (strcmp(ext, ".tif") == 0)   return "image/tiff";
588   if (strcmp(ext, ".tiff") == 0)  return "image/tiff";
589   if (strcmp(ext, ".txt") == 0)   return "text/plain";
590   if (strcmp(ext, ".uni") == 0)   return "text/uri-list";
591   if (strcmp(ext, ".viv") == 0)   return "video/vivo";
592   if (strcmp(ext, ".wav") == 0)   return "audio/wav";
593   if (strcmp(ext, ".xm") == 0)    return "audio/xm";
594   if (strcmp(ext, ".xml") == 0)   return "text/xml";
595   if (strcmp(ext, ".zip") == 0)   return "application/zip";
596   if (strcmp(ext, ".tbn") == 0)   return "image/jpeg";
597   if (strcmp(ext, ".js") == 0)    return "application/javascript";
598   if (strcmp(ext, ".css") == 0)   return "text/css";
599   return NULL;
600 }
601
602 int CWebServer::CHTTPClient::GetPermissionFlags()
603 {
604   return OPERATION_PERMISSION_ALL;
605 }
606
607 int CWebServer::CHTTPClient::GetAnnouncementFlags()
608 {
609   // Does not support broadcast
610   return 0;
611 }
612
613 bool CWebServer::CHTTPClient::SetAnnouncementFlags(int flags)
614 {
615   return false;
616 }
617 #endif