// ---------------------------------------------------------------------- // File: HttpServer.cc // Author: Andreas-Joachim Peters & Justin Lewis Salmon - CERN // ---------------------------------------------------------------------- /************************************************************************ * EOS - the CERN Disk Storage System * * Copyright (C) 2011 CERN/Switzerland * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see .* ************************************************************************/ #include "mgm/http/HttpServer.hh" #include "mgm/http/ProtocolHandlerFactory.hh" #include "mgm/XrdMgmOfs.hh" #include "mgm/Stat.hh" #include "mgm/Macros.hh" #include "common/Path.hh" #include "common/SecEntity.hh" #include "common/StringTokenizer.hh" #include "common/ErrnoToString.hh" #include "XrdNet/XrdNetAddr.hh" #include "XrdAcc/XrdAccAuthorize.hh" #include EOSMGMNAMESPACE_BEGIN #define EOSMGM_HTTP_PAGE "No such file or directory\ No such file or directory" #ifdef EOS_MICRO_HTTPD /*----------------------------------------------------------------------------*/ int HttpServer::Handler(void* cls, struct MHD_Connection* connection, const char* url, const char* method, const char* version, const char* uploadData, size_t* uploadDataSize, void** ptr) { using namespace eos::common; std::map headers; // Wait for the namespace to boot WAIT_BOOT; // If this is the first call, create an appropriate protocol handler based // on the headers and store it in *ptr. We should only return MHD_YES here // (unless error) if (*ptr == 0) { // Get the headers MHD_get_connection_values(connection, MHD_HEADER_KIND, &HttpServer::BuildHeaderMap, (void*) &headers); // Retrieve Client IP const MHD_ConnectionInfo* info = MHD_get_connection_info(connection, MHD_CONNECTION_INFO_CLIENT_ADDRESS); if (info && info->client_addr) { char host[NI_MAXHOST]; if (! getnameinfo(info->client_addr, (info->client_addr->sa_family == AF_INET) ? sizeof(struct sockaddr_in) : sizeof( struct sockaddr_in6), host, NI_MAXHOST, NULL, 0, NI_NUMERICHOST)) { headers["client-real-ip"] = host; } else { headers["client-real-ip"] = "NOIPLOOKUP"; } XrdNetAddr netaddr(info->client_addr); const char* name = netaddr.Name(); if (name) { headers["client-real-host"] = name; } } // Clients which are gateways/sudoer can pass x-forwarded-for and remote-user if (headers.count("x-forwarded-for")) { // Check if this is a http gateway and sudoer by calling the mapping function std::unique_ptr vid_tmp {new VirtualIdentity()}; if (vid_tmp) { XrdSecEntity eclient(headers.count("x-real-ip") ? "https" : "http"); eclient.tident=""; eclient.name=(char*)"nobody"; eclient.host=(char*)(headers["client-real-host"].length()?headers["client-real-host"].c_str():""); if (headers.count("x-gateway-authorization")) { eclient.endorsements = (char*)headers["x-gateway-authorization"].c_str(); } std::string stident = "https.0:0@"; stident += headers["client-real-host"]; eos::common::Mapping::IdMap(&eclient, "", stident.c_str(), *vid_tmp); if (!vid_tmp->isGateway() || ((vid_tmp->prot != "https") && (vid_tmp->prot != "http"))) { headers.erase("x-forwarded-for"); headers.erase("x-real-ip"); } eos_static_debug("vid trace: %s gw:%d", vid_tmp->getTrace().c_str(), vid_tmp->isGateway()); if (headers.count("x-gateway-authorization") && !vid_tmp->sudoer) { headers.erase("remote-user"); } } else { eos_static_err("msg=\"failed to allocate VirtualIdentity object\" " "method=%s", method); return MHD_NO; } } else { headers.erase("x-real-ip"); headers.erase("remote-user"); } // Authenticate the client eos::common::VirtualIdentity* vid = Authenticate(headers); eos_static_info("request=%s client-real-ip=%s client-real-host=%s vid.uid=%s vid.gid=%s vid.host=%s vid.tident=%s\n", method, headers["client-real-ip"].c_str(), headers["client-real-host"].c_str(), vid->uid_string.c_str(), vid->gid_string.c_str(), vid->host.c_str(), vid->tident.c_str()); eos::common::ProtocolHandler* handler; ProtocolHandlerFactory factory = ProtocolHandlerFactory(); handler = factory.CreateProtocolHandler(method, headers, vid); if (!handler) { eos_static_err("msg=\"no matching protocol for request method %s\"", method); return MHD_NO; } *ptr = handler; // PUT has to run through to avoid the generation of 100-CONTINUE before a redirect if (strcmp(method, "PUT")) { return MHD_YES; } } // Retrieve the protocol handler stored in *ptr eos::common::ProtocolHandler* protocolHandler = (eos::common::ProtocolHandler*) * ptr; // For requests which have a body (i.e. uploadDataSize != 0) we must handle // the body data on the last call to this function. We must // create the response and store it inside the protocol handler, but we must // NOT queue the response until the third call. if (!protocolHandler->GetResponse() && (!*uploadDataSize)) { // Get the request headers again MHD_get_connection_values(connection, MHD_HEADER_KIND, &HttpServer::BuildHeaderMap, (void*) &headers); // Get the request query string std::string query; MHD_get_connection_values(connection, MHD_GET_ARGUMENT_KIND, &HttpServer::BuildQueryString, (void*) &query); // Get the cookies std::map cookies; MHD_get_connection_values(connection, MHD_COOKIE_KIND, &HttpServer::BuildHeaderMap, (void*) &cookies); size_t bodySize = protocolHandler->GetBody().size(); // Make a request object eos::common::HttpRequest* request = new eos::common::HttpRequest( headers, method, url, query.c_str() ? query : "", protocolHandler->GetBody(), &bodySize, cookies); eos_static_debug("\n\n%s\n%s\n", request->ToString().c_str(), request->GetBody().c_str()); // Handle the request and build a response based on the specific protocol unless the body is not complete ... protocolHandler->HandleRequest(request); delete request; } // If we have a non-empty body, we must "process" it, set the body size to // zero, and return MHD_YES. We should not queue the response yet - we must // do that on the next (third) call. if (*uploadDataSize != 0) { // we store the partial body into the handler protocolHandler->AddToBody(uploadData, *uploadDataSize); *uploadDataSize = 0; return MHD_YES; } eos::common::HttpResponse* response = protocolHandler->GetResponse(); if (!response) { eos_static_crit("msg=\"response creation failed\""); delete protocolHandler; *ptr = 0; return MHD_NO; } eos_static_debug("\n\n%s", response->ToString().c_str()); // Create the response struct MHD_Response* mhdResponse; mhdResponse = MHD_create_response_from_buffer(response->GetBodySize(), (void*) response->GetBody().c_str(), MHD_RESPMEM_MUST_COPY); if (mhdResponse) { // Add all the response header tags headers = response->GetHeaders(); for (auto it = headers.begin(); it != headers.end(); it++) { MHD_add_response_header(mhdResponse, it->first.c_str(), it->second.c_str()); } // Queue the response int ret = MHD_queue_response(connection, response->GetResponseCode(), mhdResponse); eos_static_debug("msg=\"MHD_queue_response\" retc=%d", ret); MHD_destroy_response(mhdResponse); delete protocolHandler; *ptr = 0; return ret; } else { eos_static_crit("msg=\"response creation failed\""); delete protocolHandler; *ptr = 0; return MHD_NO; } } /*----------------------------------------------------------------------------*/ void HttpServer::CompleteHandler(void* cls, struct MHD_Connection* connection, void** con_cls, enum MHD_RequestTerminationCode toe) { std::string scode = ""; if (toe == MHD_REQUEST_TERMINATED_COMPLETED_OK) { scode = "OK"; } if (toe == MHD_REQUEST_TERMINATED_WITH_ERROR) { scode = "Error"; } if (toe == MHD_REQUEST_TERMINATED_TIMEOUT_REACHED) { scode = "Timeout"; } if (toe == MHD_REQUEST_TERMINATED_DAEMON_SHUTDOWN) { scode = "Shutdown"; } if (toe == MHD_REQUEST_TERMINATED_READ_ERROR) { scode = "ReadError"; } eos_static_info("msg=\"http connection disconnect\" reason=\"Request %s\" ", scode.c_str()); } #endif //------------------------------------------------------------------------------ // Do a "rough" mapping between HTTP verbs and access operation types // // @param http_verb type of HTTP request // // @return XRootD access operation type //------------------------------------------------------------------------------ Access_Operation MapHttpVerbToAOP(const std::string& http_verb) { Access_Operation op = AOP_Stat; if (http_verb == "GET") { op = AOP_Read; } else if (http_verb == "PUT") { op = AOP_Create; } else if (http_verb == "DELETE") { op = AOP_Delete; } return op; } //------------------------------------------------------------------------------ // HTTP object handler function called by XrdHttp //------------------------------------------------------------------------------ std::unique_ptr HttpServer::XrdHttpHandler(std::string& method, std::string& uri, std::map& headers, std::map& cookies, std::string& body, const XrdSecEntity& client, XrdAccAuthorize* authz_obj, std::string& err_msg) { using namespace eos::common; WAIT_BOOT; // Clients which are gateways/sudoer can pass x-forwarded-for and remote-user if (headers.count("x-forwarded-for")) { // Check if this is a http gateway and sudoer by calling the mapping function std::unique_ptr vid_tmp {new VirtualIdentity()}; if (vid_tmp) { XrdSecEntity eclient(client.prot); // Save initial eaAPI pointer and reset after the copy to avoid // double free of the same pointer. auto ea = eclient.eaAPI; eclient = client; eclient.eaAPI = ea; if (headers.count("x-gateway-authorization")) { eclient.endorsements = (char*)headers["x-gateway-authorization"].c_str(); } std::string stident = "https.0:0@"; stident += std::string(client.host); eos::common::Mapping::IdMap(&eclient, "", stident.c_str(), *vid_tmp); if (!vid_tmp->isGateway() || ((vid_tmp->prot != "https") && (vid_tmp->prot != "http"))) { headers.erase("x-forwarded-for"); headers.erase("x-real-ip"); } eos_static_debug("vid trace: %s gw:%d", vid_tmp->getTrace().c_str(), vid_tmp->isGateway()); if (headers.count("x-gateway-authorization") && !vid_tmp->sudoer) { headers.erase("remote-user"); } } else { err_msg = "failed to allocate memory"; eos_static_err("msg=\"failed to allocate VirtualIdentity object\" " "method=%s uri=\"%s\"", method.c_str(), uri.c_str()); return nullptr; } } bool s3_access = false; auto it_authz = headers.find("authorization"); if (it_authz != headers.end()) { if (it_authz->second.substr(0, 3) == "AWS") { s3_access = true; } } std::string query; //@todo(esindril) decide if this is needed VirtualIdentity* vid = new VirtualIdentity(); // Native XrdHttp access if (headers.find("x-forwarded-for") == headers.end() && !s3_access) { std::string path; std::unique_ptr env_opaque; if (!BuildPathAndEnvOpaque(headers, path, env_opaque)) { err_msg = "conflicting authorization info present"; eos_static_err("msg=\"%s\" path=\"%s\"", err_msg.c_str(), path.c_str()); return nullptr; } int envlen; char* ptr = env_opaque->Env(envlen); if (ptr == nullptr) { err_msg = "empty opaque info for request"; eos_static_err("msg=\"%s\" path=\"%s\"", err_msg.c_str(), path.c_str()); return nullptr; } // Get access operation type for the authz library Access_Operation acc_op = MapHttpVerbToAOP(method); const std::string env = ptr; query = env; EXEC_TIMING_BEGIN("IdMap"); Mapping::IdMap(&client, env.c_str(), client.tident, *vid, authz_obj, acc_op, path); EXEC_TIMING_END("IdMap"); } else { // HTTP access through Nginx headers["client-real-ip"] = "NOIPLOOKUP"; headers["client-real-host"] = client.host; headers["x-real-ip"] = client.host; if (client.moninfo && strlen(client.moninfo)) { headers["ssl_client_s_dn"] = client.moninfo; } vid = Authenticate(headers); } // Update the vid.name as the mapping might have changed the vid.uid and it // is the name that is used later on for all the authorization bits int errc = 0; std::string usr_name = eos::common::Mapping::UidToUserName(vid->uid, errc); vid->name = (errc ? std::to_string(vid->uid).c_str() : usr_name.c_str()); eos_static_info("request=%s client-real-ip=%s client-real-host=%s " "vid.name=%s vid.uid=%s vid.gid=%s vid.host=%s " "vid.dn=%s vid.tident=%s", method.c_str(), headers["client-real-ip"].c_str(), headers["client-real-host"].c_str(), vid->name.c_str(), vid->uid_string.c_str(), vid->gid_string.c_str(), vid->host.c_str(), vid->dn.c_str(), vid->tident.c_str()); ProtocolHandlerFactory factory = ProtocolHandlerFactory(); std::unique_ptr handler {factory.CreateProtocolHandler(method, headers, vid)}; if (!handler) { eos_static_err("msg=\"no matching protocol for request method %s\"", method.c_str()); return nullptr; } size_t bodySize = body.length(); // Retrieve the protocol handler stored in *ptr std::unique_ptr request { new eos::common::HttpRequest(headers, method, uri, (query.c_str() ? query : ""), body, &bodySize, cookies)}; eos_static_debug("\n\n%s\n%s\n", request->ToString().c_str(), request->GetBody().c_str()); handler->HandleRequest(request.get()); eos_static_debug("method=%s uri=\"%s\"client=\"%s\" msg=\"warning this is " "not the mapped identity\"", method.c_str(), uri.c_str(), eos::common::SecEntity::ToString(&client, "xrdhttp").c_str()); return handler; } //------------------------------------------------------------------------------ // Build path and opaque information based on the HTTP headers //------------------------------------------------------------------------------ bool HttpServer::BuildPathAndEnvOpaque (const std::map& normalized_headers, std::string& path, std::unique_ptr& env_opaque) { using eos::common::StringConversion; // Extract path and any opaque info that might be present in the headers // /path/to/file?and=some&opaque=info path.clear(); auto it = normalized_headers.find("xrd-http-fullresource"); if (it == normalized_headers.end()) { eos_static_err("%s", "msg=\"no xrd-http-fullresource header\""); return false; } path = it->second; std::string opaque; size_t pos = path.find('?'); if ((pos != std::string::npos) && (pos != path.length())) { opaque = path.substr(pos + 1); path = path.substr(0, pos); eos::common::Path canonical_path(path); path = canonical_path.GetFullPath().c_str(); } // Check if there is an explicit authorization header std::string http_authz; it = normalized_headers.find("authorization"); if (it != normalized_headers.end()) { http_authz = it->second; } // If opaque data aleady contains authorization info i.e. "&authz=..." and we also // have a HTTP authorization header then we fail bool has_opaque_authz = (opaque.find("authz=") != std::string::npos); if (has_opaque_authz && !http_authz.empty()) { eos_static_err("msg=\"request has both opaque and http authorization\" " "opaque=\"%s\" http_authz=\"%s\"", opaque.c_str(), http_authz.c_str()); return false; } if (!http_authz.empty()) { std::string enc_authz = StringConversion::curl_default_escaped(http_authz); opaque += "&authz="; opaque += enc_authz; } it = normalized_headers.find("xrd-http-query"); if (it != normalized_headers.end()) { std::string query = it->second; if (!query.empty()) { if (*query.begin() != '&') { opaque += "&"; } opaque += query; } } // Append eos.app tag if none is already present if (opaque.find("eos.app=") == std::string::npos) { opaque += "&eos.app=http"; } env_opaque = std::make_unique(opaque.c_str(), opaque.length()); return true; } //------------------------------------------------------------------------------ // Handle clientDN specified using RFC2253 (and RFC4514) where the // separator is "," instead of the usual "/" and also the order of the DNs // is reversed //------------------------------------------------------------------------------ std::string HttpServer::ProcessClientDN(const std::string& cdn) const { std::string new_cdn = cdn; if (new_cdn.empty()) { return new_cdn; } if (new_cdn.find(',') != std::string::npos) { // clientDN specified using RFC2253 (and RFC4514) where the separator is // "," instead of the usual "/" and DNs reversed std::replace(new_cdn.begin(), new_cdn.end(), ',', '/'); // Reverse the DN tokens auto tokens = eos::common::StringTokenizer::split >(new_cdn, '/'); new_cdn.clear(); for (auto token = tokens.rbegin(); token != tokens.rend(); ++token) { new_cdn += '/'; new_cdn += *token; } } return new_cdn; } /*----------------------------------------------------------------------------*/ eos::common::VirtualIdentity* HttpServer::Authenticate(std::map& headers) { eos::common::VirtualIdentity* vid = 0; std::string clientDN = headers["ssl_client_s_dn"]; std::string remoteUser = headers["remote-user"]; std::string dn; std::string username; unsigned pos; if (clientDN.empty() && remoteUser.empty()) { eos_static_debug("msg=\"client supplied neither SSL_CLIENT_S_DN nor " "Remote-User headers\""); } else { if (clientDN.length()) { clientDN = ProcessClientDN(clientDN); // Stat the gridmap file struct stat info; if (stat("/etc/grid-security/grid-mapfile", &info) == -1) { eos_static_warning("msg=\"error stating gridmap file: %s\"", eos::common::ErrnoToString(errno).c_str()); username = ""; } else { { static XrdSysMutex mGridMapMutex; XrdSysMutexHelper gLock(mGridMapMutex); // Initially load the file, or reload it if it was modified if (!mGridMapFileLastModTime.tv_sec || mGridMapFileLastModTime.tv_sec != info.st_mtim.tv_sec) { eos_static_info("msg=\"reloading gridmap file\""); std::ifstream in("/etc/grid-security/grid-mapfile"); std::stringstream buffer; buffer << in.rdbuf(); mGridMapFile = buffer.str(); mGridMapFileLastModTime = info.st_mtim; in.close(); } } // For proxy certificates clientDN can have multiple ../CN=... appended size_t pos = 0; int num_cns = 0; while ((pos = clientDN.find("/CN=", pos)) != std::string::npos) { ++num_cns; ++pos; } // Remove the CNs from the end one by one to check if the remaining // DN is in the map std::set proxy_dns; std::string clientDNproxy = clientDN; while (num_cns >= 2) { clientDNproxy.erase(clientDNproxy.rfind("/CN=")); proxy_dns.insert(clientDNproxy); --num_cns; } // Process each mapping std::vector mappings; eos::common::StringConversion::Tokenize(mGridMapFile, mappings, "\n"); for (auto it = mappings.begin(); it != mappings.end(); ++it) { eos_static_debug("grid mapping: %s", (*it).c_str()); // Split off the last whitespace-separated token (i.e. username) pos = (*it).find_last_of(" \t"); if (pos == string::npos) { eos_static_err("msg=malformed gridmap file"); return nullptr; } dn = (*it).substr(1, pos - 2); // Remove quotes around DN username = (*it).substr(pos + 1); // Try to match with SSL header if (dn == clientDN) { eos_static_info("msg=\"mapped client certificate successfully\" " "dn=\"%s\" username=\"%s\"", dn.c_str(), username.c_str()); break; } // Check if any of the proxy dns matches if (proxy_dns.find(dn) != proxy_dns.end()) { eos_static_info("msg=\"mapped client proxy certificate successfully\" " "dn=\"%s\"username=\"%s\"", dn.c_str(), username.c_str()); break; } username = ""; } } } else { if (remoteUser.length()) { // extract kerberos username pos = remoteUser.find_last_of("@"); std::string remoteUserName = remoteUser.substr(0, pos); username = remoteUserName; eos_static_info("msg=\"mapped client remote username successfully\" " "username=\"%s\"", username.c_str()); } } } if (username.empty()) { eos_static_info("msg=\"unauthenticated client mapped to nobody" "\" SSL_CLIENT_S_DN=\"%s\", Remote-User=\"%s\"", clientDN.c_str(), remoteUser.c_str()); username = "nobody"; } XrdSecEntity client(headers.count("x-real-ip") ? "https" : "http"); std::string remotehost; if (headers.count("x-real-ip")) { // Translate a proxied host name std::string real_ip = headers["x-real-ip"]; if (real_ip.empty()) { eos_static_err("msg=\"x-real-ip header is empty\""); return nullptr; } // XrdNetAddr deals properly with IPv6 addresses only if they use the // bracket format [ipv6_addr][:] if (real_ip.find('.') == std::string::npos) { // We can safely assume this is an IPv6 address now if (real_ip[0] != '[') { std::ostringstream oss; oss << '[' << real_ip << ']'; real_ip = oss.str(); } } remotehost = real_ip; XrdNetAddr netaddr; netaddr.Set(real_ip.c_str()); // Try to convert IP to corresponding [host] name const char* name = netaddr.Name(); if (name) { remotehost = name; } if (headers.count("auth-type")) { remotehost += "=>"; remotehost += headers["auth-type"]; } } client.host = const_cast(remotehost.c_str()); XrdOucString tident = username.c_str(); tident += ".1:1@"; tident += const_cast(headers["client-real-host"].c_str()); client.name = const_cast(username.c_str()); client.tident = const_cast(tident.c_str()); { // Make a virtual identity object vid = new eos::common::VirtualIdentity(); EXEC_TIMING_BEGIN("IdMap"); eos::common::Mapping::IdMap(&client, "eos.app=http", client.tident, *vid); EXEC_TIMING_END("IdMap"); std::string header_host = headers["host"]; size_t pos = header_host.find(':'); // remove the port if present if (pos != std::string::npos) { header_host.erase(pos); } eos_static_debug("msg=\"connection/header\" header-host=\"%s\" " "connection-host=\"%s\" real-ip=%s", header_host.c_str(), headers["client-real-host"].c_str(), headers["client-real-ip"].c_str()); // if we have been mapped to nobody, change also the name accordingly if (vid->uid == 99) { vid->name = const_cast("nobody"); } vid->dn = dn; vid->tident = tident.c_str(); } return vid; } EOSMGMNAMESPACE_END