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 +----------------------------------------------------------------------+
17 #include "hphp/runtime/server/transport.h"
19 #include <boost/algorithm/string.hpp>
21 #include "hphp/runtime/server/server.h"
22 #include "hphp/runtime/server/upload.h"
23 #include "hphp/runtime/server/server-stats.h"
24 #include "hphp/runtime/base/builtin-functions.h"
25 #include "hphp/runtime/base/file.h"
26 #include "hphp/runtime/base/string-util.h"
27 #include "hphp/runtime/base/datetime.h"
28 #include "hphp/runtime/base/runtime-option.h"
29 #include "hphp/runtime/base/url.h"
30 #include "hphp/runtime/base/zend-url.h"
31 #include "hphp/runtime/base/tv-type.h"
32 #include "hphp/runtime/server/access-log.h"
33 #include "hphp/runtime/server/http-protocol.h"
34 #include "hphp/runtime/ext/openssl/ext_openssl.h"
35 #include "hphp/runtime/ext/string/ext_string.h"
36 #include "hphp/util/brotli.h"
37 #include "hphp/util/compatibility.h"
38 #include "hphp/util/compression.h"
39 #include "hphp/util/hardware-counter.h"
40 #include "hphp/util/logger.h"
41 #include "hphp/util/service-data.h"
42 #include "hphp/util/text-util.h"
43 #include "hphp/util/timer.h"
45 #include <folly/String.h>
46 #include <enc/encode.h>
49 ///////////////////////////////////////////////////////////////////////////////
51 static const char HTTP_RESPONSE_STATS_PREFIX
[] = "http_response_";
52 const char* Transport::ENCODING_TYPE_TO_NAME
[CompressionType::Max
+ 1] = {
53 "br", "br", "gzip", ""};
55 Transport::Transport()
56 : m_instructions(0), m_sleepTime(0), m_usleepTime(0),
57 m_nsleepTimeS(0), m_nsleepTimeN(0), m_url(nullptr),
58 m_postData(nullptr), m_postDataParsed(false),
59 m_chunkedEncoding(false), m_headerSent(false),
60 m_responseCode(-1), m_firstHeaderSet(false), m_firstHeaderLine(0),
61 m_responseSize(0), m_responseTotalSize(0), m_responseSentSize(0),
62 m_flushTimeUs(0), m_sendEnded(false), m_sendContentType(true),
63 m_encodingType(CompressionType::Max
), m_isSSL(false),
64 m_compressionDecision(CompressionDecision::NotDecidedYet
),
65 m_threadType(ThreadType::RequestThread
) {
66 memset(&m_queueTime
, 0, sizeof(m_queueTime
));
67 memset(&m_wallTime
, 0, sizeof(m_wallTime
));
68 memset(&m_cpuTime
, 0, sizeof(m_cpuTime
));
69 memset(m_acceptedEncodings
, 0, sizeof(m_acceptedEncodings
));
71 m_chunksSentSizes
.clear();
74 Transport::~Transport() {
81 m_chunksSentSizes
.clear();
84 void Transport::onRequestStart(const timespec
&queueTime
) {
85 m_queueTime
= queueTime
;
86 Timer::GetMonotonicTime(m_wallTime
);
87 #ifdef CLOCK_THREAD_CPUTIME_ID
88 gettime(CLOCK_THREAD_CPUTIME_ID
, &m_cpuTime
);
91 * The hardware counter is only 48 bits, so reset this at the beginning
92 * of every request to make sure we don't overflow.
94 HardwareCounter::Reset();
95 m_instructions
= HardwareCounter::GetInstructionCount();
98 const char *Transport::getMethodName() {
99 switch (getMethod()) {
100 case Method::GET
: return "GET";
101 case Method::HEAD
: return "HEAD";
103 const char *m
= getExtendedMethod();
104 return m
? m
: "POST";
112 ///////////////////////////////////////////////////////////////////////////////
115 const char *Transport::getServerObject() {
116 const char *url
= getUrl();
117 return URL::getServerObject(url
);
120 std::string
Transport::getCommand() {
121 const char *url
= getServerObject();
122 return URL::getCommand(url
);
125 ///////////////////////////////////////////////////////////////////////////////
128 // copied and re-factored from clearsilver-0.10.5/cgi/cgi.c
129 void Transport::urlUnescape(char *value
) {
130 assertx(value
&& *value
); // check before calling this function
133 unsigned char *s
= (unsigned char *)value
;
139 } else if (s
[i
] == '%' && isxdigit(s
[i
+1]) && isxdigit(s
[i
+2])) {
141 num
= (s
[i
+1] >= 'A') ? ((s
[i
+1] & 0xdf) - 'A') + 10 : (s
[i
+1] - '0');
143 num
+= (s
[i
+2] >= 'A') ? ((s
[i
+2] & 0xdf) - 'A') + 10 : (s
[i
+2] - '0');
150 if (i
&& o
) s
[o
] = '\0';
153 void Transport::parseQuery(char *query
, ParamMap
¶ms
) {
154 if (!query
|| !*query
) return;
157 char *k
= strtok_r(query
, "&", &l
);
159 char *v
= strchr(k
, '=');
166 if (*v
) urlUnescape(v
);
169 if (*k
) urlUnescape(k
);
170 params
[k
].push_back(fv
);
171 k
= strtok_r(nullptr, "&", &l
);
175 void Transport::parseGetParams() {
176 if (m_url
== nullptr) {
177 const char *url
= getServerObject();
180 const char *p
= strchr(url
, '?');
182 m_url
= strdup(p
+ 1);
187 parseQuery(m_url
, m_getParams
);
191 void Transport::parsePostParams() {
192 if (!m_postDataParsed
) {
193 assertx(m_postData
== nullptr);
195 const char *data
= (const char *)getPostData(size
);
196 if (data
&& *data
&& size
) {
197 // Post data may be binary, but if parsePostParams() is called, any
198 // wellformed data cannot have embedded NULs. If it does, we simply
200 m_postData
= strndup(data
, size
);
201 parseQuery(m_postData
, m_postParams
);
203 m_postDataParsed
= true;
207 bool Transport::paramExists(const char *name
,
208 Method method
/* = Method::GET */) {
209 assertx(name
&& *name
);
210 if (method
== Method::GET
|| method
== Method::AUTO
) {
211 if (m_url
== nullptr) {
214 if (m_getParams
.find(name
) != m_getParams
.end()) {
219 if (method
== Method::POST
|| method
== Method::AUTO
) {
220 if (!m_postDataParsed
) {
223 if (m_postParams
.find(name
) != m_postParams
.end()) {
231 std::string
Transport::getParam(const char *name
,
232 Method method
/* = Method::GET */) {
233 assertx(name
&& *name
);
235 if (method
== Method::GET
|| method
== Method::AUTO
) {
236 if (m_url
== nullptr) {
239 ParamMap::const_iterator iter
= m_getParams
.find(name
);
240 if (iter
!= m_getParams
.end()) {
241 return iter
->second
[0];
245 if (method
== Method::POST
|| method
== Method::AUTO
) {
246 if (!m_postDataParsed
) {
249 ParamMap::const_iterator iter
= m_postParams
.find(name
);
250 if (iter
!= m_postParams
.end()) {
251 return iter
->second
[0];
258 int Transport::getIntParam(const char *name
,
259 Method method
/* = Method::GET */) {
260 std::string param
= getParam(name
, method
);
264 return atoi(param
.c_str());
267 long long Transport::getInt64Param(const char *name
,
268 Method method
/* = Method::GET */) {
269 std::string param
= getParam(name
, method
);
273 return atoll(param
.c_str());
276 void Transport::getArrayParam(const char *name
,
277 std::vector
<std::string
> &values
,
278 Method method
/* = GET */) {
279 if (method
== Method::GET
|| method
== Method::AUTO
) {
280 if (m_url
== nullptr) {
283 ParamMap::const_iterator iter
= m_getParams
.find(name
);
284 if (iter
!= m_getParams
.end()) {
285 const std::vector
<const char *> ¶ms
= iter
->second
;
286 values
.insert(values
.end(), params
.begin(), params
.end());
290 if (method
== Method::POST
|| method
== Method::AUTO
) {
291 if (!m_postDataParsed
) {
294 ParamMap::const_iterator iter
= m_postParams
.find(name
);
295 if (iter
!= m_postParams
.end()) {
296 const std::vector
<const char *> ¶ms
= iter
->second
;
297 values
.insert(values
.end(), params
.begin(), params
.end());
302 void Transport::getSplitParam(const char *name
,
303 std::vector
<std::string
> &values
,
305 Method method
/* = Method::GET */) {
306 std::string param
= getParam(name
, method
);
307 if (!param
.empty()) {
308 folly::split(delimiter
, param
, values
);
312 ///////////////////////////////////////////////////////////////////////////////
315 bool Transport::splitHeader(const String
& header
, String
&name
, const char *&value
) {
316 int pos
= header
.find(':');
318 if (pos
!= String::npos
) {
319 name
= header
.substr(0, pos
);
320 value
= header
.data() + pos
;
324 } while (*value
== ' ');
329 // header("HTTP/1.0 404 Not Found");
330 // header("HTTP/1.0 404");
331 if (strncasecmp(header
.data(), "http/", 5) == 0) {
332 int pos1
= header
.find(' ');
333 if (pos1
!= String::npos
) {
334 int pos2
= header
.find(' ', pos1
+ 1);
335 if (pos2
== String::npos
) pos2
= header
.size();
336 if (pos2
- pos1
> 1) {
337 setResponse(atoi(header
.data() + pos1
),
338 header
.size() - pos2
> 1 ? header
.data() + pos2
: nullptr);
344 throw ExtendedException(
345 "Invalid argument \"header\": [%s]", header
.c_str());
348 void Transport::addHeaderNoLock(const char *name
, const char *value
) {
349 assertx(name
&& *name
);
352 if (!m_firstHeaderSet
) {
353 m_firstHeaderSet
= true;
354 m_firstHeaderFile
= g_context
->getContainingFileName()->data();
355 m_firstHeaderLine
= g_context
->getLine();
358 std::string svalue
= value
;
359 replaceAll(svalue
, "\n", "");
360 m_responseHeaders
[name
].push_back(svalue
);
362 if (strcasecmp(name
, "Location") == 0 && m_responseCode
!= 201 &&
363 !(m_responseCode
>= 300 && m_responseCode
<=307)) {
364 /* Zend seems to set 303 on a post with HTTP version > 1.0 in the code but
365 * in our testing we can only get it to give 302.
366 Method m = getMethod();
367 if (m != Method::GET && m != Method::HEAD) {
377 void Transport::addHeader(const char *name
, const char *value
) {
378 assertx(name
&& *name
);
380 addHeaderNoLock(name
, value
);
383 void Transport::addHeader(const String
& header
) {
386 if (splitHeader(header
, name
, value
)) {
387 addHeader(name
.data(), value
);
391 void Transport::replaceHeader(const char *name
, const char *value
) {
392 assertx(name
&& *name
);
394 m_responseHeaders
[name
].clear();
395 addHeaderNoLock(name
, value
);
398 void Transport::replaceHeader(const String
& header
) {
401 if (splitHeader(header
, name
, value
)) {
402 replaceHeader(name
.data(), value
);
406 void Transport::removeHeader(const char *name
) {
408 m_responseHeaders
.erase(name
);
409 if (strcasecmp(name
, "Set-Cookie") == 0) {
410 m_responseCookiesList
.clear();
415 void Transport::removeAllHeaders() {
416 m_responseHeaders
.clear();
417 m_responseCookiesList
.clear();
420 void Transport::getResponseHeaders(HeaderMap
&headers
) {
421 headers
= m_responseHeaders
;
423 std::vector
<std::string
> &cookies
= headers
["Set-Cookie"];
424 std::list
<std::string
> cookies_existing
= getCookieLines();
425 cookies
.insert(cookies
.end(), cookies_existing
.begin(),
426 cookies_existing
.end());
429 bool Transport::acceptEncoding(const char *encoding
) {
430 // Examples of valid encodings that we want to accept
431 // gzip;q=1.0, identity; q=0.5, *;q=0
432 // compress;q=0.5, gzip;q=1.0
433 // For now, we don't care about the qvalue
435 assertx(encoding
&& *encoding
);
436 std::string header
= getHeader("Accept-Encoding");
438 // Handle leading and trailing quotes
439 size_t len
= header
.length();
441 && ((header
[0] == '"' && header
[len
-1] == '"')
442 || (header
[0] == '\'' && header
[len
-1] == '\''))) {
443 header
= header
.substr(1, len
- 2);
446 // Split the header by ','
447 std::vector
<std::string
> cTokens
;
448 folly::split(',', header
, cTokens
);
449 for (size_t i
= 0; i
< cTokens
.size(); ++i
) {
451 auto& cToken
= cTokens
[i
];
452 std::vector
<std::string
> scTokens
;
453 folly::split(';', cToken
, scTokens
);
454 assertx(scTokens
.size() > 0);
455 // lhs contains the encoding
456 // rhs, if it exists, contains the qvalue
457 std::string lhs
= boost::trim_copy(scTokens
[0]);
458 if (strcasecmp(lhs
.c_str(), encoding
) == 0) {
465 bool Transport::cookieExists(const char *name
) {
466 assertx(name
&& *name
);
467 std::string header
= getHeader("Cookie");
468 int len
= strlen(name
);
469 bool hasValue
= (strchr(name
, '=') != nullptr);
470 for (size_t pos
= header
.find(name
); pos
!= std::string::npos
;
471 pos
= header
.find(name
, pos
+ 1)) {
472 if (pos
== 0 || isspace(header
[pos
-1]) || header
[pos
-1] == ';') {
475 if (pos
== header
.size() || header
[pos
] == ';') return true;
477 if (pos
< header
.size() && header
[pos
] == '=') return true;
484 std::string
Transport::getCookie(const std::string
&name
) {
485 assertx(!name
.empty());
486 std::string header
= getHeader("Cookie");
487 for (size_t pos
= header
.find(name
); pos
!= std::string::npos
;
488 pos
= header
.find(name
, pos
+ 1)) {
489 if (pos
== 0 || isspace(header
[pos
-1]) || header
[pos
-1] == ';') {
491 if (pos
< header
.size() && header
[pos
] == '=') {
492 size_t end
= header
.find(';', pos
+ 1);
493 if (end
!= std::string::npos
) end
-= pos
+ 1;
494 return header
.substr(pos
+ 1, end
);
501 bool Transport::decideCompression() {
502 assertx(m_compressionDecision
== CompressionDecision::NotDecidedYet
);
504 if (!RuntimeOption::ForceCompressionURL
.empty() &&
505 getCommand() == RuntimeOption::ForceCompressionURL
) {
506 // ForceCompression exists only to support cases when proxy removes
507 // Accept-Encoding header but browser can read compressed data. This
508 // feature is much less relevant since HTTPS does not allow proxies to
509 // remove headers. So we won't expand support for this feature to
510 // new compression types.
511 m_acceptedEncodings
[CompressionType::Gzip
] = true;
512 m_compressionDecision
= CompressionDecision::HasTo
;
516 bool acceptsEncoding
= false;
517 if (acceptEncoding("br")) {
518 m_acceptedEncodings
[CompressionType::Brotli
] = true;
519 m_acceptedEncodings
[CompressionType::BrotliChunked
] = true;
520 acceptsEncoding
= true;
522 if (acceptEncoding("gzip")) {
523 m_acceptedEncodings
[CompressionType::Gzip
] = true;
524 acceptsEncoding
= true;
527 if (acceptsEncoding
) {
528 m_compressionDecision
= CompressionDecision::Should
;
532 if ((!RuntimeOption::ForceCompressionCookie
.empty() &&
533 cookieExists(RuntimeOption::ForceCompressionCookie
.c_str())) ||
534 (!RuntimeOption::ForceCompressionParam
.empty() &&
535 paramExists(RuntimeOption::ForceCompressionParam
.c_str()))) {
536 m_compressionDecision
= CompressionDecision::Should
;
537 m_acceptedEncodings
[CompressionType::Gzip
] = true;
541 m_compressionDecision
= CompressionDecision::ShouldNot
;
545 void Transport::setResponse(int code
, const char *info
) {
546 m_responseCode
= code
;
547 m_responseCodeInfo
= info
? info
: HttpProtocol::GetReasonString(code
);
550 std::string
Transport::getHTTPVersion() const {
554 size_t Transport::getRequestSize() const {
558 void Transport::setMimeType(const String
& mimeType
) {
559 m_mimeType
= mimeType
.data();
562 String
Transport::getMimeType() {
563 return String(m_mimeType
);
566 ///////////////////////////////////////////////////////////////////////////////
571 // Make sure cookie names do not contain any illegal characters.
572 // Throw a fatal exception if one does.
573 void validateCookieNameString(const String
& str
) {
574 if (!str
.empty() && strpbrk(str
.data(), "=,; \t\r\n\013\014")) {
575 raise_error("Cookie names can not contain any of the following "
576 "'=,; \\t\\r\\n\\013\\014'");
580 // Make sure a component (path, value, domain) of a cookie does not
581 // contain any illegal characters. Throw a fatal exception if it
583 void validateCookieString(const String
& str
, const char* component
) {
584 if (!str
.empty() && strpbrk(str
.data(), ",; \t\r\n\013\014")) {
585 raise_error("Cookie %s can not contain any of the following "
586 "',; \\t\\r\\n\\013\\014'", component
);
592 bool Transport::setCookie(const String
& name
, const String
& value
, int64_t expire
/* = 0 */,
593 const String
& path
/* = "" */, const String
& domain
/* = "" */,
594 bool secure
/* = false */,
595 bool httponly
/* = false */,
596 bool encode_url
/* = true */) {
597 validateCookieNameString(name
);
600 validateCookieString(value
, "values");
603 validateCookieString(path
, "paths");
605 validateCookieString(domain
, "domains");
607 String encoded_value
;
609 if (!value
.empty()) {
610 encoded_value
= encode_url
? url_encode(value
.data(), value
.size())
612 len
+= encoded_value
.size();
615 len
+= domain
.size();
618 cookie
.reserve(len
+ 100);
621 * MSIE doesn't delete a cookie when you set it to a null value
622 * so in order to force cookies to be deleted, even on MSIE, we
623 * pick an expiry date in the past
625 String sdt
= req::make
<DateTime
>(1, true)->
626 toString(DateTime::DateFormat::Cookie
);
627 cookie
+= name
.data();
628 cookie
+= "=deleted; expires=";
629 cookie
+= sdt
.data();
630 cookie
+= "; Max-Age=0";
632 cookie
+= name
.data();
634 cookie
+= encoded_value
.isNull() ? "" : encoded_value
.data();
636 if (expire
> 253402300799LL) {
637 raise_warning("Expiry date cannot have a year greater than 9999");
640 cookie
+= "; expires=";
641 String sdt
= req::make
<DateTime
>(expire
, true)->
642 toString(DateTime::DateFormat::Cookie
);
643 cookie
+= sdt
.data();
644 cookie
+= "; Max-Age=";
645 String sdelta
= String(expire
- time(0));
646 cookie
+= sdelta
.data();
652 cookie
+= path
.data();
654 if (!domain
.empty()) {
655 cookie
+= "; domain=";
656 cookie
+= domain
.data();
659 cookie
+= "; secure";
662 cookie
+= "; httponly";
665 // PHP5 does not deduplicate cookies. That behavior is preserved when
666 // CookieDeduplicate is not enabled. Otherwise, we will only keep the
667 // last cookie for a given name-domain-path triplet.
668 String dedup_key
= name
+ "\n" + domain
+ "\n" + path
;
670 m_responseCookiesList
.emplace(m_responseCookiesList
.end(),
671 dedup_key
.data(), cookie
);
676 std::list
<std::string
> Transport::getCookieLines() {
677 std::list
<std::string
> ret
;
678 if (RuntimeOption::AllowDuplicateCookies
) {
679 for(CookieList::const_iterator iter
= m_responseCookiesList
.begin();
680 iter
!= m_responseCookiesList
.end(); ++iter
) {
681 ret
.push_back(iter
->second
);
684 // We will dedupe with last-one-wins semantics by walking backwards and
685 // including only those whose dedupe key we have not seen yet, then
686 // reversing the list
687 std::unordered_set
<std::string
> already_seen
;
688 for(auto iter
= m_responseCookiesList
.crbegin();
689 iter
!= m_responseCookiesList
.crend(); ++iter
) {
690 if (already_seen
.find(iter
->first
) == already_seen
.end()) {
691 ret
.push_front(iter
->second
);
692 already_seen
.insert(iter
->first
);
700 ///////////////////////////////////////////////////////////////////////////////
702 void Transport::prepareHeaders(bool compressed
, bool chunked
,
703 const StringHolder
&response
, const StringHolder
& orig_response
) {
704 for (HeaderMap::const_iterator iter
= m_responseHeaders
.begin();
705 iter
!= m_responseHeaders
.end(); ++iter
) {
706 const std::vector
<std::string
> &values
= iter
->second
;
707 for (unsigned int i
= 0; i
< values
.size(); i
++) {
708 addHeaderImpl(iter
->first
.c_str(), values
[i
].c_str());
712 const std::list
<std::string
> cookies
= getCookieLines();
713 for (std::list
<std::string
>::const_iterator iter
= cookies
.begin();
714 iter
!= cookies
.end(); ++iter
) {
715 addHeaderImpl("Set-Cookie", iter
->c_str());
718 if (RuntimeOption::ServerAddVaryEncoding
) {
720 * Our response may vary depending on the Accept-Encoding header if
721 * - we compressed it, and compression was not forced; or
722 * - we didn't compress it because the client does not accept gzip
725 if (compressed
? m_compressionDecision
!= CompressionDecision::HasTo
726 : (isCompressionEnabled() &&
727 !(acceptEncoding("gzip") || acceptEncoding("br")))) {
728 addHeaderImpl("Vary", "Accept-Encoding");
733 addHeaderImpl("Content-Encoding", compressionName(m_encodingType
));
734 removeHeaderImpl("Content-Length");
735 // Remove the Content-MD5 header coming from PHP if we compressed the data,
736 // as the checksum is going to be invalid.
737 auto it
= m_responseHeaders
.find("Content-MD5");
738 if (it
!= m_responseHeaders
.end()) {
739 removeHeaderImpl("Content-MD5");
740 // Re-add it back unless this is a chunked response. We'd have to buffer
741 // the response completely to compute the MD5, which defeats the purpose
744 raise_warning("Cannot use chunked HTTP response and Content-MD5 header "
745 "at the same time. Dropping Content-MD5.");
747 std::string cur_md5
= it
->second
[0];
748 String expected_md5
= StringUtil::Base64Encode(StringUtil::MD5(
749 orig_response
.data(), orig_response
.size(), true));
750 // Can never trust these PHP people...
751 if (expected_md5
.c_str() != cur_md5
) {
752 raise_warning("Content-MD5 mismatch. Expected: %s, Got: %s",
753 expected_md5
.c_str(), cur_md5
.c_str());
755 addHeaderImpl("Content-MD5", StringUtil::Base64Encode(StringUtil::MD5(
756 response
.data(), response
.size(), true)).c_str());
761 if (m_responseHeaders
.find("Content-Type") == m_responseHeaders
.end() &&
762 m_responseCode
!= 304) {
763 std::string contentType
= "text/html";
764 if (IniSetting::Get("default_charset") != "") {
765 contentType
+= "; charset=" + IniSetting::Get("default_charset");
767 addHeaderImpl("Content-Type", contentType
.c_str());
770 if (RuntimeOption::ExposeHPHP
) {
771 addHeaderImpl("X-Powered-By", (String("HHVM/") + HHVM_VERSION
).c_str());
774 if ((RuntimeOption::ExposeXFBServer
|| RuntimeOption::ExposeXFBDebug
) &&
775 !RuntimeOption::XFBDebugSSLKey
.empty() &&
776 m_responseHeaders
.find("X-FB-Debug") == m_responseHeaders
.end()) {
777 String ip
= this->getServerAddr();
778 String key
= RuntimeOption::XFBDebugSSLKey
;
779 String
cipher("AES-256-CBC");
780 auto const iv_len
= HHVM_FN(openssl_cipher_iv_length
)(cipher
).toInt32();
781 auto const iv
= HHVM_FN(openssl_random_pseudo_bytes
)(iv_len
).toString();
782 auto const encrypted
= HHVM_FN(openssl_encrypt
)(
783 ip
, cipher
, key
, k_OPENSSL_RAW_DATA
, iv
785 auto const output
= StringUtil::Base64Encode(iv
+ encrypted
);
787 auto const decrypted
= HHVM_FN(openssl_decrypt
)(
788 encrypted
, cipher
, key
, k_OPENSSL_RAW_DATA
, iv
790 assertx(decrypted
.get()->same(ip
.get()));
792 addHeaderImpl("X-FB-Debug", output
.c_str());
795 // shutting down servers, so need to terminate all Keep-Alive connections
796 if (!RuntimeOption::EnableKeepAlive
|| isServerStopping()) {
797 addHeaderImpl("Connection", "close");
798 removeHeaderImpl("Keep-Alive");
800 // so lower level transports can ignore incoming "Connection: keep-alive"
801 removeRequestHeaderImpl("Connection");
807 void LogException(const char* msg
) {
810 } catch (Exception
& e
) {
811 Logger::Error("%s: %s", msg
, e
.getMessage().c_str());
812 } catch (std::exception
& e
) {
813 Logger::Error("%s: %s", msg
, e
.what());
814 } catch (Object
& e
) {
816 Logger::Error("%s: %s", msg
, e
.toString().c_str());
818 Logger::Error("%s: (e.toString() failed)", msg
);
821 Logger::Error("%s: (unknown exception)", msg
);
825 bool isOff(const String
& s
) {
826 return s
.size() == 3 && bstrcaseeq(s
.data(), "off", 3);
828 bool isOn(const String
& s
) {
829 return s
.size() == 2 && bstrcaseeq(s
.data(), "on", 2);
831 void finalizeCompressionOnOff(int8_t& state
, const char* ini_key
) {
837 IniSetting::Get(ini_key
, value
);
839 /* default off, can opt in */
840 state
= isOn(value
) ? 1 : 0;
841 } else /* state == 1 */ {
842 /* default on, can opt out */
843 state
= isOff(value
) ? 0 : 1;
848 const char* Transport::compressionName(CompressionType type
) {
849 return ENCODING_TYPE_TO_NAME
[static_cast<int>(type
)];
852 StringHolder
Transport::prepareResponse(const void* data
,
856 StringHolder
response((const char*)data
, size
);
858 // we don't use chunk encoding to send anything pre-compressed
859 assertx(!compressed
|| !m_chunkedEncoding
);
861 if (m_compressionDecision
== CompressionDecision::NotDecidedYet
) {
866 // pre-compressed responses are always gzip
867 m_encodingType
= CompressionType::Gzip
;
871 if (!isCompressionEnabled() ||
872 m_compressionDecision
== CompressionDecision::ShouldNot
) {
877 finalizeCompressionOnOff(
878 m_compressionEnabled
[CompressionType::Brotli
], "brotli.compression");
879 finalizeCompressionOnOff(
880 m_compressionEnabled
[CompressionType::BrotliChunked
],
881 "brotli.chunked_compression");
882 finalizeCompressionOnOff(
883 m_compressionEnabled
[CompressionType::Gzip
], "zlib.output_compression");
885 // If PHP disables a particular compression, then it is the same as if
886 // encoding is not accepted.
887 for (int i
= 0; i
< CompressionType::Max
; i
++) {
888 if (!m_compressionEnabled
[i
]) {
889 m_acceptedEncodings
[i
] = false;
893 // Gzip has 20 bytes header, so anything smaller than a few bytes probably
894 // wouldn't benefit much from compression
895 if (m_chunkedEncoding
|| size
> 50 ||
896 m_compressionDecision
== CompressionDecision::HasTo
) {
897 if (m_chunkedEncoding
&&
898 m_acceptedEncodings
[CompressionType::BrotliChunked
]) {
899 m_encodingType
= CompressionType::Brotli
;
900 } else if (!m_chunkedEncoding
&&
901 m_acceptedEncodings
[CompressionType::Brotli
]) {
902 m_encodingType
= CompressionType::Brotli
;
903 } else if (m_acceptedEncodings
[CompressionType::Gzip
]) {
904 m_encodingType
= CompressionType::Gzip
;
909 if (m_encodingType
== CompressionType::Brotli
) {
910 response
= compressBrotli(data
, size
, compressed
, last
);
911 } else if (m_encodingType
== CompressionType::Gzip
) {
912 response
= compressGzip(data
, size
, compressed
, last
);
918 StringHolder
Transport::compressGzip(const void *data
, int size
,
919 bool &compressed
, bool last
) {
920 StringHolder
response((const char *)data
, size
);
922 int compressionLevel
= RuntimeOption::GzipCompressionLevel
;
923 String compressionLevelStr
;
924 IniSetting::Get("zlib.output_compression_level", compressionLevelStr
);
925 int level
= compressionLevelStr
.toInt64();
926 if (level
> compressionLevel
&&
927 level
<= RuntimeOption::GzipMaxCompressionLevel
) {
928 compressionLevel
= level
;
930 if (m_compressor
== nullptr) {
931 m_compressor
= std::make_unique
<StreamCompressor
>(
932 compressionLevel
, CODING_GZIP
, true);
935 char *compressedData
=
936 m_compressor
->compress((const char*)data
, len
, last
);
937 if (compressedData
) {
938 StringHolder
deleter(compressedData
, len
, true);
939 if (m_chunkedEncoding
|| len
< size
||
940 m_compressionDecision
== CompressionDecision::HasTo
) {
941 response
= std::move(deleter
);
945 Logger::Error("Unable to compress response: level=%d len=%d",
946 compressionLevel
, len
);
952 StringHolder
Transport::compressBrotli(const void *data
, int size
,
953 bool &compressed
, bool last
) {
954 if (m_brotliCompressor
== nullptr) {
955 brotli::BrotliParams params
;
957 (brotli::BrotliParams::Mode
)RuntimeOption::BrotliCompressionMode
;
960 IniSetting::Get("brotli.compression_quality", quality
);
961 params
.quality
= quality
.asInt64Val();
963 Variant lgWindowSize
;
964 IniSetting::Get("brotli.compression_lgwin", lgWindowSize
);
965 params
.lgwin
= lgWindowSize
.asInt64Val();
966 if (size
&& !m_chunkedEncoding
) {
967 // If there is only one block (i.e. non-chunked content) set a maximum
968 // brotli window of ceil(log2(size)). This way the reader doesn't have
969 // to waste memory constructing a larger window which will never be used.
970 params
.lgwin
= std::min(
971 static_cast<unsigned int>(params
.lgwin
),
972 folly::findLastSet(static_cast<unsigned int>(size
) - 1));
975 m_brotliCompressor
= std::make_unique
<brotli::BrotliCompressor
>(params
);
979 auto compressedData
=
980 HPHP::compressBrotli(m_brotliCompressor
.get(), data
, len
, last
);
981 if (!compressedData
) {
982 Logger::Error("Unable to compress response to brotli: size=%d", size
);
983 return StringHolder((const char*)data
, size
);
987 return StringHolder(compressedData
, len
, true);
990 void Transport::enableCompression() {
991 m_compressionEnabled
[CompressionType::Brotli
] =
992 RuntimeOption::BrotliCompressionEnabled
;
993 m_compressionEnabled
[CompressionType::BrotliChunked
] =
994 RuntimeOption::BrotliChunkedCompressionEnabled
;
995 m_compressionEnabled
[CompressionType::Gzip
] =
996 RuntimeOption::GzipCompressionLevel
? 1 : 0;
999 void Transport::disableCompression() {
1000 for (int i
= 0; i
< CompressionType::Max
; ++i
) {
1001 m_compressionEnabled
[i
] = 0;
1005 bool Transport::isCompressionEnabled() const {
1006 return m_compressionEnabled
[CompressionType::Brotli
] ||
1007 m_compressionEnabled
[CompressionType::BrotliChunked
] ||
1008 m_compressionEnabled
[CompressionType::Gzip
];
1011 void Transport::sendRaw(void *data
, int size
, int code
/* = 200 */,
1012 bool compressed
/* = false */,
1013 bool chunked
/* = false */,
1014 const char *codeInfo
/* = nullptr */
1016 // There are post-send functions that can run. Any output from them should
1017 // be ignored as it doesn't make sense to try and send data after the
1018 // request has ended.
1023 if (!compressed
&& RuntimeOption::ForceChunkedEncoding
) {
1027 // I don't think there is any need to send an empty chunk, other than sending
1028 // out headers earlier, which seems to be a useless feature.
1029 if (size
== 0 && (chunked
|| m_chunkedEncoding
)) {
1033 if (m_chunkedEncoding
) {
1035 assertx(!compressed
);
1036 } else if (chunked
) {
1037 m_chunkedEncoding
= true;
1038 assertx(!compressed
);
1041 sendRawInternal(data
, size
, code
, compressed
, codeInfo
);
1044 void Transport::sendRawInternal(const void *data
, int size
,
1045 int code
/* = 200 */,
1046 bool compressed
/* = false */,
1047 const char *codeInfo
/* = nullptr */
1050 bool chunked
= m_chunkedEncoding
;
1052 if (!g_context
->m_headerCallbackDone
&&
1053 !cellIsNull(&g_context
->m_headerCallback
)) {
1054 // We could use m_headerSent here, however it seems we can still
1055 // end up in an infinite loop when:
1056 // m_headerCallback calls flush()
1057 // flush() triggers php's recursion guard
1058 // the recursion guard calls back into m_headerCallback
1059 g_context
->m_headerCallbackDone
= true;
1061 vm_call_user_func(cellAsVariant(g_context
->m_headerCallback
),
1064 LogException("HeaderCallback");
1068 // compression handling
1069 ServerStatsHelper
ssh("send");
1070 StringHolder response
= prepareResponse(data
, size
, compressed
, !chunked
);
1072 if (m_responseCode
< 0) {
1073 setResponse(code
, codeInfo
);
1076 // HTTP header handling
1077 if (!m_headerSent
) {
1078 prepareHeaders(compressed
, chunked
, response
,
1079 StringHolder(static_cast<const char*>(data
), size
));
1080 m_headerSent
= true;
1083 m_responseSize
+= response
.size();
1084 ServerStats::SetThreadMode(ServerStats::ThreadMode::Writing
);
1085 sendImpl(response
.data(), response
.size(), m_responseCode
, chunked
, false);
1086 ServerStats::SetThreadMode(ServerStats::ThreadMode::Processing
);
1088 ServerStats::LogBytes(size
);
1089 if (RuntimeOption::EnableStats
&& RuntimeOption::EnableWebStats
) {
1090 ServerStats::Log("network.uncompressed", size
);
1091 ServerStats::Log("network.compressed", response
.size());
1095 void Transport::onSendEnd() {
1096 bool eomSent
= false;
1097 if ((m_compressor
|| m_brotliCompressor
) && m_chunkedEncoding
) {
1098 assertx(m_headerSent
);
1099 bool compressed
= false;
1100 StringHolder response
= prepareResponse("", 0, compressed
, true);
1101 sendImpl(response
.data(), response
.size(), m_responseCode
, true, true);
1103 } else if (!m_headerSent
) {
1104 m_compressionDecision
= CompressionDecision::ShouldNot
;
1105 sendRawInternal("", 0);
1107 auto httpResponseStats
= ServiceData::createTimeSeries(
1108 folly::to
<std::string
>(HTTP_RESPONSE_STATS_PREFIX
, getResponseCode()),
1109 {ServiceData::StatsType::SUM
});
1110 httpResponseStats
->addValue(1);
1114 // Record that we have ended the request so any further output is discarded.
1118 void Transport::redirect(const char *location
, int code
/* = 302 */,
1119 const char *info
/* = nullptr */) {
1120 addHeaderImpl("Location", location
);
1121 setResponse(code
, info
);
1122 sendString("Moved", code
);
1125 void Transport::onFlushProgress(int writtenSize
, int64_t delayUs
) {
1126 m_responseSentSize
+= writtenSize
;
1127 m_flushTimeUs
+= delayUs
;
1128 m_chunksSentSizes
.push_back(writtenSize
);
1131 void Transport::onChunkedProgress(int writtenSize
) {
1132 m_responseSentSize
+= writtenSize
;
1133 m_chunksSentSizes
.push_back(writtenSize
);
1136 void Transport::getChunkSentSizes(Array
&ret
) {
1137 for (unsigned int i
= 0; i
< m_chunksSentSizes
.size(); i
++) {
1138 ret
.append(m_chunksSentSizes
[i
]);
1142 int Transport::getLastChunkSentSize() {
1143 size_t size
= m_chunksSentSizes
.size();
1144 return size
== 0 ? 0 : m_chunksSentSizes
.back();
1147 ///////////////////////////////////////////////////////////////////////////////
1150 bool Transport::isUploadedFile(const String
& filename
) {
1151 return is_uploaded_file(filename
.c_str());
1154 ///////////////////////////////////////////////////////////////////////////////
1157 const char *Transport::getThreadTypeName() const {
1158 switch (m_threadType
) {
1159 case ThreadType::RequestThread
: return "Web Request";
1160 case ThreadType::PageletThread
: return "Pagelet Thread";
1161 case ThreadType::XboxThread
: return "Xbox Thread";
1162 case ThreadType::RpcThread
: return "RPC Thread";
1167 void Transport::debuggerInfo(InfoVec
&info
) {
1168 Add(info
, "Thread Type", getThreadTypeName());
1169 Add(info
, "URL", getCommand());
1170 Add(info
, "HTTP", getHTTPVersion());
1171 Add(info
, "Method", getMethodName());
1172 if (getMethod() == Method::POST
) {
1173 size_t size
; getPostData(size
);
1174 Add(info
, "Post Data", FormatSize(size
));
1178 ///////////////////////////////////////////////////////////////////////////////