2 +----------------------------------------------------------------------+
4 +----------------------------------------------------------------------+
5 | Copyright (c) 2010-present Facebook, Inc. (http://www.facebook.com) |
6 +----------------------------------------------------------------------+
7 | This source file is subject to version 3.01 of the PHP license, |
8 | that is bundled with this package in the file LICENSE, and is |
9 | available through the world-wide-web at the following url: |
10 | http://www.php.net/license/3_01.txt |
11 | If you did not receive a copy of the PHP license and are unable to |
12 | obtain it through the world-wide-web, please send a note to |
13 | license@php.net so we can mail you a copy immediately. |
14 +----------------------------------------------------------------------+
16 #include "hphp/runtime/server/http-request-handler.h"
21 #include "hphp/runtime/base/datetime.h"
22 #include "hphp/runtime/base/execution-context.h"
23 #include "hphp/runtime/base/hhprof.h"
24 #include "hphp/runtime/base/init-fini-node.h"
25 #include "hphp/runtime/base/preg.h"
26 #include "hphp/runtime/base/program-functions.h"
27 #include "hphp/runtime/base/resource-data.h"
28 #include "hphp/runtime/base/runtime-option.h"
29 #include "hphp/runtime/debugger/debugger.h"
30 #include "hphp/runtime/ext/extension-registry.h"
31 #include "hphp/runtime/ext/std/ext_std_function.h"
32 #include "hphp/runtime/ext/xdebug/status.h"
33 #include "hphp/runtime/server/access-log.h"
34 #include "hphp/runtime/server/files-match.h"
35 #include "hphp/runtime/server/http-protocol.h"
36 #include "hphp/runtime/server/request-uri.h"
37 #include "hphp/runtime/server/server-stats.h"
38 #include "hphp/runtime/server/source-root-info.h"
39 #include "hphp/runtime/server/static-content-cache.h"
40 #include "hphp/runtime/vm/debugger-hook.h"
42 #include "hphp/util/alloc.h"
43 #include "hphp/util/hardware-counter.h"
44 #include "hphp/util/lock.h"
45 #include "hphp/util/mutex.h"
46 #include "hphp/util/network.h"
47 #include "hphp/util/service-data.h"
48 #include "hphp/util/stack-trace.h"
49 #include "hphp/util/struct-log.h"
50 #include "hphp/util/timer.h"
57 ///////////////////////////////////////////////////////////////////////////////
59 static ReadWriteMutex s_proxyMutex
;
60 static __thread
unsigned int s_randState
= 0xfaceb00c;
62 static bool matchAnyPattern(const std::string
&path
,
63 const std::vector
<std::string
> &patterns
) {
64 String
spath(path
.c_str(), path
.size(), CopyString
);
65 for (unsigned int i
= 0; i
< patterns
.size(); i
++) {
66 Variant ret
= preg_match(String(patterns
[i
].c_str(), patterns
[i
].size(),
69 if (ret
.toInt64() > 0) return true;
75 * Returns true iff a request to the given path should be delegated to the
78 static bool shouldProxyPath(const std::string
& path
) {
79 ReadLock
lock(s_proxyMutex
);
81 if (RuntimeOption::ProxyOriginRaw
.empty()) return false;
83 if (RuntimeOption::UseServeURLs
&& RuntimeOption::ServeURLs
.count(path
)) {
87 if (RuntimeOption::UseProxyURLs
) {
88 if (RuntimeOption::ProxyURLs
.count(path
)) return true;
89 if (matchAnyPattern(path
, RuntimeOption::ProxyPatterns
)) return true;
92 if (RuntimeOption::ProxyPercentageRaw
> 0) {
93 if ((abs(rand_r(&s_randState
)) % 100) < RuntimeOption::ProxyPercentageRaw
) {
101 static std::string
getProxyPath(const char* origPath
) {
102 ReadLock
lock(s_proxyMutex
);
104 return RuntimeOption::ProxyOriginRaw
+ origPath
;
107 void setProxyOriginPercentage(const std::string
& origin
, int percentage
) {
108 WriteLock
lock(s_proxyMutex
);
110 RuntimeOption::ProxyOriginRaw
= origin
;
111 RuntimeOption::ProxyPercentageRaw
= percentage
;
112 Logger::Warning("Updated proxy origin to `%s' and percentage to %d\n",
113 origin
.c_str(), percentage
);
116 ///////////////////////////////////////////////////////////////////////////////
118 THREAD_LOCAL(AccessLog::ThreadData
, HttpRequestHandler::s_accessLogThreadData
);
120 AccessLog
HttpRequestHandler::s_accessLog(
121 &(HttpRequestHandler::getAccessLogThreadData
));
123 HttpRequestHandler::HttpRequestHandler(int timeout
)
124 : RequestHandler(timeout
), m_pathTranslation(true)
125 , m_requestTimedOutOnQueue(ServiceData::createTimeSeries(
126 "requests_timed_out_on_queue",
127 {ServiceData::StatsType::COUNT
})) { }
129 void HttpRequestHandler::sendStaticContent(Transport
*transport
,
130 const char *data
, int len
,
133 const std::string
&cmd
,
136 assert(cmd
.rfind('.') != std::string::npos
);
137 assert(strcmp(ext
, cmd
.c_str() + cmd
.rfind('.') + 1) == 0);
139 auto iter
= RuntimeOption::StaticFileExtensions
.find(ext
);
140 if (iter
!= RuntimeOption::StaticFileExtensions
.end()) {
141 string val
= iter
->second
;
142 const char *valp
= val
.c_str();
143 if (strncmp(valp
, "text/", 5) == 0 &&
144 (strcmp(valp
+ 5, "plain") == 0 ||
145 strcmp(valp
+ 5, "html") == 0)) {
146 // Apache adds character set for these two types
148 val
+= IniSetting::Get("default_charset");
151 transport
->addHeader("Content-Type", valp
);
153 transport
->addHeader("Content-Type", "application/octet-stream");
156 time_t base
= time(nullptr);
157 if (RuntimeOption::ExpiresActive
) {
158 time_t exp
= base
+ RuntimeOption::ExpiresDefault
;
160 snprintf(age
, sizeof(age
), "max-age=%d", RuntimeOption::ExpiresDefault
);
161 transport
->addHeader("Cache-Control", age
);
162 transport
->addHeader("Expires",
163 req::make
<DateTime
>(exp
, true)->toString(
164 DateTime::DateFormat::HttpHeader
).c_str());
168 transport
->addHeader("Last-Modified",
169 req::make
<DateTime
>(mtime
, true)->toString(
170 DateTime::DateFormat::HttpHeader
).c_str());
172 transport
->addHeader("Accept-Ranges", "bytes");
174 for (unsigned int i
= 0; i
< RuntimeOption::FilesMatches
.size(); i
++) {
175 FilesMatch
&rule
= *RuntimeOption::FilesMatches
[i
];
176 if (rule
.match(cmd
)) {
177 const vector
<string
> &headers
= rule
.getHeaders();
178 for (unsigned int j
= 0; j
< headers
.size(); j
++) {
179 transport
->addHeader(String(headers
[j
]));
184 // misnomer, it means we have made decision on compression, transport
185 // should not attempt to compress it.
186 transport
->disableCompression();
188 transport
->sendRaw((void*)data
, len
, 200, compressed
);
189 transport
->onSendEnd();
192 void HttpRequestHandler::logToAccessLog(Transport
* transport
) {
193 GetAccessLog().onNewRequest();
194 GetAccessLog().log(transport
, VirtualHost::GetCurrent());
197 void HttpRequestHandler::setupRequest(Transport
* transport
) {
198 MemoryManager::requestInit();
199 HHProf::Request::Setup(transport
);
201 g_context
.getCheck();
202 GetAccessLog().onNewRequest();
204 // Set current virtual host.
205 HttpProtocol::GetVirtualHost(transport
);
208 void HttpRequestHandler::teardownRequest(Transport
* transport
) noexcept
{
209 SCOPE_EXIT
{ always_assert(tl_heap
->empty()); };
211 const VirtualHost
*vhost
= VirtualHost::GetCurrent();
212 GetAccessLog().log(transport
, vhost
);
214 // HPHP logs may need to access data in ServerStats, so we have to clear the
215 // hashtable after writing the log entry.
216 ServerStats::Reset();
217 m_sourceRootInfo
.clear();
219 if (is_hphp_session_initialized()) {
220 hphp_session_exit(transport
);
222 // Even though there are no sessions, memory is allocated to perform
223 // INI setting bindings when the thread is initialized.
224 hphp_memory_cleanup();
227 MemoryManager::requestShutdown();
228 HHProf::Request::Teardown();
231 void HttpRequestHandler::handleRequest(Transport
*transport
) {
232 ExecutionProfiler
ep(ThreadInfo::RuntimeFunctions
);
234 Logger::OnNewRequest();
235 transport
->enableCompression();
237 ServerStatsHelper
ssh("all",
238 ServerStatsHelper::TRACK_MEMORY
|
239 ServerStatsHelper::TRACK_HWINST
);
240 Logger::Verbose("receiving %s", transport
->getCommand().c_str());
242 // will clear all extra logging when this function goes out of scope
243 StackTraceNoHeap::ExtraLoggingClearer clearer
;
244 StackTraceNoHeap::AddExtraLogging("URL", transport
->getUrl());
246 // resolve virtual host
247 const VirtualHost
*vhost
= VirtualHost::GetCurrent();
249 if (vhost
->disabled() ||
250 vhost
->isBlocking(transport
->getCommand(), transport
->getRemoteHost())) {
251 transport
->sendString("Not Found", 404);
252 transport
->onSendEnd();
256 // don't serve the request if it's been sitting in queue for longer than our
257 // allowed request timeout.
258 int requestTimeoutSeconds
=
259 vhost
->getRequestTimeoutSeconds(getDefaultTimeout());
260 if (requestTimeoutSeconds
> 0) {
262 Timer::GetMonotonicTime(now
);
263 const timespec
& queueTime
= transport
->getQueueTime();
265 if (gettime_diff_us(queueTime
, now
) > requestTimeoutSeconds
* 1000000) {
266 transport
->sendString("Service Unavailable", 503);
267 transport
->onSendEnd();
268 m_requestTimedOutOnQueue
->addValue(1);
273 ServerStats::StartRequest(transport
->getCommand().c_str(),
274 transport
->getRemoteHost(),
275 vhost
->getName().c_str());
277 // resolve source root
278 always_assert(!m_sourceRootInfo
.hasValue());
279 m_sourceRootInfo
.emplace(transport
);
280 if (m_sourceRootInfo
->error()) {
281 m_sourceRootInfo
->handleError(transport
);
286 string pathTranslation
= m_pathTranslation
?
287 vhost
->getPathTranslation().c_str() : "";
288 RequestURI
reqURI(vhost
, transport
, pathTranslation
,
289 m_sourceRootInfo
->path());
291 return; // already handled with redirection or 404
293 string path
= reqURI
.path().data();
294 string absPath
= reqURI
.absolutePath().data();
296 // determine whether we should compress response
297 bool compressed
= transport
->decideCompression();
299 const char *data
; int len
;
300 const char *ext
= reqURI
.ext();
302 if (reqURI
.forbidden()) {
303 transport
->sendString("Forbidden", 403);
304 transport
->onSendEnd();
308 // Determine which extensions should be treated as php
309 // source code. If the execution engine doesn't understand
310 // the source, the content will be spit out verbatim.
311 bool treatAsContent
= ext
&&
312 strcasecmp(ext
, "php") != 0 &&
313 strcasecmp(ext
, "hh") != 0 &&
314 (RuntimeOption::PhpFileExtensions
.empty() ||
315 !RuntimeOption::PhpFileExtensions
.count(ext
));
317 // If this is not a php file, check the static content cache
318 if (treatAsContent
) {
319 bool original
= compressed
;
320 // check against static content cache
321 if (StaticContentCache::TheCache
.find(path
, data
, len
, compressed
)) {
322 ScopedMem decompressed_data
;
323 // (qigao) not calling stat at this point because the timestamp of
324 // local cache file is not valuable, maybe misleading. This way
325 // the Last-Modified header will not show in response.
326 // stat(RuntimeOption::FileCache.c_str(), &st);
327 if (!original
&& compressed
) {
328 data
= gzdecode(data
, len
);
329 if (data
== nullptr) {
330 raise_fatal_error("cannot unzip compressed data");
332 decompressed_data
= const_cast<char*>(data
);
335 sendStaticContent(transport
, data
, len
, 0, compressed
, path
, ext
);
336 ServerStats::LogPage(path
, 200);
340 if (RuntimeOption::EnableStaticContentFromDisk
) {
341 String translated
= File::TranslatePath(String(absPath
));
342 if (!translated
.empty()) {
343 CstrBuffer
sb(translated
.data());
347 stat(translated
.data(), &st
);
348 sendStaticContent(transport
, sb
.data(), sb
.size(), st
.st_mtime
,
350 ServerStats::LogPage(path
, 200);
357 // proxy any URLs that not specified in ServeURLs
358 if (shouldProxyPath(path
)) {
359 for (int i
= 0; i
< RuntimeOption::ProxyRetry
; i
++) {
360 bool force
= (i
== RuntimeOption::ProxyRetry
- 1); // last one
361 if (handleProxyRequest(transport
, force
)) break;
366 // record request for debugging purpose
367 std::string tmpfile
= HttpProtocol::RecordRequest(transport
);
371 ThreadInfo::s_threadInfo
->m_reqInjectionData
.
372 setTimeout(requestTimeoutSeconds
);
376 ret
= executePHPRequest(transport
, reqURI
, m_sourceRootInfo
.value());
383 } catch (const Eval::DebuggerException
&e
) {
386 } catch (const XDebugExitExn
& e
) {
389 } catch (Object
&e
) {
391 emsg
= e
.toString().data();
395 } catch (const std::exception
&e
) {
400 g_context
->onShutdownPostSend();
401 Eval::Debugger::InterruptPSPEnded(transport
->getUrl());
403 Logger::Error("Unhandled server exception: %s", emsg
.c_str());
405 transport
->sendString(response
, code
);
406 transport
->onSendEnd();
409 HttpProtocol::ClearRecord(ret
, tmpfile
);
412 void HttpRequestHandler::abortRequest(Transport
* transport
) {
413 // TODO: t5284137 add some tests for abortRequest
414 transport
->sendString("Service Unavailable", 503);
415 transport
->onSendEnd();
418 bool HttpRequestHandler::executePHPRequest(Transport
*transport
,
420 SourceRootInfo
&sourceRootInfo
) {
421 auto context
= g_context
.getNoCheck();
422 OBFlags obFlags
= OBFlags::Default
;
423 if (transport
->getHTTPVersion() != "1.1") {
424 obFlags
|= OBFlags::OutputDisabled
;
426 context
->obStart(uninit_null(), 0, obFlags
);
427 context
->obProtect(true);
428 if (RuntimeOption::ImplicitFlush
) {
429 context
->obSetImplicitFlush(true);
431 if (RuntimeOption::EnableOutputBuffering
) {
432 if (RuntimeOption::OutputHandler
.empty()) {
435 context
->obStart(String(RuntimeOption::OutputHandler
));
438 context
->setTransport(transport
);
439 InitFiniNode::RequestStart();
441 string file
= reqURI
.absolutePath().c_str();
443 ServerStatsHelper
ssh("input");
444 HttpProtocol::PrepareSystemVariables(transport
, reqURI
, sourceRootInfo
);
445 InitFiniNode::GlobalsInit();
447 if (RuntimeOption::EnableDebugger
) {
448 Eval::DSandboxInfo sInfo
= sourceRootInfo
.getSandboxInfo();
449 Eval::Debugger::RegisterSandbox(sInfo
);
450 context
->setSandboxId(sInfo
.id());
453 sourceRootInfo
.clear();
459 // Let the debugger initialize.
460 // FIXME: hphpd can be initialized this way as well
461 DEBUGGER_ATTACHED_ONLY(phpDebuggerRequestInitHook());
462 if (RuntimeOption::EnableDebugger
) {
463 Eval::Debugger::InterruptRequestStarted(transport
->getUrl());
467 std::string errorMsg
= "Internal Server Error";
468 ret
= hphp_invoke(context
, file
, false, Array(), uninit_null(),
469 RuntimeOption::RequestInitFunction
,
470 RuntimeOption::RequestInitDocument
,
473 false /* warmupOnly */,
474 false /* richErrorMessage */);
477 String content
= context
->obDetachContents();
478 transport
->sendRaw((void*)content
.data(), content
.size());
479 code
= transport
->getResponseCode();
483 string errorPage
= context
->getErrorPage().data();
484 if (errorPage
.empty()) {
485 errorPage
= RuntimeOption::ErrorDocument500
;
487 if (!errorPage
.empty()) {
488 context
->obProtect(false);
491 context
->obProtect(true);
492 ret
= hphp_invoke(context
, errorPage
, false, Array(), uninit_null(),
493 RuntimeOption::RequestInitFunction
,
494 RuntimeOption::RequestInitDocument
,
497 false /* warmupOnly */,
498 false /* richErrorMessage */);
500 String content
= context
->obDetachContents();
501 transport
->sendRaw((void*)content
.data(), content
.size());
502 code
= transport
->getResponseCode();
504 Logger::Error("Unable to invoke error page %s", errorPage
.c_str());
505 errorPage
.clear(); // so we fall back to 500 return
508 if (errorPage
.empty()) {
509 if (RuntimeOption::ServerErrorMessage
) {
510 transport
->sendString(errorMsg
, 500, false, false, "hphp_invoke");
512 transport
->sendString(RuntimeOption::FatalErrorMessage
,
513 500, false, false, "hphp_invoke");
518 transport
->sendString("RequestInitDocument Not Found", 404);
521 if (RuntimeOption::EnableDebugger
) {
522 Eval::Debugger::InterruptRequestEnded(transport
->getUrl());
525 std::unique_ptr
<StructuredLogEntry
> entry
;
526 if (RuntimeOption::EvalProfileHWStructLog
) {
527 entry
= std::make_unique
<StructuredLogEntry
>();
528 entry
->setInt("response_code", code
);
529 auto queueBegin
= transport
->getQueueTime();
530 auto const queueTimeUs
= gettime_diff_us(queueBegin
,
531 transport
->getWallTime());
532 entry
->setInt("queue-time-us", queueTimeUs
);
534 HardwareCounter::UpdateServiceData(transport
->getCpuTime(),
535 transport
->getWallTime(),
538 if (entry
) StructuredLog::log("hhvm_request_perf", *entry
);
540 // If we have registered post-send shutdown functions, end the request before
541 // executing them. If we don't, be compatible with Zend by allowing usercode
542 // in hphp_context_shutdown to run before we end the request.
544 context
->hasShutdownFunctions(ExecutionContext::ShutdownType::PostSend
);
546 transport
->onSendEnd();
548 context
->onShutdownPostSend();
549 Eval::Debugger::InterruptPSPEnded(transport
->getUrl());
550 hphp_context_shutdown();
552 transport
->onSendEnd();
554 hphp_context_exit(false);
555 ServerStats::LogPage(file
, code
);
559 bool HttpRequestHandler::handleProxyRequest(Transport
*transport
, bool force
) {
560 auto const url
= getProxyPath(transport
->getServerObject());
564 StringBuffer response
;
565 if (!HttpProtocol::ProxyRequest(transport
, force
, url
, code
, error
,
570 transport
->sendString(error
, 500, false, false, "handleProxyRequest");
574 const char* respData
= response
.data();
578 transport
->sendRaw((void*)respData
, response
.size(), code
);
582 ///////////////////////////////////////////////////////////////////////////////