Codemod asserts to assertxs in the runtime
[hiphop-php.git] / hphp / runtime / server / transport.cpp
blob695bccb4932340ce58854b01af493b3b738cd675
1 /*
2 +----------------------------------------------------------------------+
3 | HipHop for PHP |
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>
48 namespace HPHP {
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));
70 enableCompression();
71 m_chunksSentSizes.clear();
74 Transport::~Transport() {
75 if (m_url) {
76 free(m_url);
78 if (m_postData) {
79 free(m_postData);
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);
89 #endif
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";
102 case Method::POST: {
103 const char *m = getExtendedMethod();
104 return m ? m : "POST";
106 default:
107 break;
109 return "";
112 ///////////////////////////////////////////////////////////////////////////////
113 // url
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 ///////////////////////////////////////////////////////////////////////////////
126 // parameters
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
132 int i = 0, o = 0;
133 unsigned char *s = (unsigned char *)value;
135 while (s[i]) {
136 if (s[i] == '+') {
137 s[o++] = ' ';
138 i++;
139 } else if (s[i] == '%' && isxdigit(s[i+1]) && isxdigit(s[i+2])) {
140 char num;
141 num = (s[i+1] >= 'A') ? ((s[i+1] & 0xdf) - 'A') + 10 : (s[i+1] - '0');
142 num *= 16;
143 num += (s[i+2] >= 'A') ? ((s[i+2] & 0xdf) - 'A') + 10 : (s[i+2] - '0');
144 s[o++] = num;
145 i+=3;
146 } else {
147 s[o++] = s[i++];
150 if (i && o) s[o] = '\0';
153 void Transport::parseQuery(char *query, ParamMap &params) {
154 if (!query || !*query) return;
156 char *l;
157 char *k = strtok_r(query, "&", &l);
158 while (k && *k) {
159 char *v = strchr(k, '=');
160 const char *fv;
161 if (v == nullptr) {
162 fv = "";
163 } else {
164 *v = '\0';
165 v++;
166 if (*v) urlUnescape(v);
167 fv = 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();
178 assertx(url);
180 const char *p = strchr(url, '?');
181 if (p) {
182 m_url = strdup(p + 1);
183 } else {
184 m_url = strdup("");
187 parseQuery(m_url, m_getParams);
191 void Transport::parsePostParams() {
192 if (!m_postDataParsed) {
193 assertx(m_postData == nullptr);
194 size_t size;
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
199 // truncate it.
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) {
212 parseGetParams();
214 if (m_getParams.find(name) != m_getParams.end()) {
215 return true;
219 if (method == Method::POST || method == Method::AUTO) {
220 if (!m_postDataParsed) {
221 parsePostParams();
223 if (m_postParams.find(name) != m_postParams.end()) {
224 return true;
228 return false;
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) {
237 parseGetParams();
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) {
247 parsePostParams();
249 ParamMap::const_iterator iter = m_postParams.find(name);
250 if (iter != m_postParams.end()) {
251 return iter->second[0];
255 return "";
258 int Transport::getIntParam(const char *name,
259 Method method /* = Method::GET */) {
260 std::string param = getParam(name, method);
261 if (param.empty()) {
262 return 0;
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);
270 if (param.empty()) {
271 return 0;
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) {
281 parseGetParams();
283 ParamMap::const_iterator iter = m_getParams.find(name);
284 if (iter != m_getParams.end()) {
285 const std::vector<const char *> &params = iter->second;
286 values.insert(values.end(), params.begin(), params.end());
290 if (method == Method::POST || method == Method::AUTO) {
291 if (!m_postDataParsed) {
292 parsePostParams();
294 ParamMap::const_iterator iter = m_postParams.find(name);
295 if (iter != m_postParams.end()) {
296 const std::vector<const char *> &params = iter->second;
297 values.insert(values.end(), params.begin(), params.end());
302 void Transport::getSplitParam(const char *name,
303 std::vector<std::string> &values,
304 char delimiter,
305 Method method /* = Method::GET */) {
306 std::string param = getParam(name, method);
307 if (!param.empty()) {
308 folly::split(delimiter, param, values);
312 ///////////////////////////////////////////////////////////////////////////////
313 // headers
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;
322 do {
323 value++;
324 } while (*value == ' ');
326 return true;
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);
339 return false;
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);
350 assertx(value);
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) {
368 setResponse(303);
369 } else {
370 setResponse(302);
373 setResponse(302);
377 void Transport::addHeader(const char *name, const char *value) {
378 assertx(name && *name);
379 assertx(value);
380 addHeaderNoLock(name, value);
383 void Transport::addHeader(const String& header) {
384 String name;
385 const char *value;
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);
393 assertx(value);
394 m_responseHeaders[name].clear();
395 addHeaderNoLock(name, value);
398 void Transport::replaceHeader(const String& header) {
399 String name;
400 const char *value;
401 if (splitHeader(header, name, value)) {
402 replaceHeader(name.data(), value);
406 void Transport::removeHeader(const char *name) {
407 if (name && *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();
440 if (len >= 2
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) {
450 // Then split by ';'
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) {
459 return true;
462 return false;
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] == ';') {
473 pos += len;
474 if (hasValue) {
475 if (pos == header.size() || header[pos] == ';') return true;
476 } else {
477 if (pos < header.size() && header[pos] == '=') return true;
481 return false;
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] == ';') {
490 pos += name.size();
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);
498 return "";
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;
513 return true;
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;
529 return true;
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;
538 return true;
541 m_compressionDecision = CompressionDecision::ShouldNot;
542 return false;
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 {
551 return "1.1";
554 size_t Transport::getRequestSize() const {
555 return 0;
558 void Transport::setMimeType(const String& mimeType) {
559 m_mimeType = mimeType.data();
562 String Transport::getMimeType() {
563 return String(m_mimeType);
566 ///////////////////////////////////////////////////////////////////////////////
567 // cookies
569 namespace {
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
582 // does.
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);
599 if (!encode_url) {
600 validateCookieString(value, "values");
603 validateCookieString(path, "paths");
605 validateCookieString(domain, "domains");
607 String encoded_value;
608 int len = 0;
609 if (!value.empty()) {
610 encoded_value = encode_url ? url_encode(value.data(), value.size())
611 : value;
612 len += encoded_value.size();
614 len += path.size();
615 len += domain.size();
617 std::string cookie;
618 cookie.reserve(len + 100);
619 if (value.empty()) {
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";
631 } else {
632 cookie += name.data();
633 cookie += "=";
634 cookie += encoded_value.isNull() ? "" : encoded_value.data();
635 if (expire > 0) {
636 if (expire > 253402300799LL) {
637 raise_warning("Expiry date cannot have a year greater than 9999");
638 return false;
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();
650 if (!path.empty()) {
651 cookie += "; path=";
652 cookie += path.data();
654 if (!domain.empty()) {
655 cookie += "; domain=";
656 cookie += domain.data();
658 if (secure) {
659 cookie += "; secure";
661 if (httponly) {
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);
673 return true;
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);
683 } else {
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);
696 return ret;
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
723 * or brotli.
725 if (compressed ? m_compressionDecision != CompressionDecision::HasTo
726 : (isCompressionEnabled() &&
727 !(acceptEncoding("gzip") || acceptEncoding("br")))) {
728 addHeaderImpl("Vary", "Accept-Encoding");
732 if (compressed) {
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
742 // of chunking.
743 if (chunked) {
744 raise_warning("Cannot use chunked HTTP response and Content-MD5 header "
745 "at the same time. Dropping Content-MD5.");
746 } else {
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
784 ).toString();
785 auto const output = StringUtil::Base64Encode(iv + encrypted);
786 if (debug) {
787 auto const decrypted = HHVM_FN(openssl_decrypt)(
788 encrypted, cipher, key, k_OPENSSL_RAW_DATA, iv
789 ).toString();
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");
805 namespace {
807 void LogException(const char* msg) {
808 try {
809 throw;
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) {
815 try {
816 Logger::Error("%s: %s", msg, e.toString().c_str());
817 } catch (...) {
818 Logger::Error("%s: (e.toString() failed)", msg);
820 } catch (...) {
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) {
832 if (state == 0) {
833 return;
836 String value;
837 IniSetting::Get(ini_key, value);
838 if (state == -1) {
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,
853 int size,
854 bool& compressed,
855 bool last) {
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) {
862 decideCompression();
865 if (compressed) {
866 // pre-compressed responses are always gzip
867 m_encodingType = CompressionType::Gzip;
868 return response;
871 if (!isCompressionEnabled() ||
872 m_compressionDecision == CompressionDecision::ShouldNot) {
873 return response;
876 if (!m_headerSent) {
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);
915 return response;
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);
934 int len = size;
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);
942 compressed = true;
944 } else {
945 Logger::Error("Unable to compress response: level=%d len=%d",
946 compressionLevel, len);
949 return response;
952 StringHolder Transport::compressBrotli(const void *data, int size,
953 bool &compressed, bool last) {
954 if (m_brotliCompressor == nullptr) {
955 brotli::BrotliParams params;
956 params.mode =
957 (brotli::BrotliParams::Mode)RuntimeOption::BrotliCompressionMode;
959 Variant quality;
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);
978 size_t len = size;
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);
986 compressed = true;
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.
1019 if (m_sendEnded) {
1020 return;
1023 if (!compressed && RuntimeOption::ForceChunkedEncoding) {
1024 chunked = true;
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)) {
1030 return;
1033 if (m_chunkedEncoding) {
1034 chunked = true;
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;
1060 try {
1061 vm_call_user_func(cellAsVariant(g_context->m_headerCallback),
1062 init_null_variant);
1063 } catch (...) {
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);
1102 eomSent = 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);
1111 if (!eomSent) {
1112 onSendEndImpl();
1114 // Record that we have ended the request so any further output is discarded.
1115 m_sendEnded = true;
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 ///////////////////////////////////////////////////////////////////////////////
1148 // support rfc1867
1150 bool Transport::isUploadedFile(const String& filename) {
1151 return is_uploaded_file(filename.c_str());
1154 ///////////////////////////////////////////////////////////////////////////////
1155 // IDebuggable
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";
1164 return "(unknown)";
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 ///////////////////////////////////////////////////////////////////////////////