fd5d218e8e
Drop the request when it's been sitting on the select queue longer than the virtual host timeout allows for. This is orthogonal to the expiration timeout on the queue itself. This can be useful if we ever allow select queue timeout to be configured to be higher than the virtual host request timeout.
437 linhas
16 KiB
C++
437 linhas
16 KiB
C++
/*
|
|
+----------------------------------------------------------------------+
|
|
| HipHop for PHP |
|
|
+----------------------------------------------------------------------+
|
|
| Copyright (c) 2010-2013 Facebook, Inc. (http://www.facebook.com) |
|
|
+----------------------------------------------------------------------+
|
|
| This source file is subject to version 3.01 of the PHP license, |
|
|
| that is bundled with this package in the file LICENSE, and is |
|
|
| available through the world-wide-web at the following url: |
|
|
| http://www.php.net/license/3_01.txt |
|
|
| If you did not receive a copy of the PHP license and are unable to |
|
|
| obtain it through the world-wide-web, please send a note to |
|
|
| license@php.net so we can mail you a copy immediately. |
|
|
+----------------------------------------------------------------------+
|
|
*/
|
|
|
|
#include "hphp/runtime/base/server/http_request_handler.h"
|
|
#include "hphp/runtime/base/program_functions.h"
|
|
#include "hphp/runtime/base/execution_context.h"
|
|
#include "hphp/runtime/base/runtime_option.h"
|
|
#include "hphp/util/timer.h"
|
|
#include "hphp/runtime/base/server/static_content_cache.h"
|
|
#include "hphp/runtime/base/server/dynamic_content_cache.h"
|
|
#include "hphp/runtime/base/server/server_stats.h"
|
|
#include "hphp/util/network.h"
|
|
#include "hphp/runtime/base/preg.h"
|
|
#include "hphp/runtime/ext/ext_function.h"
|
|
#include "hphp/runtime/base/server/access_log.h"
|
|
#include "hphp/runtime/base/server/source_root_info.h"
|
|
#include "hphp/runtime/base/server/request_uri.h"
|
|
#include "hphp/runtime/base/server/http_protocol.h"
|
|
#include "hphp/runtime/base/time/datetime.h"
|
|
#include "hphp/runtime/debugger/debugger.h"
|
|
#include "hphp/util/alloc.h"
|
|
#include "hphp/util/service_data.h"
|
|
|
|
namespace HPHP {
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
IMPLEMENT_THREAD_LOCAL(AccessLog::ThreadData,
|
|
HttpRequestHandler::s_accessLogThreadData);
|
|
|
|
AccessLog HttpRequestHandler::s_accessLog(
|
|
&(HttpRequestHandler::getAccessLogThreadData));
|
|
|
|
HttpRequestHandler::HttpRequestHandler()
|
|
: m_pathTranslation(true),
|
|
m_requestTimedOutOnQueue(ServiceData::createTimeseries(
|
|
"requests_timed_out_on_queue",
|
|
{ServiceData::StatsType::COUNT})) {
|
|
}
|
|
|
|
void HttpRequestHandler::sendStaticContent(Transport *transport,
|
|
const char *data, int len,
|
|
time_t mtime,
|
|
bool compressed,
|
|
const std::string &cmd,
|
|
const char *ext) {
|
|
assert(ext);
|
|
assert(cmd.rfind('.') != string::npos);
|
|
assert(strcmp(ext, cmd.c_str() + cmd.rfind('.') + 1) == 0);
|
|
|
|
hphp_string_imap<string>::const_iterator iter =
|
|
RuntimeOption::StaticFileExtensions.find(ext);
|
|
if (iter != RuntimeOption::StaticFileExtensions.end()) {
|
|
string val = iter->second;
|
|
const char *valp = val.c_str();
|
|
if (strncmp(valp, "text/", 5) == 0 &&
|
|
(strcmp(valp + 5, "plain") == 0 ||
|
|
strcmp(valp + 5, "html") == 0)) {
|
|
// Apache adds character set for these two types
|
|
val += "; charset=";
|
|
val += RuntimeOption::DefaultCharsetName;
|
|
valp = val.c_str();
|
|
}
|
|
transport->addHeader("Content-Type", valp);
|
|
} else {
|
|
transport->addHeader("Content-Type", "application/octet-stream");
|
|
}
|
|
|
|
time_t base = time(nullptr);
|
|
if (RuntimeOption::ExpiresActive) {
|
|
time_t exp = base + RuntimeOption::ExpiresDefault;
|
|
char age[20];
|
|
snprintf(age, sizeof(age), "max-age=%d", RuntimeOption::ExpiresDefault);
|
|
transport->addHeader("Cache-Control", age);
|
|
transport->addHeader("Expires",
|
|
DateTime(exp, true).toString(DateTime::DateFormat::HttpHeader).c_str());
|
|
}
|
|
|
|
if (mtime) {
|
|
transport->addHeader("Last-Modified",
|
|
DateTime(mtime, true).toString(DateTime::DateFormat::HttpHeader).c_str());
|
|
}
|
|
transport->addHeader("Accept-Ranges", "bytes");
|
|
|
|
for (unsigned int i = 0; i < RuntimeOption::FilesMatches.size(); i++) {
|
|
FilesMatch &rule = *RuntimeOption::FilesMatches[i];
|
|
if (rule.match(cmd)) {
|
|
const vector<string> &headers = rule.getHeaders();
|
|
for (unsigned int j = 0; j < headers.size(); j++) {
|
|
transport->addHeader(String(headers[j]));
|
|
}
|
|
}
|
|
}
|
|
|
|
// misnomer, it means we have made decision on compression, transport
|
|
// should not attempt to compress it.
|
|
transport->disableCompression();
|
|
|
|
transport->sendRaw((void*)data, len, 200, compressed);
|
|
}
|
|
|
|
void HttpRequestHandler::handleRequest(Transport *transport) {
|
|
ExecutionProfiler ep(ThreadInfo::RuntimeFunctions);
|
|
|
|
Logger::OnNewRequest();
|
|
GetAccessLog().onNewRequest();
|
|
transport->enableCompression();
|
|
|
|
ServerStatsHelper ssh("all", ServerStatsHelper::TRACK_MEMORY);
|
|
Logger::Verbose("receiving %s", transport->getCommand().c_str());
|
|
|
|
// will clear all extra logging when this function goes out of scope
|
|
StackTraceNoHeap::ExtraLoggingClearer clearer;
|
|
StackTraceNoHeap::AddExtraLogging("URL", transport->getUrl());
|
|
|
|
// resolve virtual host
|
|
const VirtualHost *vhost = HttpProtocol::GetVirtualHost(transport);
|
|
assert(vhost);
|
|
if (vhost->disabled() ||
|
|
vhost->isBlocking(transport->getCommand(), transport->getRemoteHost())) {
|
|
transport->sendString("Not Found", 404);
|
|
return;
|
|
}
|
|
|
|
// don't serve the request if it's been sitting in queue for longer than our
|
|
// allowed request timeout.
|
|
int requestTimeoutSeconds = (vhost->getRequestTimeoutSeconds() > 0 ?
|
|
vhost->getRequestTimeoutSeconds() :
|
|
RuntimeOption::RequestTimeoutSeconds);
|
|
if (requestTimeoutSeconds > 0) {
|
|
timespec now;
|
|
gettime(CLOCK_MONOTONIC, &now);
|
|
const timespec& queueTime = transport->getQueueTime();
|
|
|
|
if (gettime_diff_us(queueTime, now) > requestTimeoutSeconds * 1000000) {
|
|
transport->sendString("Service Unavailable", 503);
|
|
m_requestTimedOutOnQueue->addValue(1);
|
|
return;
|
|
}
|
|
}
|
|
|
|
ServerStats::StartRequest(transport->getCommand().c_str(),
|
|
transport->getRemoteHost(),
|
|
vhost->getName().c_str());
|
|
|
|
// resolve source root
|
|
string host = transport->getHeader("Host");
|
|
SourceRootInfo sourceRootInfo(host.c_str());
|
|
|
|
if (sourceRootInfo.error()) {
|
|
sourceRootInfo.handleError(transport);
|
|
return;
|
|
}
|
|
|
|
// request URI
|
|
string pathTranslation = m_pathTranslation ?
|
|
vhost->getPathTranslation().c_str() : "";
|
|
RequestURI reqURI(vhost, transport, sourceRootInfo.path(), pathTranslation);
|
|
if (reqURI.done()) {
|
|
return; // already handled with redirection or 404
|
|
}
|
|
string path = reqURI.path().data();
|
|
string absPath = reqURI.absolutePath().data();
|
|
|
|
// determine whether we should compress response
|
|
bool compressed = transport->decideCompression();
|
|
|
|
const char *data; int len;
|
|
const char *ext = reqURI.ext();
|
|
|
|
if (reqURI.forbidden()) {
|
|
transport->sendString("Forbidden", 403);
|
|
return;
|
|
}
|
|
|
|
bool cachableDynamicContent =
|
|
(!RuntimeOption::StaticFileGenerators.empty() &&
|
|
RuntimeOption::StaticFileGenerators.find(path) !=
|
|
RuntimeOption::StaticFileGenerators.end());
|
|
|
|
// If this is not a php file, check the static and dynamic content caches
|
|
if (ext && strcasecmp(ext, "php") != 0) {
|
|
if (RuntimeOption::EnableStaticContentCache) {
|
|
bool original = compressed;
|
|
// check against static content cache
|
|
if (StaticContentCache::TheCache.find(path, data, len, compressed)) {
|
|
Util::ScopedMem decompressed_data;
|
|
// (qigao) not calling stat at this point because the timestamp of
|
|
// local cache file is not valuable, maybe misleading. This way
|
|
// the Last-Modified header will not show in response.
|
|
// stat(RuntimeOption::FileCache.c_str(), &st);
|
|
if (!original && compressed) {
|
|
data = gzdecode(data, len);
|
|
if (data == nullptr) {
|
|
throw FatalErrorException("cannot unzip compressed data");
|
|
}
|
|
decompressed_data = const_cast<char*>(data);
|
|
compressed = false;
|
|
}
|
|
sendStaticContent(transport, data, len, 0, compressed, path, ext);
|
|
StaticContentCache::TheFileCache->adviseOutMemory();
|
|
ServerStats::LogPage(path, 200);
|
|
GetAccessLog().log(transport, vhost);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (RuntimeOption::EnableStaticContentFromDisk) {
|
|
String translated = File::TranslatePath(String(absPath));
|
|
if (!translated.empty()) {
|
|
CstrBuffer sb(translated.data());
|
|
if (sb.valid()) {
|
|
struct stat st;
|
|
st.st_mtime = 0;
|
|
stat(translated.data(), &st);
|
|
sendStaticContent(transport, sb.data(), sb.size(), st.st_mtime,
|
|
false, path, ext);
|
|
ServerStats::LogPage(path, 200);
|
|
GetAccessLog().log(transport, vhost);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// check static contents that were generated by dynamic pages
|
|
if (cachableDynamicContent) {
|
|
// check against dynamic content cache
|
|
assert(transport->getUrl());
|
|
string key = path + transport->getUrl();
|
|
if (DynamicContentCache::TheCache.find(key, data, len, compressed)) {
|
|
sendStaticContent(transport, data, len, 0, compressed, path, ext);
|
|
ServerStats::LogPage(path, 200);
|
|
GetAccessLog().log(transport, vhost);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// proxy any URLs that not specified in ServeURLs
|
|
if (!RuntimeOption::ProxyOrigin.empty() &&
|
|
((RuntimeOption::UseServeURLs &&
|
|
RuntimeOption::ServeURLs.find(path) ==
|
|
RuntimeOption::ServeURLs.end()) ||
|
|
(RuntimeOption::UseProxyURLs &&
|
|
(RuntimeOption::ProxyURLs.find(path) !=
|
|
RuntimeOption::ProxyURLs.end() ||
|
|
MatchAnyPattern(path, RuntimeOption::ProxyPatterns) ||
|
|
(abs(rand()) % 100) < RuntimeOption::ProxyPercentage)))) {
|
|
for (int i = 0; i < RuntimeOption::ProxyRetry; i++) {
|
|
bool force = (i == RuntimeOption::ProxyRetry - 1); // last one
|
|
if (handleProxyRequest(transport, force)) break;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// record request for debugging purpose
|
|
std::string tmpfile = HttpProtocol::RecordRequest(transport);
|
|
|
|
// main body
|
|
hphp_session_init();
|
|
vhost->setRequestTimeoutSeconds();
|
|
|
|
bool ret = false;
|
|
try {
|
|
ret = executePHPRequest(transport, reqURI, sourceRootInfo,
|
|
cachableDynamicContent);
|
|
} catch (const Eval::DebuggerException &e) {
|
|
transport->sendString(e.what(), 200);
|
|
transport->onSendEnd();
|
|
hphp_context_exit(g_context.getNoCheck(), true, true, transport->getUrl());
|
|
} catch (...) {
|
|
Logger::Error("Unhandled exception in HPHP server engine.");
|
|
}
|
|
GetAccessLog().log(transport, vhost);
|
|
/*
|
|
* HPHP logs may need to access data in ServerStats, so we have to
|
|
* clear the hashtable after writing the log entry.
|
|
*/
|
|
ServerStats::Reset();
|
|
hphp_session_exit();
|
|
|
|
HttpProtocol::ClearRecord(ret, tmpfile);
|
|
}
|
|
|
|
bool HttpRequestHandler::executePHPRequest(Transport *transport,
|
|
RequestURI &reqURI,
|
|
SourceRootInfo &sourceRootInfo,
|
|
bool cachableDynamicContent) {
|
|
ExecutionContext *context = hphp_context_init();
|
|
if (RuntimeOption::ImplicitFlush) {
|
|
context->obSetImplicitFlush(true);
|
|
}
|
|
if (RuntimeOption::EnableOutputBuffering) {
|
|
if (RuntimeOption::OutputHandler.empty()) {
|
|
context->obStart();
|
|
} else {
|
|
context->obStart(String(RuntimeOption::OutputHandler));
|
|
}
|
|
}
|
|
context->setTransport(transport);
|
|
|
|
string file = reqURI.absolutePath().c_str();
|
|
{
|
|
ServerStatsHelper ssh("input");
|
|
HttpProtocol::PrepareSystemVariables(transport, reqURI, sourceRootInfo);
|
|
|
|
if (RuntimeOption::EnableDebugger) {
|
|
Eval::DSandboxInfo sInfo = sourceRootInfo.getSandboxInfo();
|
|
Eval::Debugger::RegisterSandbox(sInfo);
|
|
context->setSandboxId(sInfo.id());
|
|
}
|
|
reqURI.clear();
|
|
sourceRootInfo.clear();
|
|
}
|
|
|
|
int code;
|
|
bool ret = true;
|
|
|
|
if (RuntimeOption::EnableDebugger) {
|
|
Eval::Debugger::InterruptRequestStarted(transport->getUrl());
|
|
}
|
|
|
|
bool error = false;
|
|
std::string errorMsg = "Internal Server Error";
|
|
ret = hphp_invoke(context, file, false, Array(), uninit_null(),
|
|
RuntimeOption::RequestInitFunction,
|
|
RuntimeOption::RequestInitDocument,
|
|
error, errorMsg);
|
|
|
|
if (ret) {
|
|
String content = context->obDetachContents();
|
|
if (cachableDynamicContent && !content.empty()) {
|
|
assert(transport->getUrl());
|
|
string key = file + transport->getUrl();
|
|
DynamicContentCache::TheCache.store(key, content.data(),
|
|
content.size());
|
|
}
|
|
transport->sendRaw((void*)content.data(), content.size());
|
|
code = transport->getResponseCode();
|
|
} else if (error) {
|
|
code = 500;
|
|
|
|
string errorPage = context->getErrorPage().data();
|
|
if (errorPage.empty()) {
|
|
errorPage = RuntimeOption::ErrorDocument500;
|
|
}
|
|
if (!errorPage.empty()) {
|
|
context->obProtect(false);
|
|
context->obEndAll();
|
|
context->obStart();
|
|
context->obProtect(true);
|
|
ret = hphp_invoke(context, errorPage, false, Array(), uninit_null(),
|
|
RuntimeOption::RequestInitFunction,
|
|
RuntimeOption::RequestInitDocument,
|
|
error, errorMsg);
|
|
if (ret) {
|
|
String content = context->obDetachContents();
|
|
transport->sendRaw((void*)content.data(), content.size());
|
|
code = transport->getResponseCode();
|
|
} else {
|
|
Logger::Error("Unable to invoke error page %s", errorPage.c_str());
|
|
errorPage.clear(); // so we fall back to 500 return
|
|
}
|
|
}
|
|
if (errorPage.empty()) {
|
|
if (RuntimeOption::ServerErrorMessage) {
|
|
transport->sendString(errorMsg, 500, false, false, "hphp_invoke");
|
|
} else {
|
|
transport->sendString(RuntimeOption::FatalErrorMessage,
|
|
500, false, false, "hphp_invoke");
|
|
}
|
|
}
|
|
} else {
|
|
code = 404;
|
|
transport->sendString("Not Found", 404);
|
|
}
|
|
|
|
if (RuntimeOption::EnableDebugger) {
|
|
Eval::Debugger::InterruptRequestEnded(transport->getUrl());
|
|
}
|
|
|
|
transport->onSendEnd();
|
|
hphp_context_exit(context, true, true, transport->getUrl());
|
|
ServerStats::LogPage(file, code);
|
|
return ret;
|
|
}
|
|
|
|
bool HttpRequestHandler::handleProxyRequest(Transport *transport, bool force) {
|
|
string url = RuntimeOption::ProxyOrigin + transport->getServerObject();
|
|
|
|
int code = 0;
|
|
std::string error;
|
|
StringBuffer response;
|
|
if (!HttpProtocol::ProxyRequest(transport, force, url, code, error,
|
|
response)) {
|
|
return false;
|
|
}
|
|
if (code == 0) {
|
|
transport->sendString(error, 500, false, false, "handleProxyRequest");
|
|
return true;
|
|
}
|
|
|
|
const char* respData = response.data();
|
|
if (!respData) {
|
|
respData = "";
|
|
}
|
|
transport->sendRaw((void*)respData, response.size(), code);
|
|
return true;
|
|
}
|
|
|
|
bool HttpRequestHandler::MatchAnyPattern
|
|
(const std::string &path, const std::vector<std::string> &patterns) {
|
|
String spath(path.c_str(), path.size(), AttachLiteral);
|
|
for (unsigned int i = 0; i < patterns.size(); i++) {
|
|
Variant ret = preg_match(String(patterns[i].c_str(), patterns[i].size(),
|
|
AttachLiteral),
|
|
spath);
|
|
if (ret.toInt64() > 0) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
}
|