Logo ROOT   6.30.04
Reference Guide
 All Namespaces Files Pages
TCivetweb.cxx
Go to the documentation of this file.
1 // $Id$
2 // Author: Sergey Linev 21/12/2013
3 
4 /*************************************************************************
5  * Copyright (C) 1995-2013, Rene Brun and Fons Rademakers. *
6  * All rights reserved. *
7  * *
8  * For the licensing terms see $ROOTSYS/LICENSE. *
9  * For the list of contributors see $ROOTSYS/README/CREDITS. *
10  *************************************************************************/
11 
12 #include "TCivetweb.h"
13 
14 #include "../civetweb/civetweb.h"
15 
16 #include <stdlib.h>
17 #include <string.h>
18 
19 #ifdef _MSC_VER
20 #include <windows.h>
21 #include <tchar.h>
22 #endif
23 
24 #include "THttpServer.h"
25 #include "THttpWSEngine.h"
26 #include "TUrl.h"
27 
28 
29 
30 //////////////////////////////////////////////////////////////////////////
31 // //
32 // TCivetwebWSEngine //
33 // //
34 // Implementation of THttpWSEngine for Civetweb //
35 // //
36 //////////////////////////////////////////////////////////////////////////
37 
38 class TCivetwebWSEngine : public THttpWSEngine {
39 protected:
40  struct mg_connection *fWSconn;
41 
42  /// True websocket requires extra thread to parallelize sending
43  Bool_t SupportSendThrd() const override { return kTRUE; }
44 
45 public:
46  TCivetwebWSEngine(struct mg_connection *conn) : THttpWSEngine(), fWSconn(conn) {}
47 
48  virtual ~TCivetwebWSEngine() = default;
49 
50  UInt_t GetId() const override { return TString::Hash((void *)&fWSconn, sizeof(void *)); }
51 
52  void ClearHandle(Bool_t terminate) override
53  {
54  if (fWSconn && terminate)
55  mg_websocket_write(fWSconn, MG_WEBSOCKET_OPCODE_CONNECTION_CLOSE, nullptr, 0);
56  fWSconn = nullptr;
57  }
58 
59  void Send(const void *buf, int len) override
60  {
61  if (fWSconn)
62  mg_websocket_write(fWSconn, MG_WEBSOCKET_OPCODE_BINARY, (const char *)buf, len);
63  }
64 
65  /////////////////////////////////////////////////////////
66  /// Special method to send binary data with text header
67  /// For normal websocket it is two separated operation, for other engines could be combined together,
68  /// but emulates as two messages on client side
69  void SendHeader(const char *hdr, const void *buf, int len) override
70  {
71  if (fWSconn) {
72  mg_websocket_write(fWSconn, MG_WEBSOCKET_OPCODE_TEXT, hdr, strlen(hdr));
73  mg_websocket_write(fWSconn, MG_WEBSOCKET_OPCODE_BINARY, (const char *)buf, len);
74  }
75  }
76 
77  void SendCharStar(const char *str) override
78  {
79  if (fWSconn)
80  mg_websocket_write(fWSconn, MG_WEBSOCKET_OPCODE_TEXT, str, strlen(str));
81  }
82 };
83 
84 //////////////////////////////////////////////////////////////////////////
85 
86 int websocket_connect_handler(const struct mg_connection *conn, void *)
87 {
88  const struct mg_request_info *request_info = mg_get_request_info(conn);
89  if (!request_info)
90  return 1;
91 
92  TCivetweb *engine = (TCivetweb *)request_info->user_data;
93  if (!engine || engine->IsTerminating())
94  return 1;
95  THttpServer *serv = engine->GetServer();
96  if (!serv)
97  return 1;
98 
99  auto arg = std::make_shared<THttpCallArg>();
100  arg->SetPathAndFileName(request_info->local_uri); // path and file name
101  arg->SetQuery(request_info->query_string); // query arguments
102  arg->SetWSId(TString::Hash((void *)&conn, sizeof(void *)));
103  arg->SetMethod("WS_CONNECT");
104 
105  Bool_t execres = serv->ExecuteWS(arg, kTRUE, kTRUE);
106 
107  return execres && !arg->Is404() ? 0 : 1;
108 }
109 
110 //////////////////////////////////////////////////////////////////////////
111 
112 void websocket_ready_handler(struct mg_connection *conn, void *)
113 {
114  const struct mg_request_info *request_info = mg_get_request_info(conn);
115 
116  TCivetweb *engine = (TCivetweb *)request_info->user_data;
117  if (!engine || engine->IsTerminating())
118  return;
119  THttpServer *serv = engine->GetServer();
120  if (!serv)
121  return;
122 
123  auto arg = std::make_shared<THttpCallArg>();
124  arg->SetPathAndFileName(request_info->local_uri); // path and file name
125  arg->SetQuery(request_info->query_string); // query arguments
126  arg->SetMethod("WS_READY");
127 
128  // delegate ownership to the arg, id will be automatically set
129  arg->CreateWSEngine<TCivetwebWSEngine>(conn);
130 
131  serv->ExecuteWS(arg, kTRUE, kTRUE);
132 }
133 
134 //////////////////////////////////////////////////////////////////////////
135 
136 int websocket_data_handler(struct mg_connection *conn, int code, char *data, size_t len, void *)
137 {
138  const struct mg_request_info *request_info = mg_get_request_info(conn);
139 
140  // do not handle empty data
141  if (len == 0)
142  return 1;
143 
144  TCivetweb *engine = (TCivetweb *)request_info->user_data;
145  if (!engine || engine->IsTerminating())
146  return 1;
147  THttpServer *serv = engine->GetServer();
148  if (!serv)
149  return 1;
150 
151  std::string *conn_data = (std::string *) mg_get_user_connection_data(conn);
152 
153  // this is continuation of the request
154  if (!(code & 0x80)) {
155  if (!conn_data) {
156  conn_data = new std::string(data,len);
157  mg_set_user_connection_data(conn, conn_data);
158  } else {
159  conn_data->append(data,len);
160  }
161  return 1;
162  }
163 
164  auto arg = std::make_shared<THttpCallArg>();
165  arg->SetPathAndFileName(request_info->local_uri); // path and file name
166  arg->SetQuery(request_info->query_string); // query arguments
167  arg->SetWSId(TString::Hash((void *)&conn, sizeof(void *)));
168  arg->SetMethod("WS_DATA");
169 
170  if (conn_data) {
171  mg_set_user_connection_data(conn, nullptr);
172  conn_data->append(data,len);
173  arg->SetPostData(std::move(*conn_data));
174  delete conn_data;
175  } else {
176  arg->SetPostData(std::string(data,len));
177  }
178 
179  serv->ExecuteWS(arg, kTRUE, kTRUE);
180 
181  return 1;
182 }
183 
184 //////////////////////////////////////////////////////////////////////////
185 
186 void websocket_close_handler(const struct mg_connection *conn, void *)
187 {
188  const struct mg_request_info *request_info = mg_get_request_info(conn);
189 
190  TCivetweb *engine = (TCivetweb *)request_info->user_data;
191  if (!engine || engine->IsTerminating())
192  return;
193  THttpServer *serv = engine->GetServer();
194  if (!serv)
195  return;
196 
197  auto arg = std::make_shared<THttpCallArg>();
198  arg->SetPathAndFileName(request_info->local_uri); // path and file name
199  arg->SetQuery(request_info->query_string); // query arguments
200  arg->SetWSId(TString::Hash((void *)&conn, sizeof(void *)));
201  arg->SetMethod("WS_CLOSE");
202 
203  serv->ExecuteWS(arg, kTRUE, kFALSE); // do not wait for result of execution
204 }
205 
206 //////////////////////////////////////////////////////////////////////////
207 
208 static int log_message_handler(const struct mg_connection *conn, const char *message)
209 {
210  const struct mg_context *ctx = mg_get_context(conn);
211 
212  TCivetweb *engine = (TCivetweb *)mg_get_user_data(ctx);
213 
214  if (engine)
215  return engine->ProcessLog(message);
216 
217  // provide debug output
218  if ((gDebug > 0) || (strstr(message, "cannot bind to") != 0))
219  fprintf(stderr, "Error in <TCivetweb::Log> %s\n", message);
220 
221  return 0;
222 }
223 
224 //////////////////////////////////////////////////////////////////////////
225 
226 static int begin_request_handler(struct mg_connection *conn, void *)
227 {
228  const struct mg_request_info *request_info = mg_get_request_info(conn);
229 
230  TCivetweb *engine = (TCivetweb *)request_info->user_data;
231  if (!engine || engine->IsTerminating())
232  return 0;
233  THttpServer *serv = engine->GetServer();
234  if (!serv)
235  return 0;
236 
237  auto arg = std::make_shared<THttpCallArg>();
238 
239  TString filename;
240 
241  Bool_t execres = kTRUE, debug = engine->IsDebugMode();
242 
243  if (!debug && serv->IsFileRequested(request_info->local_uri, filename)) {
244  if ((filename.Length() > 3) && ((filename.Index(".js") != kNPOS) || (filename.Index(".css") != kNPOS))) {
245  std::string buf = THttpServer::ReadFileContent(filename.Data());
246  if (buf.empty()) {
247  arg->Set404();
248  } else {
249  arg->SetContentType(THttpServer::GetMimeType(filename.Data()));
250  arg->SetContent(std::move(buf));
251  if (engine->GetMaxAge() > 0)
252  arg->AddHeader("Cache-Control", TString::Format("max-age=%d", engine->GetMaxAge()));
253  else
254  arg->AddNoCacheHeader();
255  arg->SetZipping();
256  }
257  } else {
258  arg->SetFile(filename.Data());
259  }
260  } else {
261  arg->SetPathAndFileName(request_info->local_uri); // path and file name
262  arg->SetQuery(request_info->query_string); // query arguments
263  arg->SetTopName(engine->GetTopName());
264  arg->SetMethod(request_info->request_method); // method like GET or POST
265  if (request_info->remote_user)
266  arg->SetUserName(request_info->remote_user);
267 
268  TString header;
269  for (int n = 0; n < request_info->num_headers; n++)
270  header.Append(
271  TString::Format("%s: %s\r\n", request_info->http_headers[n].name, request_info->http_headers[n].value));
272  arg->SetRequestHeader(header);
273 
274  const char *len = mg_get_header(conn, "Content-Length");
275  Int_t ilen = len ? TString(len).Atoi() : 0;
276 
277  if (ilen > 0) {
278  std::string buf;
279  buf.resize(ilen);
280  Int_t iread = mg_read(conn, (void *) buf.data(), ilen);
281  if (iread == ilen)
282  arg->SetPostData(std::move(buf));
283  }
284 
285  if (debug) {
286  TString cont;
287  cont.Append("<title>Civetweb echo</title>");
288  cont.Append("<h1>Civetweb echo</h1>\n");
289 
290  static int count = 0;
291 
292  cont.Append(TString::Format("Request %d:<br/>\n<pre>\n", ++count));
293  cont.Append(TString::Format(" Method : %s\n", arg->GetMethod()));
294  cont.Append(TString::Format(" PathName : %s\n", arg->GetPathName()));
295  cont.Append(TString::Format(" FileName : %s\n", arg->GetFileName()));
296  cont.Append(TString::Format(" Query : %s\n", arg->GetQuery()));
297  cont.Append(TString::Format(" PostData : %ld\n", arg->GetPostDataLength()));
298  if (arg->GetUserName())
299  cont.Append(TString::Format(" User : %s\n", arg->GetUserName()));
300 
301  cont.Append("</pre><p>\n");
302 
303  cont.Append("Environment:<br/>\n<pre>\n");
304  for (int n = 0; n < request_info->num_headers; n++)
305  cont.Append(
306  TString::Format(" %s = %s\n", request_info->http_headers[n].name, request_info->http_headers[n].value));
307  cont.Append("</pre><p>\n");
308 
309  arg->SetContentType("text/html");
310 
311  arg->SetContent(cont);
312 
313  } else {
314  execres = serv->ExecuteHttp(arg);
315  }
316  }
317 
318  if (!execres || arg->Is404()) {
319  std::string hdr = arg->FillHttpHeader("HTTP/1.1");
320  mg_printf(conn, "%s", hdr.c_str());
321  } else if (arg->IsFile()) {
322  filename = (const char *)arg->GetContent();
323 #ifdef _MSC_VER
324  // resolve Windows links which are not supported by civetweb
325  const int BUFSIZE = 2048;
326  TCHAR Path[BUFSIZE];
327 
328  auto hFile = CreateFile(filename.Data(), // file to open
329  GENERIC_READ, // open for reading
330  FILE_SHARE_READ, // share for reading
331  NULL, // default security
332  OPEN_EXISTING, // existing file only
333  FILE_ATTRIBUTE_NORMAL, // normal file
334  NULL); // no attr. template
335 
336  if( hFile != INVALID_HANDLE_VALUE) {
337  auto dwRet = GetFinalPathNameByHandle( hFile, Path, BUFSIZE, VOLUME_NAME_DOS );
338  // produced file name may include \\? symbols, which are indicating long file name
339  if(dwRet < BUFSIZE)
340  filename = Path;
341  CloseHandle(hFile);
342  }
343 #endif
344  mg_send_file(conn, filename.Data());
345  } else {
346 
347  Bool_t dozip = kFALSE;
348  switch (arg->GetZipping()) {
349  case THttpCallArg::kNoZip: dozip = kFALSE; break;
350  case THttpCallArg::kZipLarge:
351  if (arg->GetContentLength() < 10000) break;
352  case THttpCallArg::kZip:
353  // check if request header has Accept-Encoding
354  for (int n = 0; n < request_info->num_headers; n++) {
355  TString name = request_info->http_headers[n].name;
356  if (name.Index("Accept-Encoding", 0, TString::kIgnoreCase) != 0)
357  continue;
358  TString value = request_info->http_headers[n].value;
359  dozip = (value.Index("gzip", 0, TString::kIgnoreCase) != kNPOS);
360  break;
361  }
362 
363  break;
364  case THttpCallArg::kZipAlways: dozip = kTRUE; break;
365  }
366 
367  if (dozip)
368  arg->CompressWithGzip();
369 
370  std::string hdr = arg->FillHttpHeader("HTTP/1.1");
371  mg_printf(conn, "%s", hdr.c_str());
372 
373  if (arg->GetContentLength() > 0)
374  mg_write(conn, arg->GetContent(), (size_t)arg->GetContentLength());
375  }
376 
377  // Returning non-zero tells civetweb that our function has replied to
378  // the client, and civetweb should not send client any more data.
379  return 1;
380 }
381 
382 //////////////////////////////////////////////////////////////////////////
383 // //
384 // TCivetweb //
385 // //
386 // http server implementation, based on civetweb embedded server //
387 // It is default kind of engine, created for THttpServer //
388 // Currently v1.8 from https://github.com/civetweb/civetweb is used //
389 // //
390 // Following additional options can be specified: //
391 // top=foldername - name of top folder, seen in the browser //
392 // thrds=N - use N threads to run civetweb server (default 5) //
393 // auth_file - global authentication file //
394 // auth_domain - domain name, used for authentication //
395 // //
396 // Example: //
397 // new THttpServer("http:8080?top=MyApp&thrds=3"); //
398 // //
399 // Authentication: //
400 // When auth_file and auth_domain parameters are specified, access //
401 // to running http server will be possible only after user //
402 // authentication, using so-call digest method. To generate //
403 // authentication file, htdigest routine should be used: //
404 // //
405 // [shell] htdigest -c .htdigest domain_name user //
406 // //
407 // When creating server, parameters should be: //
408 // //
409 // new THttpServer("http:8080?auth_file=.htdigets&auth_domain=domain_name"); //
410 // //
411 //////////////////////////////////////////////////////////////////////////
412 
413 ////////////////////////////////////////////////////////////////////////////////
414 /// constructor
415 
416 TCivetweb::TCivetweb(Bool_t only_secured)
417  : THttpEngine("civetweb", "compact embedded http server"), fCtx(nullptr), fCallbacks(nullptr), fTopName(),
418  fDebug(kFALSE), fTerminating(kFALSE), fOnlySecured(only_secured)
419 {
420 }
421 
422 ////////////////////////////////////////////////////////////////////////////////
423 /// destructor
424 
425 TCivetweb::~TCivetweb()
426 {
427  if (fCtx && !fTerminating)
428  mg_stop((struct mg_context *)fCtx);
429  if (fCallbacks)
430  free(fCallbacks);
431 }
432 
433 ////////////////////////////////////////////////////////////////////////////////
434 /// process civetweb log message, can be used to detect critical errors
435 
436 Int_t TCivetweb::ProcessLog(const char *message)
437 {
438  if ((gDebug > 0) || (strstr(message, "cannot bind to") != 0))
439  Error("Log", "%s", message);
440 
441  return 0;
442 }
443 
444 ////////////////////////////////////////////////////////////////////////////////
445 /// Creates embedded civetweb server
446 /// As main argument, http port should be specified like "8090".
447 /// Or one can provide combination of ipaddress and portnumber like 127.0.0.1:8090
448 /// Extra parameters like in URL string could be specified after '?' mark:
449 /// thrds=N - there N is number of threads used by the civetweb (default is 10)
450 /// top=name - configure top name, visible in the web browser
451 /// ssl_certificate=filename - SSL certificate, see docs/OpenSSL.md from civetweb
452 /// auth_file=filename - authentication file name, created with htdigets utility
453 /// auth_domain=domain - authentication domain
454 /// websocket_timeout=tm - set web sockets timeout in seconds (default 300)
455 /// websocket_disable - disable web sockets handling (default enabled)
456 /// bind - ip address to bind server socket
457 /// loopback - bind specified port to loopback 127.0.0.1 address
458 /// debug - enable debug mode, server always returns html page with request info
459 /// log=filename - configure civetweb log file
460 /// max_age=value - configures "Cache-Control: max_age=value" http header for all file-related requests, default 3600
461 /// nocache - try to fully disable cache control for file requests
462 /// Examples:
463 /// http:8080?websocket_disable
464 /// http:7546?thrds=30&websocket_timeout=20
465 
466 Bool_t TCivetweb::Create(const char *args)
467 {
468  fCallbacks = malloc(sizeof(struct mg_callbacks));
469  memset(fCallbacks, 0, sizeof(struct mg_callbacks));
470  //((struct mg_callbacks *) fCallbacks)->begin_request = begin_request_handler;
471  ((struct mg_callbacks *)fCallbacks)->log_message = log_message_handler;
472  TString sport = IsSecured() ? "8480s" : "8080", num_threads = "10", websocket_timeout = "300000";
473  TString auth_file, auth_domain, log_file, ssl_cert, max_age;
474  Bool_t use_ws = kTRUE;
475 
476  // extract arguments
477  if (args && (strlen(args) > 0)) {
478 
479  // first extract port number
480  sport = "";
481  while ((*args != 0) && (*args != '?') && (*args != '/'))
482  sport.Append(*args++);
483  if (IsSecured() && (sport.Index("s")==kNPOS)) sport.Append("s");
484 
485  // than search for extra parameters
486  while ((*args != 0) && (*args != '?'))
487  args++;
488 
489  if (*args == '?') {
490  TUrl url(TString::Format("http://localhost/folder%s", args));
491 
492  if (url.IsValid()) {
493  url.ParseOptions();
494 
495  const char *top = url.GetValueFromOptions("top");
496  if (top)
497  fTopName = top;
498 
499  const char *log = url.GetValueFromOptions("log");
500  if (log)
501  log_file = log;
502 
503  Int_t thrds = url.GetIntValueFromOptions("thrds");
504  if (thrds > 0)
505  num_threads.Form("%d", thrds);
506 
507  const char *afile = url.GetValueFromOptions("auth_file");
508  if (afile)
509  auth_file = afile;
510 
511  const char *adomain = url.GetValueFromOptions("auth_domain");
512  if (adomain)
513  auth_domain = adomain;
514 
515  const char *sslc = url.GetValueFromOptions("ssl_cert");
516  if (sslc)
517  ssl_cert = sslc;
518 
519  Int_t wtmout = url.GetIntValueFromOptions("websocket_timeout");
520  if (wtmout > 0) {
521  websocket_timeout.Format("%d", wtmout * 1000);
522  use_ws = kTRUE;
523  }
524 
525  if (url.HasOption("websocket_disable"))
526  use_ws = kFALSE;
527 
528  if (url.HasOption("debug"))
529  fDebug = kTRUE;
530 
531  if (url.HasOption("loopback") && (sport.Index(":") == kNPOS))
532  sport = TString("127.0.0.1:") + sport;
533 
534  if (url.HasOption("bind") && (sport.Index(":") == kNPOS)) {
535  const char *addr = url.GetValueFromOptions("bind");
536  if (addr && strlen(addr))
537  sport = TString(addr) + ":" + sport;
538  }
539 
540  if (GetServer() && url.HasOption("cors")) {
541  const char *cors = url.GetValueFromOptions("cors");
542  GetServer()->SetCors(cors && *cors ? cors : "*");
543  }
544 
545  if (url.HasOption("nocache"))
546  fMaxAge = 0;
547 
548  if (url.HasOption("max_age"))
549  fMaxAge = url.GetIntValueFromOptions("max_age");
550 
551  max_age.Form("%d", fMaxAge);
552  }
553  }
554  }
555 
556  const char *options[20];
557  int op(0);
558 
559  Info("Create", "Starting HTTP server on port %s", sport.Data());
560 
561  options[op++] = "listening_ports";
562  options[op++] = sport.Data();
563  options[op++] = "num_threads";
564  options[op++] = num_threads.Data();
565 
566  if (use_ws) {
567  options[op++] = "websocket_timeout_ms";
568  options[op++] = websocket_timeout.Data();
569  }
570 
571  if ((auth_file.Length() > 0) && (auth_domain.Length() > 0)) {
572  options[op++] = "global_auth_file";
573  options[op++] = auth_file.Data();
574  options[op++] = "authentication_domain";
575  options[op++] = auth_domain.Data();
576  }
577 
578  if (log_file.Length() > 0) {
579  options[op++] = "error_log_file";
580  options[op++] = log_file.Data();
581  }
582 
583  if (ssl_cert.Length() > 0) {
584  options[op++] = "ssl_certificate";
585  options[op++] = ssl_cert.Data();
586  } else if (IsSecured()) {
587  Error("Create", "No SSL certificate file configured");
588  }
589 
590  if (max_age.Length() > 0) {
591  options[op++] = "static_file_max_age";
592  options[op++] = max_age.Data();
593  }
594 
595  options[op++] = nullptr;
596 
597  // Start the web server.
598  fCtx = mg_start((struct mg_callbacks *)fCallbacks, this, options);
599 
600  if (!fCtx)
601  return kFALSE;
602 
603  mg_set_request_handler((struct mg_context *)fCtx, "/", begin_request_handler, nullptr);
604 
605  if (use_ws)
606  mg_set_websocket_handler((struct mg_context *)fCtx, "**root.websocket$", websocket_connect_handler,
607  websocket_ready_handler, websocket_data_handler, websocket_close_handler, nullptr);
608 
609  return kTRUE;
610 }