| // Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "p2p/http_server/connection_delegate.h" |
| |
| #include <arpa/inet.h> |
| #include <dirent.h> |
| #include <fcntl.h> |
| #include <netinet/in.h> |
| #include <stdint.h> |
| #include <sys/socket.h> |
| #include <sys/stat.h> |
| #include <sys/types.h> |
| #include <sys/xattr.h> |
| #include <unistd.h> |
| |
| #include <algorithm> |
| #include <cassert> |
| #include <cctype> |
| #include <cerrno> |
| #include <cinttypes> |
| #include <iomanip> |
| #include <string> |
| #include <vector> |
| |
| #include <base/files/file_path.h> |
| #include <base/logging.h> |
| #include <base/strings/string_number_conversions.h> |
| #include <base/time/time.h> |
| |
| #include "p2p/common/constants.h" |
| #include "p2p/common/server_message.h" |
| #include "p2p/common/util.h" |
| #include "p2p/http_server/server.h" |
| |
| using std::map; |
| using std::string; |
| using std::vector; |
| |
| using base::Time; |
| using base::TimeDelta; |
| |
| using p2p::common::ClockInterface; |
| using p2p::constants::kBytesPerKB; |
| using p2p::constants::kBytesPerMB; |
| using p2p::util::P2PServerMessageType; |
| using p2p::util::P2PServerRequestResult; |
| |
| namespace p2p { |
| |
| namespace http_server { |
| |
| ConnectionDelegate::ConnectionDelegate(int dirfd, |
| int fd, |
| const string& pretty_addr, |
| ServerInterface* server, |
| int64_t max_download_rate) |
| : ConnectionDelegateInterface(), |
| dirfd_(dirfd), |
| fd_(fd), |
| pretty_addr_(pretty_addr), |
| server_(server), |
| max_download_rate_(max_download_rate), |
| total_bytes_sent_(0) { |
| CHECK_NE(-1, fd_); |
| CHECK(server_ != NULL); |
| } |
| |
| ConnectionDelegateInterface* ConnectionDelegate::Construct( |
| int dirfd, |
| int fd, |
| const string& pretty_addr, |
| ServerInterface* server, |
| int64_t max_download_rate) { |
| return new ConnectionDelegate(dirfd, fd, pretty_addr, server, |
| max_download_rate); |
| } |
| |
| ConnectionDelegate::~ConnectionDelegate() { |
| CHECK_EQ(-1, fd_); |
| } |
| |
| bool ConnectionDelegate::ReadLine(string* str) { |
| CHECK(str != NULL); |
| |
| while (true) { |
| char buf[kLineBufSize]; |
| ssize_t num_recv; |
| int n; |
| |
| num_recv = recv(fd_, buf, sizeof buf, MSG_PEEK); |
| if (num_recv == -1) { |
| PLOG(ERROR) << "Error reading"; |
| return false; |
| } |
| CHECK_GE(num_recv, 0); |
| |
| // When num_recv is 0 the other end has closed the socket. If we reach this |
| // point, even with a partial line in str, we didn't get a full line and |
| // should return since no fruther data will come from the file descriptor. |
| if (num_recv == 0) |
| return false; |
| |
| for (n = 0; n < num_recv; ++n) { |
| str->push_back(buf[n]); |
| |
| if (buf[n] == '\n') { |
| num_recv = recv(fd_, buf, n + 1, 0); // skip processed data |
| CHECK(num_recv == n + 1); |
| return true; |
| } |
| |
| if (str->size() > kMaxLineLength) { |
| LOG(ERROR) << "Max line length (" << kMaxLineLength << ") exceeded"; |
| return false; |
| } |
| } |
| num_recv = recv(fd_, buf, n, 0); // skip |
| CHECK(num_recv == n); |
| } |
| } |
| |
| // Removes "\r\n" from the passed in string. Returns false if |
| // the string didn't end in "\r\n". |
| static bool TrimCRLF(string* str) { |
| CHECK(str != NULL); |
| const char* c = str->c_str(); |
| size_t len = str->size(); |
| if (len < 2) |
| return false; |
| if (strcmp(c + len - 2, "\r\n") != 0) |
| return false; |
| str->resize(len - 2); |
| return true; |
| } |
| |
| P2PServerRequestResult ConnectionDelegate::ParseHttpRequest() { |
| string request_line; |
| map<string, string> headers; |
| size_t sp1_pos, sp2_pos; |
| string request_method; |
| string request_uri; |
| string request_http_version; |
| |
| if (!ReadLine(&request_line) || !TrimCRLF(&request_line)) |
| return p2p::util::kP2PRequestResultMalformed; |
| |
| VLOG(1) << "Request line: `" << request_line << "'"; |
| |
| sp1_pos = request_line.find(" "); |
| if (sp1_pos == string::npos) { |
| LOG(ERROR) << "Malformed request line, didn't find starting space" |
| << " (request_line=`" << request_line << "')"; |
| return p2p::util::kP2PRequestResultMalformed; |
| } |
| sp2_pos = request_line.rfind(" "); |
| if (sp2_pos == string::npos) { |
| LOG(ERROR) << "Malformed request line, didn't find ending space" |
| << " (request_line=`" << request_line << "')"; |
| return p2p::util::kP2PRequestResultMalformed; |
| } |
| if (sp2_pos == sp1_pos) { |
| LOG(ERROR) << "Malformed request line, initial space is the same as " |
| << "ending space (request_line=`" << request_line << "')"; |
| return p2p::util::kP2PRequestResultMalformed; |
| } |
| CHECK(sp2_pos > sp1_pos); |
| |
| request_method = string(request_line, 0, sp1_pos); |
| request_uri = string(request_line, sp1_pos + 1, sp2_pos - sp1_pos - 1); |
| request_http_version = string(request_line, sp2_pos + 1, string::npos); |
| |
| VLOG(1) << "Parsed request line. " |
| << "method=`" << request_method << "' " |
| << "uri=`" << request_uri << "' " |
| << "http_version=`" << request_http_version << "'"; |
| |
| while (true) { |
| string line; |
| size_t colon_pos; |
| |
| if (!ReadLine(&line) || !TrimCRLF(&line)) |
| return p2p::util::kP2PRequestResultMalformed; |
| |
| if (line == "") |
| break; |
| |
| // TODO(zeuthen): support header continuation. This TODO item is tracked in |
| // https://code.google.com/p/chromium/issues/detail?id=246326 |
| colon_pos = line.find(": "); |
| if (colon_pos == string::npos) { |
| LOG(ERROR) << "Malformed HTTP header (line=`" << line << "')"; |
| return p2p::util::kP2PRequestResultMalformed; |
| } |
| |
| string key = string(line, 0, colon_pos); |
| string value = string(line, colon_pos + 2, string::npos); |
| |
| // HTTP headers are case-insensitive so lower-case. |
| std::transform(key.begin(), key.end(), key.begin(), |
| static_cast<int (*)(int c)>(std::tolower)); |
| |
| VLOG(1) << "Header[" << headers.size() << "] `" << key << "' -> `" << value |
| << "'"; |
| headers[key] = value; |
| |
| if (headers.size() == kMaxHeaders) { |
| LOG(ERROR) << "Exceeded maximum (" << kMaxHeaders |
| << ") number of HTTP headers"; |
| return p2p::util::kP2PRequestResultMalformed; |
| } |
| } |
| |
| // OK, looks like a valid HTTP request. Service the client. |
| return ServiceHttpRequest(request_method, request_uri, request_http_version, |
| headers); |
| } |
| |
| void ConnectionDelegate::Run() { |
| P2PServerRequestResult req_res = ParseHttpRequest(); |
| |
| // Report P2P.Server.RequestResult every time a HTTP request is handled. |
| server_->ReportServerMessage(p2p::util::kP2PServerRequestResult, req_res); |
| |
| if (shutdown(fd_, SHUT_RDWR) != 0) { |
| PLOG(ERROR) << "Error shutting down socket"; |
| } |
| if (close(fd_) != 0) { |
| PLOG(ERROR) << "Error closing socket"; |
| } |
| fd_ = -1; |
| |
| server_->ConnectionTerminated(this); |
| |
| delete this; |
| } |
| |
| bool ConnectionDelegate::SendResponse(int http_response_code, |
| const string& http_response_status, |
| const map<string, string>& headers, |
| const string& body) { |
| string response; |
| const char* buf; |
| size_t num_to_send; |
| size_t num_total_sent; |
| size_t body_size = body.size(); |
| bool has_content_length = false; |
| bool has_server = false; |
| |
| response = "HTTP/1.1 "; |
| response += std::to_string(http_response_code); |
| response += " "; |
| response += http_response_status; |
| response += "\r\n"; |
| for (auto const& h : headers) { |
| response += h.first + ": " + h.second + "\r\n"; |
| |
| const char* header_name = h.first.c_str(); |
| if (strcasecmp(header_name, "Content-Length") == 0) |
| has_content_length = true; |
| else if (strcasecmp(header_name, "Server") == 0) |
| has_server = true; |
| } |
| |
| if (body_size > 0 && !has_content_length) { |
| response += string("Content-Length: "); |
| response += std::to_string(body_size) + "\r\n"; |
| } |
| |
| if (!has_server) |
| response += "Server: p2p\r\n"; |
| |
| response += "Connection: close\r\n"; |
| response += "\r\n"; |
| response += body; |
| |
| buf = response.c_str(); |
| num_to_send = response.size(); |
| num_total_sent = 0; |
| while (num_to_send > 0) { |
| ssize_t num_sent = send(fd_, buf + num_total_sent, num_to_send, 0); |
| if (num_sent == -1) { |
| PLOG(ERROR) << "Error sending"; |
| return false; |
| } |
| CHECK_GT(num_sent, 0); |
| num_to_send -= num_sent; |
| num_total_sent += num_sent; |
| } |
| return true; |
| } |
| |
| /* ------------------------------------------------------------------------ */ |
| |
| bool ConnectionDelegate::SendSimpleResponse( |
| int http_response_code, const string& http_response_status) { |
| map<string, string> headers; |
| return SendResponse(http_response_code, http_response_status, headers, ""); |
| } |
| |
| /* ------------------------------------------------------------------------ */ |
| |
| // Attempt to parse |range_str| as a "ranges-specifier" as defined in |
| // section 14.35 of RFC 2616. This is typically used in the "Range" |
| // header of HTTP requests. See |
| // |
| // http://tools.ietf.org/html/rfc2616#section-14.35 |
| // |
| // NOTE: To keep things simpler, we deliberately do _not_ support the |
| // full byte range specification. |
| static bool ParseRange(const string& range_str, |
| uint64_t file_size, |
| uint64_t* range_start, |
| uint64_t* range_end) { |
| const char* s = range_str.c_str(); |
| |
| CHECK(range_start != NULL); |
| CHECK(range_end != NULL); |
| |
| if (sscanf(s, "bytes=%" SCNu64 "-%" SCNu64, range_start, range_end) == 2) { |
| return *range_start <= *range_end; |
| } else if (sscanf(s, "bytes=%" SCNu64 "-", range_start) == 1 && |
| *range_start <= file_size - 1) { |
| *range_end = file_size - 1; |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool ConnectionDelegate::SendFile(int file_fd, size_t num_bytes_to_send) { |
| ClockInterface* clock; |
| total_time_spent_ = TimeDelta(); |
| int seconds_spent_waiting = 0; |
| char buf[kPayloadBufferSize]; |
| |
| clock = server_->Clock(); |
| |
| total_bytes_sent_ = 0; |
| while (total_bytes_sent_ < num_bytes_to_send) { |
| size_t num_to_read = |
| std::min(sizeof buf, num_bytes_to_send - total_bytes_sent_); |
| size_t num_to_send_from_buf; |
| size_t num_sent_from_buf; |
| ssize_t num_read; |
| |
| Time time_start = clock->GetMonotonicTime(); |
| num_read = read(file_fd, buf, num_to_read); |
| if (num_read == 0) { |
| // EOF - handle this by sleeping and trying again later. |
| VLOG(1) << "Got EOF so sleeping one second"; |
| // Don't include the time sleeping in total_time_spent_. |
| total_time_spent_ += clock->GetMonotonicTime() - time_start; |
| clock->Sleep(TimeDelta::FromSeconds(1)); |
| time_start = clock->GetMonotonicTime(); |
| seconds_spent_waiting++; |
| |
| // Give up if socket is no longer connected. |
| if (IsStillConnected()) { |
| continue; |
| } else { |
| LOG(INFO) << pretty_addr_ << " - peer no longer connected; giving up"; |
| return false; |
| } |
| } else if (num_read < 0) { |
| // Note that the file is expected to be on a filesystem so Linux |
| // guarantees that we never get EAGAIN. In other words, we never |
| // get partial reads e.g. either we get everything we ask for or |
| // none of it. |
| PLOG(ERROR) << "Error reading"; |
| return false; |
| } |
| |
| num_to_send_from_buf = num_read; |
| num_sent_from_buf = 0; |
| while (num_to_send_from_buf > 0) { |
| ssize_t num_sent = |
| send(fd_, buf + num_sent_from_buf, num_to_send_from_buf, 0); |
| if (num_sent == -1) { |
| PLOG(ERROR) << "Error sending"; |
| return false; |
| } |
| CHECK_GT(num_sent, 0); |
| num_to_send_from_buf -= num_sent; |
| num_sent_from_buf += num_sent; |
| } |
| total_bytes_sent_ += num_sent_from_buf; |
| total_time_spent_ += clock->GetMonotonicTime() - time_start; |
| |
| // Limit download speed, if requested. Right now the speed is |
| // calculated by considering the entire download session - this |
| // could be improved by using e.g. a sliding window over the last |
| // 30 seconds or so. |
| if (max_download_rate_ != 0) { |
| int64_t bytes_allowed = |
| max_download_rate_ * total_time_spent_.InSecondsF(); |
| if (static_cast<int64_t>(total_bytes_sent_) > bytes_allowed) { |
| int64_t over_budget = |
| static_cast<int64_t>(total_bytes_sent_) - bytes_allowed; |
| int64_t usec_to_sleep = |
| (over_budget / static_cast<double>(max_download_rate_)) * |
| Time::kMicrosecondsPerSecond; |
| TimeDelta sleep_duration = TimeDelta::FromMicroseconds(usec_to_sleep); |
| clock->Sleep(sleep_duration); |
| total_time_spent_ += sleep_duration; |
| } |
| } |
| } |
| |
| // If we served a file, log the time it took us. |
| double total_seconds_spent = |
| total_time_spent_.InSecondsF() + seconds_spent_waiting; |
| if (total_bytes_sent_ > 0 && total_seconds_spent > 0) { |
| LOG(INFO) << pretty_addr_ << " - sent " << total_bytes_sent_ |
| << " bytes of response body in " << std::fixed |
| << std::setprecision(3) << total_seconds_spent << " seconds" |
| << " (" << (total_bytes_sent_ / total_seconds_spent / 1e6) |
| << " MB/s) including " << seconds_spent_waiting |
| << " seconds spent waiting for content in the file."; |
| } |
| |
| return true; |
| } |
| |
| void ConnectionDelegate::ReportSendFileMetrics(bool send_file_result) { |
| // Report P2P.Server.DownloadSpeedKBps with the average speed at wich the |
| // download was served at, every time a file was served, interrupted or not. |
| int average_speed_kbps = 0; |
| if (total_time_spent_.InSecondsF() > 0.) { |
| average_speed_kbps = |
| total_bytes_sent_ / total_time_spent_.InSecondsF() / kBytesPerKB; |
| } |
| server_->ReportServerMessage(p2p::util::kP2PServerDownloadSpeedKBps, |
| average_speed_kbps); |
| |
| // TODO(deymo): Compute and report the P2P.Server.PeakDownloadSpeedKBps also |
| // handling better the download speed computed in this function to consider |
| // only a window of time instead of the average speed for the whole file. |
| |
| // Handle the error condition. |
| if (!send_file_result) { |
| if (total_bytes_sent_ > 0) { |
| // Report P2P.Server.ContentServedInterruptedMB every time we have served |
| // part of a file but the transmission was interrupted. |
| server_->ReportServerMessage(p2p::util::kP2PServerServedInterruptedMB, |
| total_bytes_sent_ / kBytesPerMB); |
| } |
| return; |
| } |
| |
| if (total_bytes_sent_ > 0) { |
| // Report P2P.Server.ContentServedSuccessfullyMB every time we have served |
| // a file to its end. |
| server_->ReportServerMessage(p2p::util::kP2PServerServedSuccessfullyMB, |
| total_bytes_sent_ / kBytesPerMB); |
| } |
| } |
| |
| P2PServerRequestResult ConnectionDelegate::ServiceHttpRequest( |
| const string& method, |
| const string& uri, |
| const string& version, |
| const map<string, string>& headers) { |
| struct stat statbuf; |
| size_t file_size = 0; |
| map<string, string> response_headers; |
| uint64_t range_first, range_last, range_len; |
| uint response_code; |
| const char* response_string; |
| map<string, string>::const_iterator header_it; |
| string file_name; |
| int file_fd = -1; |
| char ea_value[64] = {0}; |
| bool send_file_result; |
| ssize_t ea_size; |
| // Initialize the result in an invalid RequestResult. |
| P2PServerRequestResult req_res = p2p::util::kNumP2PServerRequestResults; |
| int range_begin_percentage = 0; |
| |
| // Log User-Agent, if available |
| header_it = headers.find("user-agent"); |
| if (header_it != headers.end()) { |
| LOG(INFO) << pretty_addr_ << " - user agent: " << header_it->second; |
| } |
| |
| if (!(method == "GET" || method == "POST")) { |
| SendSimpleResponse(501, "Method Not Implemented"); |
| // A peer should never request something different than GET or POST. Report |
| // this as a malformed request. |
| req_res = p2p::util::kP2PRequestResultMalformed; |
| goto out; |
| } |
| |
| // Ensure the URI contains exactly one '/' |
| if (uri[0] != '/' || uri.find('/', 1) != string::npos) { |
| SendSimpleResponse(400, "Bad Request"); |
| req_res = p2p::util::kP2PRequestResultMalformed; |
| goto out; |
| } |
| |
| LOG(INFO) << pretty_addr_ << " - requesting resource with URI " << uri; |
| |
| // Handle /index.html |
| if (uri == "/" || uri == "/index.html") { |
| SendSimpleResponse(404, "No index"); |
| req_res = p2p::util::kP2PRequestResultIndex; |
| goto out; |
| } |
| |
| file_name = uri.substr(1) + ".p2p"; |
| VLOG(1) << "Opening `" << file_name << "'"; |
| file_fd = openat(dirfd_, file_name.c_str(), O_RDONLY); |
| if (file_fd == -1) { |
| SendSimpleResponse(404, string("Error opening file: ") + strerror(errno)); |
| req_res = p2p::util::kP2PRequestResultNotFound; |
| goto out; |
| } |
| |
| if (fstat(file_fd, &statbuf) != 0) { |
| SendSimpleResponse(404, "Error getting information about file"); |
| req_res = p2p::util::kP2PRequestResultNotFound; |
| goto out; |
| } |
| file_size = statbuf.st_size; |
| VLOG(1) << "File is " << file_size << " bytes"; |
| LOG(INFO) << "File is " << file_size << " bytes"; |
| |
| ea_size = |
| fgetxattr(file_fd, "user.cros-p2p-filesize", &ea_value, sizeof ea_value); |
| if (ea_size > 0 && ea_value[0] != 0) { |
| int64_t val; |
| if (base::StringToInt64(ea_value, &val)) { |
| VLOG(1) << "Read user.cros-p2p-filesize=" << val; |
| if (static_cast<size_t>(val) > file_size) { |
| // Simply update file_size to what the EA says - code below |
| // handles that by checking for EOF and sleeping |
| file_size = val; |
| } |
| } |
| } |
| |
| if (file_size == 0) { |
| range_first = 0; |
| range_last = 0; |
| range_len = 0; |
| response_code = 200; |
| response_string = "OK"; |
| } else { |
| header_it = headers.find("range"); |
| if (header_it != headers.end()) { |
| if (!ParseRange(header_it->second, file_size, &range_first, |
| &range_last)) { |
| SendSimpleResponse(400, "Error parsing Range header"); |
| req_res = p2p::util::kP2PRequestResultMalformed; |
| goto out; |
| } |
| if (range_last >= file_size) { |
| SendSimpleResponse(416, "Requested Range Not Satisfiable"); |
| req_res = p2p::util::kP2PRequestResultMalformed; |
| goto out; |
| } |
| response_code = 206; |
| response_string = "Partial Content"; |
| response_headers["Content-Range"] = std::to_string(range_first) + "-" + |
| std::to_string(range_last) + "/" + |
| std::to_string(file_size); |
| } else { |
| range_first = 0; |
| range_last = file_size - 1; |
| response_code = 200; |
| response_string = "OK"; |
| } |
| CHECK(range_first <= range_last); |
| CHECK(range_last < file_size); |
| range_len = range_last - range_first + 1; |
| } |
| |
| response_headers["Content-Type"] = "application/octet-stream"; |
| response_headers["Content-Length"] = std::to_string(range_len); |
| if (!SendResponse(response_code, response_string, response_headers, "")) { |
| req_res = p2p::util::kP2PRequestResultResponseInterrupted; |
| goto out; |
| } |
| |
| if (range_first > 0) { |
| if (lseek(file_fd, static_cast<off_t>(range_first), SEEK_SET) != |
| static_cast<off_t>(range_first)) { |
| PLOG(ERROR) << "Error seeking"; |
| req_res = p2p::util::kP2PRequestResultNotFound; |
| goto out; |
| } |
| } |
| |
| // From now on, we don't report a result as Malformed. Report the |
| // P2P.Server.RangeBeginPercentage at the begining of the file serving period, |
| // since it is being reported either the transmission is interrupted or nor. |
| if (file_size > 0) |
| range_begin_percentage = 100.0 * range_first / file_size; |
| server_->ReportServerMessage(p2p::util::kP2PServerRangeBeginPercentage, |
| range_begin_percentage); |
| |
| // Send the file and report the metrics associated with the transfer. |
| send_file_result = SendFile(file_fd, range_len); |
| ReportSendFileMetrics(send_file_result); |
| |
| req_res = send_file_result ? p2p::util::kP2PRequestResultResponseSent |
| : p2p::util::kP2PRequestResultResponseInterrupted; |
| |
| out: |
| if (file_fd != -1) |
| close(file_fd); |
| return req_res; |
| } |
| |
| bool ConnectionDelegate::IsStillConnected() { |
| char buf[1]; |
| ssize_t num_recv; |
| |
| // Sockets become readable when closed by the peer, which can be |
| // used to figure out if the other end is still connected |
| num_recv = recv(fd_, buf, 0, MSG_DONTWAIT | MSG_PEEK); |
| if (num_recv == -1) |
| return true; |
| return false; |
| } |
| |
| } // namespace http_server |
| |
| } // namespace p2p |