// $Id$ // Author: Sergey Linev 21/12/2013 /************************************************************************* * Copyright (C) 1995-2013, Rene Brun and Fons Rademakers. * * All rights reserved. * * * * For the licensing terms see $ROOTSYS/LICENSE. * * For the list of contributors see $ROOTSYS/README/CREDITS. * *************************************************************************/ #include "TCivetweb.h" #include "../civetweb/civetweb.h" #include <stdlib.h> #include <string.h> #ifdef _MSC_VER #include <windows.h> #include <tchar.h> #endif #include "THttpServer.h" #include "THttpWSEngine.h" #include "TUrl.h" ////////////////////////////////////////////////////////////////////////// // // // TCivetwebWSEngine // // // // Implementation of THttpWSEngine for Civetweb // // // ////////////////////////////////////////////////////////////////////////// class TCivetwebWSEngine : public THttpWSEngine { protected: struct mg_connection *fWSconn; /// True websocket requires extra thread to parallelize sending Bool_t SupportSendThrd() const override { return kTRUE; } public: TCivetwebWSEngine(struct mg_connection *conn) : THttpWSEngine(), fWSconn(conn) {} virtual ~TCivetwebWSEngine() = default; UInt_t GetId() const override { return TString::Hash((void *)&fWSconn, sizeof(void *)); } void ClearHandle(Bool_t terminate) override { if (fWSconn && terminate) mg_websocket_write(fWSconn, MG_WEBSOCKET_OPCODE_CONNECTION_CLOSE, nullptr, 0); fWSconn = nullptr; } void Send(const void *buf, int len) override { if (fWSconn) mg_websocket_write(fWSconn, MG_WEBSOCKET_OPCODE_BINARY, (const char *)buf, len); } ///////////////////////////////////////////////////////// /// Special method to send binary data with text header /// For normal websocket it is two separated operation, for other engines could be combined together, /// but emulates as two messages on client side void SendHeader(const char *hdr, const void *buf, int len) override { if (fWSconn) { mg_websocket_write(fWSconn, MG_WEBSOCKET_OPCODE_TEXT, hdr, strlen(hdr)); mg_websocket_write(fWSconn, MG_WEBSOCKET_OPCODE_BINARY, (const char *)buf, len); } } void SendCharStar(const char *str) override { if (fWSconn) mg_websocket_write(fWSconn, MG_WEBSOCKET_OPCODE_TEXT, str, strlen(str)); } }; ////////////////////////////////////////////////////////////////////////// int websocket_connect_handler(const struct mg_connection *conn, void *) { const struct mg_request_info *request_info = mg_get_request_info(conn); if (!request_info) return 1; TCivetweb *engine = (TCivetweb *)request_info->user_data; if (!engine || engine->IsTerminating()) return 1; THttpServer *serv = engine->GetServer(); if (!serv) return 1; auto arg = std::make_shared<THttpCallArg>(); arg->SetPathAndFileName(request_info->local_uri); // path and file name arg->SetQuery(request_info->query_string); // query arguments arg->SetWSId(TString::Hash((void *)&conn, sizeof(void *))); arg->SetMethod("WS_CONNECT"); Bool_t execres = serv->ExecuteWS(arg, kTRUE, kTRUE); return execres && !arg->Is404() ? 0 : 1; } ////////////////////////////////////////////////////////////////////////// void websocket_ready_handler(struct mg_connection *conn, void *) { const struct mg_request_info *request_info = mg_get_request_info(conn); TCivetweb *engine = (TCivetweb *)request_info->user_data; if (!engine || engine->IsTerminating()) return; THttpServer *serv = engine->GetServer(); if (!serv) return; auto arg = std::make_shared<THttpCallArg>(); arg->SetPathAndFileName(request_info->local_uri); // path and file name arg->SetQuery(request_info->query_string); // query arguments arg->SetMethod("WS_READY"); // delegate ownership to the arg, id will be automatically set arg->CreateWSEngine<TCivetwebWSEngine>(conn); serv->ExecuteWS(arg, kTRUE, kTRUE); } ////////////////////////////////////////////////////////////////////////// int websocket_data_handler(struct mg_connection *conn, int code, char *data, size_t len, void *) { const struct mg_request_info *request_info = mg_get_request_info(conn); // do not handle empty data if (len == 0) return 1; TCivetweb *engine = (TCivetweb *)request_info->user_data; if (!engine || engine->IsTerminating()) return 1; THttpServer *serv = engine->GetServer(); if (!serv) return 1; std::string *conn_data = (std::string *) mg_get_user_connection_data(conn); // this is continuation of the request if (!(code & 0x80)) { if (!conn_data) { conn_data = new std::string(data,len); mg_set_user_connection_data(conn, conn_data); } else { conn_data->append(data,len); } return 1; } auto arg = std::make_shared<THttpCallArg>(); arg->SetPathAndFileName(request_info->local_uri); // path and file name arg->SetQuery(request_info->query_string); // query arguments arg->SetWSId(TString::Hash((void *)&conn, sizeof(void *))); arg->SetMethod("WS_DATA"); if (conn_data) { mg_set_user_connection_data(conn, nullptr); conn_data->append(data,len); arg->SetPostData(std::move(*conn_data)); delete conn_data; } else { arg->SetPostData(std::string(data,len)); } serv->ExecuteWS(arg, kTRUE, kTRUE); return 1; } ////////////////////////////////////////////////////////////////////////// void websocket_close_handler(const struct mg_connection *conn, void *) { const struct mg_request_info *request_info = mg_get_request_info(conn); TCivetweb *engine = (TCivetweb *)request_info->user_data; if (!engine || engine->IsTerminating()) return; THttpServer *serv = engine->GetServer(); if (!serv) return; auto arg = std::make_shared<THttpCallArg>(); arg->SetPathAndFileName(request_info->local_uri); // path and file name arg->SetQuery(request_info->query_string); // query arguments arg->SetWSId(TString::Hash((void *)&conn, sizeof(void *))); arg->SetMethod("WS_CLOSE"); serv->ExecuteWS(arg, kTRUE, kFALSE); // do not wait for result of execution } ////////////////////////////////////////////////////////////////////////// static int log_message_handler(const struct mg_connection *conn, const char *message) { const struct mg_context *ctx = mg_get_context(conn); TCivetweb *engine = (TCivetweb *)mg_get_user_data(ctx); if (engine) return engine->ProcessLog(message); // provide debug output if ((gDebug > 0) || (strstr(message, "cannot bind to") != 0)) fprintf(stderr, "Error in <TCivetweb::Log> %s\n", message); return 0; } ////////////////////////////////////////////////////////////////////////// static int begin_request_handler(struct mg_connection *conn, void *) { const struct mg_request_info *request_info = mg_get_request_info(conn); TCivetweb *engine = (TCivetweb *)request_info->user_data; if (!engine || engine->IsTerminating()) return 0; THttpServer *serv = engine->GetServer(); if (!serv) return 0; auto arg = std::make_shared<THttpCallArg>(); TString filename; Bool_t execres = kTRUE, debug = engine->IsDebugMode(); if (!debug && serv->IsFileRequested(request_info->local_uri, filename)) { if ((filename.Length() > 3) && ((filename.Index(".js") != kNPOS) || (filename.Index(".css") != kNPOS))) { std::string buf = THttpServer::ReadFileContent(filename.Data()); if (buf.empty()) { arg->Set404(); } else { arg->SetContentType(THttpServer::GetMimeType(filename.Data())); arg->SetContent(std::move(buf)); if (engine->GetMaxAge() > 0) arg->AddHeader("Cache-Control", TString::Format("max-age=%d", engine->GetMaxAge())); else arg->AddNoCacheHeader(); arg->SetZipping(); } } else { arg->SetFile(filename.Data()); } } else { arg->SetPathAndFileName(request_info->local_uri); // path and file name arg->SetQuery(request_info->query_string); // query arguments arg->SetTopName(engine->GetTopName()); arg->SetMethod(request_info->request_method); // method like GET or POST if (request_info->remote_user) arg->SetUserName(request_info->remote_user); TString header; for (int n = 0; n < request_info->num_headers; n++) header.Append( TString::Format("%s: %s\r\n", request_info->http_headers[n].name, request_info->http_headers[n].value)); arg->SetRequestHeader(header); const char *len = mg_get_header(conn, "Content-Length"); Int_t ilen = len ? TString(len).Atoi() : 0; if (ilen > 0) { std::string buf; buf.resize(ilen); Int_t iread = mg_read(conn, (void *) buf.data(), ilen); if (iread == ilen) arg->SetPostData(std::move(buf)); } if (debug) { TString cont; cont.Append("<title>Civetweb echo</title>"); cont.Append("<h1>Civetweb echo</h1>\n"); static int count = 0; cont.Append(TString::Format("Request %d:<br/>\n<pre>\n", ++count)); cont.Append(TString::Format(" Method : %s\n", arg->GetMethod())); cont.Append(TString::Format(" PathName : %s\n", arg->GetPathName())); cont.Append(TString::Format(" FileName : %s\n", arg->GetFileName())); cont.Append(TString::Format(" Query : %s\n", arg->GetQuery())); cont.Append(TString::Format(" PostData : %ld\n", arg->GetPostDataLength())); if (arg->GetUserName()) cont.Append(TString::Format(" User : %s\n", arg->GetUserName())); cont.Append("</pre><p>\n"); cont.Append("Environment:<br/>\n<pre>\n"); for (int n = 0; n < request_info->num_headers; n++) cont.Append( TString::Format(" %s = %s\n", request_info->http_headers[n].name, request_info->http_headers[n].value)); cont.Append("</pre><p>\n"); arg->SetContentType("text/html"); arg->SetContent(cont); } else { execres = serv->ExecuteHttp(arg); } } if (!execres || arg->Is404()) { std::string hdr = arg->FillHttpHeader("HTTP/1.1"); mg_printf(conn, "%s", hdr.c_str()); } else if (arg->IsFile()) { filename = (const char *)arg->GetContent(); #ifdef _MSC_VER // resolve Windows links which are not supported by civetweb const int BUFSIZE = 2048; TCHAR Path[BUFSIZE]; auto hFile = CreateFile(filename.Data(), // file to open GENERIC_READ, // open for reading FILE_SHARE_READ, // share for reading NULL, // default security OPEN_EXISTING, // existing file only FILE_ATTRIBUTE_NORMAL, // normal file NULL); // no attr. template if( hFile != INVALID_HANDLE_VALUE) { auto dwRet = GetFinalPathNameByHandle( hFile, Path, BUFSIZE, VOLUME_NAME_DOS ); // produced file name may include \\? symbols, which are indicating long file name if(dwRet < BUFSIZE) filename = Path; CloseHandle(hFile); } #endif mg_send_file(conn, filename.Data()); } else { Bool_t dozip = kFALSE; switch (arg->GetZipping()) { case THttpCallArg::kNoZip: dozip = kFALSE; break; case THttpCallArg::kZipLarge: if (arg->GetContentLength() < 10000) break; case THttpCallArg::kZip: // check if request header has Accept-Encoding for (int n = 0; n < request_info->num_headers; n++) { TString name = request_info->http_headers[n].name; if (name.Index("Accept-Encoding", 0, TString::kIgnoreCase) != 0) continue; TString value = request_info->http_headers[n].value; dozip = (value.Index("gzip", 0, TString::kIgnoreCase) != kNPOS); break; } break; case THttpCallArg::kZipAlways: dozip = kTRUE; break; } if (dozip) arg->CompressWithGzip(); std::string hdr = arg->FillHttpHeader("HTTP/1.1"); mg_printf(conn, "%s", hdr.c_str()); if (arg->GetContentLength() > 0) mg_write(conn, arg->GetContent(), (size_t)arg->GetContentLength()); } // Returning non-zero tells civetweb that our function has replied to // the client, and civetweb should not send client any more data. return 1; } ////////////////////////////////////////////////////////////////////////// // // // TCivetweb // // // // http server implementation, based on civetweb embedded server // // It is default kind of engine, created for THttpServer // // Currently v1.8 from https://github.com/civetweb/civetweb is used // // // // Following additional options can be specified: // // top=foldername - name of top folder, seen in the browser // // thrds=N - use N threads to run civetweb server (default 5) // // auth_file - global authentication file // // auth_domain - domain name, used for authentication // // // // Example: // // new THttpServer("http:8080?top=MyApp&thrds=3"); // // // // Authentication: // // When auth_file and auth_domain parameters are specified, access // // to running http server will be possible only after user // // authentication, using so-call digest method. To generate // // authentication file, htdigest routine should be used: // // // // [shell] htdigest -c .htdigest domain_name user // // // // When creating server, parameters should be: // // // // new THttpServer("http:8080?auth_file=.htdigets&auth_domain=domain_name"); // // // ////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////// /// constructor TCivetweb::TCivetweb(Bool_t only_secured) : THttpEngine("civetweb", "compact embedded http server"), fCtx(nullptr), fCallbacks(nullptr), fTopName(), fDebug(kFALSE), fTerminating(kFALSE), fOnlySecured(only_secured) { } //////////////////////////////////////////////////////////////////////////////// /// destructor TCivetweb::~TCivetweb() { if (fCtx && !fTerminating) mg_stop((struct mg_context *)fCtx); if (fCallbacks) free(fCallbacks); } //////////////////////////////////////////////////////////////////////////////// /// process civetweb log message, can be used to detect critical errors Int_t TCivetweb::ProcessLog(const char *message) { if ((gDebug > 0) || (strstr(message, "cannot bind to") != 0)) Error("Log", "%s", message); return 0; } //////////////////////////////////////////////////////////////////////////////// /// Creates embedded civetweb server /// As main argument, http port should be specified like "8090". /// Or one can provide combination of ipaddress and portnumber like 127.0.0.1:8090 /// Extra parameters like in URL string could be specified after '?' mark: /// thrds=N - there N is number of threads used by the civetweb (default is 10) /// top=name - configure top name, visible in the web browser /// ssl_certificate=filename - SSL certificate, see docs/OpenSSL.md from civetweb /// auth_file=filename - authentication file name, created with htdigets utility /// auth_domain=domain - authentication domain /// websocket_timeout=tm - set web sockets timeout in seconds (default 300) /// websocket_disable - disable web sockets handling (default enabled) /// bind - ip address to bind server socket /// loopback - bind specified port to loopback 127.0.0.1 address /// debug - enable debug mode, server always returns html page with request info /// log=filename - configure civetweb log file /// max_age=value - configures "Cache-Control: max_age=value" http header for all file-related requests, default 3600 /// nocache - try to fully disable cache control for file requests /// Examples: /// http:8080?websocket_disable /// http:7546?thrds=30&websocket_timeout=20 Bool_t TCivetweb::Create(const char *args) { fCallbacks = malloc(sizeof(struct mg_callbacks)); memset(fCallbacks, 0, sizeof(struct mg_callbacks)); //((struct mg_callbacks *) fCallbacks)->begin_request = begin_request_handler; ((struct mg_callbacks *)fCallbacks)->log_message = log_message_handler; TString sport = IsSecured() ? "8480s" : "8080", num_threads = "10", websocket_timeout = "300000"; TString auth_file, auth_domain, log_file, ssl_cert, max_age; Bool_t use_ws = kTRUE; // extract arguments if (args && (strlen(args) > 0)) { // first extract port number sport = ""; while ((*args != 0) && (*args != '?') && (*args != '/')) sport.Append(*args++); if (IsSecured() && (sport.Index("s")==kNPOS)) sport.Append("s"); // than search for extra parameters while ((*args != 0) && (*args != '?')) args++; if (*args == '?') { TUrl url(TString::Format("http://localhost/folder%s", args)); if (url.IsValid()) { url.ParseOptions(); const char *top = url.GetValueFromOptions("top"); if (top) fTopName = top; const char *log = url.GetValueFromOptions("log"); if (log) log_file = log; Int_t thrds = url.GetIntValueFromOptions("thrds"); if (thrds > 0) num_threads.Form("%d", thrds); const char *afile = url.GetValueFromOptions("auth_file"); if (afile) auth_file = afile; const char *adomain = url.GetValueFromOptions("auth_domain"); if (adomain) auth_domain = adomain; const char *sslc = url.GetValueFromOptions("ssl_cert"); if (sslc) ssl_cert = sslc; Int_t wtmout = url.GetIntValueFromOptions("websocket_timeout"); if (wtmout > 0) { websocket_timeout.Format("%d", wtmout * 1000); use_ws = kTRUE; } if (url.HasOption("websocket_disable")) use_ws = kFALSE; if (url.HasOption("debug")) fDebug = kTRUE; if (url.HasOption("loopback") && (sport.Index(":") == kNPOS)) sport = TString("127.0.0.1:") + sport; if (url.HasOption("bind") && (sport.Index(":") == kNPOS)) { const char *addr = url.GetValueFromOptions("bind"); if (addr && strlen(addr)) sport = TString(addr) + ":" + sport; } if (GetServer() && url.HasOption("cors")) { const char *cors = url.GetValueFromOptions("cors"); GetServer()->SetCors(cors && *cors ? cors : "*"); } if (url.HasOption("nocache")) fMaxAge = 0; if (url.HasOption("max_age")) fMaxAge = url.GetIntValueFromOptions("max_age"); max_age.Form("%d", fMaxAge); } } } const char *options[20]; int op(0); Info("Create", "Starting HTTP server on port %s", sport.Data()); options[op++] = "listening_ports"; options[op++] = sport.Data(); options[op++] = "num_threads"; options[op++] = num_threads.Data(); if (use_ws) { options[op++] = "websocket_timeout_ms"; options[op++] = websocket_timeout.Data(); } if ((auth_file.Length() > 0) && (auth_domain.Length() > 0)) { options[op++] = "global_auth_file"; options[op++] = auth_file.Data(); options[op++] = "authentication_domain"; options[op++] = auth_domain.Data(); } if (log_file.Length() > 0) { options[op++] = "error_log_file"; options[op++] = log_file.Data(); } if (ssl_cert.Length() > 0) { options[op++] = "ssl_certificate"; options[op++] = ssl_cert.Data(); } else if (IsSecured()) { Error("Create", "No SSL certificate file configured"); } if (max_age.Length() > 0) { options[op++] = "static_file_max_age"; options[op++] = max_age.Data(); } options[op++] = nullptr; // Start the web server. fCtx = mg_start((struct mg_callbacks *)fCallbacks, this, options); if (!fCtx) return kFALSE; mg_set_request_handler((struct mg_context *)fCtx, "/", begin_request_handler, nullptr); if (use_ws) mg_set_websocket_handler((struct mg_context *)fCtx, "**root.websocket$", websocket_connect_handler, websocket_ready_handler, websocket_data_handler, websocket_close_handler, nullptr); return kTRUE; }