sq_callback_result_t Webserver::BeginRequestCallback()

in be/src/util/webserver.cc [656:990]


sq_callback_result_t Webserver::BeginRequestCallback(struct sq_connection* connection,
    struct sq_request_info* request_info) {
  if (VLOG_IS_ON(4)) {
    VLOG(4) << request_info->request_method << " " << request_info->uri << " "
            << request_info->http_version;
    for (int i = 0; i < request_info->num_headers; ++i) {
      VLOG(4) << "  " << request_info->http_headers[i].name << ": "
              << request_info->http_headers[i].value;
    }
  }

  if (strncmp("OPTIONS", request_info->request_method, 7) == 0) {
    // Let Squeasel deal with the request. OPTIONS requests should not require
    // authentication, so do this before doing SPNEGO.
    return SQ_CONTINUE_HANDLING;
  }

  vector<string> response_headers;
  bool authenticated = false;
  // Random value from cookie that we'll also use as a csrf_token to implement the
  // "Double Submit Cookie" and custom header (X-Requested-By) patterns for preventing
  // cross-site request forgery (CSRF).
  std::string cookie_rand_value;
  // With JWTs we can skip CSRF protection because browsers won't send "Authorization:
  // Bearer" headers automatically.
  bool check_csrf_protection = true;
  // Flags if we have a valid cookie to test for CSRF.
  bool cookie_authenticated = false;

  // Try authenticating with JWT token first, if enabled.
  if (use_jwt_ || use_oauth_) {
    const char* auth_value = nullptr;
    const char* value = sq_get_header(connection, "Authorization");
    if (value != nullptr) auth_value = StripLeadingWhiteSpace(value);
    // Check Authorization header with the Bearer authentication scheme as:
    // Authorization: Bearer <token>
    // A well-formed JWT consists of three concatenated Base64url-encoded strings,
    // separated by dots (.).
    if (auth_value != nullptr && strncasecmp(auth_value, "Bearer ", 7) == 0
        && strchr(auth_value, '.') != nullptr) {
      string bearer_token= string(auth_value + 7);
      StripWhiteSpace(&bearer_token);
      if (!bearer_token.empty()) {
        if (use_jwt_) {
          if (JWTTokenAuth(bearer_token, connection, request_info)) {
            total_jwt_token_auth_success_->Increment(1);
            authenticated = true;
            check_csrf_protection = false;
            // TODO: cookies are not added, but are not needed right now
          }
        }
        if (!authenticated && use_oauth_) {
          if (OAuthTokenAuth(bearer_token, connection, request_info)) {
            total_oauth_token_auth_success_->Increment(1);
            authenticated = true;
            check_csrf_protection = false;
            // TODO: cookies are not added, but are not needed right now
          }
        }
        if (!authenticated) {
          if (use_jwt_) {
            LOG(INFO) << "Invalid JWT token provided";
            total_jwt_token_auth_failure_->Increment(1);
          }
          if (use_oauth_) {
            LOG(INFO) << "Invalid OAuth token provided";
            total_oauth_token_auth_failure_->Increment(1);
          }
        }
      }
    }
  }

  if (!authenticated && auth_mode_ == AuthMode::NONE) {
    // With AuthMode::NONE, any protection can be bypassed. We sometimes initialize a 2nd
    // Metrics webserver using AuthMode::NONE, and metrics counters are not named
    // uniquely to work with two webservers using cookies so we skip using cookies.
    authenticated = true;
    check_csrf_protection = false;
  }

  // Try authenticating with a cookie, if enabled.
  if (!authenticated && use_cookies_) {
    const char* cookie_header = sq_get_header(connection, "Cookie");
    string username;
    if (cookie_header != nullptr) {
      Status cookie_status =
          AuthenticateCookie(hash_, cookie_header, &username, &cookie_rand_value);
      if (cookie_status.ok()) {
        authenticated = true;
        cookie_authenticated = true;
        request_info->remote_user = strdup(username.c_str());
        total_cookie_auth_success_->Increment(1);
      } else {
        LOG(INFO) << "Invalid cookie provided: " << cookie_header << ": "
                  << cookie_status.GetDetail();
        response_headers.push_back(Substitute("Set-Cookie: $0", GetDeleteCookie()));
        total_cookie_auth_failure_->Increment(1);
      }
    }
  }

  if (!authenticated && auth_mode_ == AuthMode::HTPASSWD) {
    // Squeasel already handled HTPASSWD authentication. We still enable CSRF protection
    // as browsers automatically include HTPASSWD credentials in requests, so add and use
    // cookies to avoid requiring the custom header.
    authenticated = true;
    AddCookie(request_info->remote_user, &response_headers, &cookie_rand_value);
  }

  // Connections originating from trusted domains should not require authentication.
  // Returns a cookie on the first successful auth attempt. This check is performed after
  // checking for cookie to avoid subsequent reverse DNS lookups which can be
  // unpredictably costly.
  if (!authenticated && check_trusted_domain_) {
    const char* xff_origin = sq_get_header(connection, "X-Forwarded-For");
    std::string_view xff_origin_sv = !xff_origin ? "" : xff_origin;
    string origin;
    if (FLAGS_trusted_domain_use_xff_header) {
      Status status = GetXFFOriginClientAddress(xff_origin_sv, origin);
      if (!status.ok()) LOG(WARNING) << status.GetDetail();
    } else {
      origin = GetRemoteAddress(request_info).ToString();
    }

    StripWhiteSpace(&origin);
    if (origin.empty() && FLAGS_trusted_domain_use_xff_header &&
        FLAGS_trusted_domain_empty_xff_header_use_origin) {
      origin = GetRemoteAddress(request_info).ToString();
      StripWhiteSpace(&origin);
    }
    if (!origin.empty()) {
      if (TrustedDomainCheck(origin, connection, request_info)) {
        total_trusted_domain_check_success_->Increment(1);
        authenticated = true;
        AddCookie(request_info->remote_user, &response_headers, &cookie_rand_value);
      }
    }
  }

  if (!authenticated && check_trusted_auth_header_) {
    const char* auth_header =
        sq_get_header(connection, FLAGS_trusted_auth_header.c_str());
    if (auth_header != nullptr) {
      string err_msg;
      if (GetUsernameFromAuthHeader(connection, request_info, err_msg)) {
        total_trusted_auth_header_check_success_->Increment(1);
        authenticated = true;
        AddCookie(request_info->remote_user, &response_headers, &cookie_rand_value);
      } else {
        LOG(ERROR) << "Found trusted auth header but " << err_msg;
      }
    }
  }

  if (!authenticated) {
    if (auth_mode_ == AuthMode::SPNEGO) {
      sq_callback_result_t spnego_result =
          HandleSpnego(connection, request_info, &response_headers);
      if (spnego_result == SQ_CONTINUE_HANDLING) {
        // Spnego negotiation was successful.
        AddCookie(request_info->remote_user, &response_headers, &cookie_rand_value);
      } else {
        // Spnego negotiation is incomplete or failed, stop processing the request.
        return spnego_result;
      }
    } else {
      DCHECK(auth_mode_ == AuthMode::LDAP);
      Status basic_status = HandleBasic(connection, request_info, &response_headers);
      if (basic_status.ok()) {
        // Basic auth was successful.
        total_basic_auth_success_->Increment(1);
        AddCookie(request_info->remote_user, &response_headers, &cookie_rand_value);
      } else {
        total_basic_auth_failure_->Increment(1);
        if (!sq_get_header(connection, "Authorization")) {
          // This case is expected, as some clients will always initially try to connect
          // without an 'Authorization' and only provide one after getting the
          // 'WWW-Authenticate' back, so we don't log it as an error.
          VLOG(2) << "Not authenticated: no Authorization header provided.";
        } else {
          LOG(ERROR) << "Failed to authenticate: " << basic_status.GetDetail();
        }
        response_headers.push_back("WWW-Authenticate: Basic");
        SendResponse(connection, "401 Authentication Required", "text/plain",
            "Must authenticate with Basic authentication.", response_headers);
        return SQ_HANDLED_OK;
      }
    }
  }

  if (!FLAGS_webserver_doc_root.empty() && FLAGS_enable_webserver_doc_root) {
    if (strncmp(DOC_FOLDER, request_info->uri, DOC_FOLDER_LEN) == 0) {
      VLOG(2) << "HTTP File access: " << request_info->uri;
      // Let Squeasel deal with this request; returning NULL will fall through
      // to the default handler which will serve files.
      return SQ_CONTINUE_HANDLING;
    }
  }

  WebRequest req;
  req.source_socket = GetRemoteAddress(request_info).ToString();
  if (request_info->query_string != nullptr) {
    req.query_string = request_info->query_string;
    BuildArgumentMap(request_info->query_string, &req.parsed_args);
  }
  if (request_info->remote_user != nullptr) {
    req.source_user = request_info->remote_user;
  }

  HttpStatusCode response = HttpStatusCode::Ok;
  ContentType content_type = HTML;
  const UrlHandler* url_handler = nullptr;
  {
    shared_lock<shared_mutex> lock(url_handlers_lock_);
    UrlHandlerMap::const_iterator it = url_handlers_.find(request_info->uri);
    if (it == url_handlers_.end()) {
      response = HttpStatusCode::NotFound;
      req.parsed_args[ERROR_KEY] = Substitute("No URI handler for '$0'",
          request_info->uri);
      url_handler = &error_handler_;
    } else {
      url_handler = &it->second;
    }
  }

  MonotonicStopWatch sw;
  sw.Start();

  req.request_method = request_info->request_method;
  if (req.request_method == "POST") {
    const char* content_len_str = sq_get_header(connection, "Content-Length");
    int32_t content_len = 0;
    if (content_len_str == nullptr ||
        !safe_strto32(content_len_str, &content_len)) {
      sq_printf(connection,
                "HTTP/1.1 %s\r\n",
                HttpStatusCodeToString(HttpStatusCode::LengthRequired).c_str());
      return SQ_HANDLED_OK;
    }
    if (content_len > FLAGS_webserver_max_post_length_bytes) {
      // TODO: for this and other HTTP requests, we should log the
      // remote IP, etc.
      LOG(WARNING) << "Rejected POST with content length " << content_len;
      sq_printf(connection,
                "HTTP/1.1 %s\r\n",
                HttpStatusCodeToString(HttpStatusCode::RequestEntityTooLarge).c_str());
      return SQ_HANDLED_CLOSE_CONNECTION;
    }

    char buf[8192];
    int rem = content_len;
    while (rem > 0) {
      int n = sq_read(connection, buf, std::min<int>(sizeof(buf), rem));
      if (n <= 0) {
        LOG(WARNING) << "error reading POST data: expected "
                     << content_len << " bytes but only read "
                     << req.post_data.size();
        sq_printf(connection,
                  "HTTP/1.1 %s\r\n",
                  HttpStatusCodeToString(HttpStatusCode::InternalServerError).c_str());
        return SQ_HANDLED_CLOSE_CONNECTION;
      }

      req.post_data.append(buf, n);
      rem -= n;
    }

    if (check_csrf_protection) {
      // Always require 1) a csrf_token and cookie or 2) X-Requested-By header.
      if (cookie_authenticated) {
        std::vector<char> csrf_token(RAND_MAX_LENGTH+1, '\0');
        int csrf_len = sq_get_var(req.post_data.c_str(), req.post_data.size(),
            "csrf_token", csrf_token.data(), csrf_token.size());
        if (csrf_len == -1) {
          LOG(WARNING) << "CSRF protection: rejected POST without CSRF token";
          sq_printf(connection, "HTTP/1.1 403 Forbidden\r\n");
          return SQ_HANDLED_CLOSE_CONNECTION;
        } else if (csrf_len == -2) {
          LOG(WARNING) << "CSRF protection: CSRF token is too long";
          sq_printf(connection, "HTTP/1.1 403 Forbidden\r\n");
          return SQ_HANDLED_CLOSE_CONNECTION;
        }
        DCHECK(csrf_len >= 0 && csrf_len < csrf_token.size());

        // Prevent CSRF for POSTs using the Double Submit Cookie pattern only if cookie
        // authentication succeeded.
        if (cookie_rand_value != csrf_token.data()) {
          LOG(WARNING) << "CSRF protection: rejected POST with token mismatch: "
                      << cookie_rand_value << " != " << csrf_token.data();
          const char* msg = "please refresh the page and try again";
          SendResponse(connection, "403 Forbidden", "text/plain", msg, response_headers);
          return SQ_HANDLED_CLOSE_CONNECTION;
        }
      } else {
        // Require a custom header matching csrf_token in the POST body.
        const char* csrf_header = sq_get_header(connection, "X-Requested-By");
        if (csrf_header == nullptr) {
          const char* msg = "rejected POST missing X-Requested-By header";
          LOG(WARNING) << "CSRF protection: " << msg;
          SendResponse(connection, "403 Forbidden", "text/plain", msg, response_headers);
          return SQ_HANDLED_CLOSE_CONNECTION;
        }
      }
    }
  }

  // The output of this page is accumulated into this stringstream.
  stringstream output;
  if (strncmp("HEAD", request_info->request_method, 4) == 0) {
    // For a HEAD call do not generate the response body.
    VLOG(4) << "Not generating output for HEAD call on " << request_info->uri;
  } else if (!url_handler->use_templates()) {
    content_type = PLAIN;
    url_handler->raw_callback()(req, &output, &response);
  } else {
    RenderUrlWithTemplate(
        connection, req, *url_handler, &output, &content_type, cookie_rand_value);
  }

  uint64_t elapsed_time_ns = sw.ElapsedTime();
  if (elapsed_time_ns > FLAGS_slow_http_response_warning_threshold_ms * 1000 * 1000) {
    LOG(WARNING) << "Rendering page " << request_info->uri << " took "
        << PrettyPrinter::Print(sw.ElapsedTime(), TUnit::TIME_NS)
        << ". User: " << req.source_user << ". Address: " << req.source_socket
        << ". Args: " << req.query_string;
  } else {
    VLOG(3) << "Rendering page " << request_info->uri << " took "
            << PrettyPrinter::Print(sw.ElapsedTime(), TUnit::TIME_NS);
  }
  SendResponse(connection, HttpStatusCodeToString(response),
      Webserver::GetMimeType(content_type), output.str(), response_headers);

  return SQ_HANDLED_OK;
}