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;
}