| // Copyright 2015 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 "webservd/request.h" |
| |
| #include <arpa/inet.h> |
| #include <microhttpd.h> |
| #include <netinet/in.h> |
| |
| #include <base/bind.h> |
| #include <base/files/file.h> |
| #include <base/guid.h> |
| #include <brillo/http/http_request.h> |
| #include <brillo/http/http_utils.h> |
| #include <brillo/mime_utils.h> |
| #include <brillo/streams/file_stream.h> |
| #include <brillo/strings/string_utils.h> |
| #include "webservd/log_manager.h" |
| #include "webservd/protocol_handler.h" |
| #include "webservd/request_handler_interface.h" |
| #include "webservd/server_interface.h" |
| #include "webservd/temp_file_manager.h" |
| |
| namespace webservd { |
| |
| // Helper class to provide static callback methods to microhttpd library, |
| // with the ability to access private methods of Request class. |
| class RequestHelper { |
| public: |
| static int PostDataIterator(void* cls, |
| MHD_ValueKind /* kind */, |
| const char* key, |
| const char* filename, |
| const char* content_type, |
| const char* transfer_encoding, |
| const char* data, |
| uint64_t off, |
| size_t size) { |
| auto self = reinterpret_cast<Request*>(cls); |
| return self->ProcessPostData(key, filename, content_type, transfer_encoding, |
| data, off, size) |
| ? MHD_YES |
| : MHD_NO; |
| } |
| |
| static int ValueCallback(void* cls, |
| MHD_ValueKind kind, |
| const char* key, |
| const char* value) { |
| auto self = reinterpret_cast<Request*>(cls); |
| std::string data; |
| if (value) |
| data = value; |
| if (kind == MHD_HEADER_KIND) { |
| self->headers_.emplace_back(brillo::http::GetCanonicalHeaderName(key), |
| data); |
| } else if (kind == MHD_COOKIE_KIND) { |
| // TODO(avakulenko): add support for cookies... |
| } else if (kind == MHD_POSTDATA_KIND) { |
| self->post_data_.emplace_back(key, data); |
| } else if (kind == MHD_GET_ARGUMENT_KIND) { |
| self->get_data_.emplace_back(key, data); |
| } |
| return MHD_YES; |
| } |
| }; |
| |
| FileInfo::FileInfo(const std::string& in_field_name, |
| const std::string& in_file_name, |
| const std::string& in_content_type, |
| const std::string& in_transfer_encoding) |
| : field_name(in_field_name), |
| file_name(in_file_name), |
| content_type(in_content_type), |
| transfer_encoding(in_transfer_encoding) {} |
| |
| Request::Request(const std::string& request_handler_id, |
| const std::string& url, |
| const std::string& method, |
| const std::string& version, |
| MHD_Connection* connection, |
| ProtocolHandler* protocol_handler) |
| : id_{base::GenerateGUID()}, |
| request_handler_id_{request_handler_id}, |
| url_{url}, |
| method_{method}, |
| version_{version}, |
| connection_{connection}, |
| protocol_handler_{protocol_handler} { |
| // Here we create the data pipe used to transfer the request body from the |
| // web server to the remote request handler. |
| int pipe_fds[2] = {-1, -1}; |
| CHECK_EQ(0, pipe(pipe_fds)); |
| request_data_pipe_out_ = base::File{pipe_fds[0]}; |
| CHECK(request_data_pipe_out_.IsValid()); |
| request_data_stream_ = |
| brillo::FileStream::FromFileDescriptor(pipe_fds[1], true, nullptr); |
| CHECK(request_data_stream_); |
| |
| // POST request processor. |
| post_processor_ = MHD_create_post_processor( |
| connection, 1024, &RequestHelper::PostDataIterator, this); |
| } |
| |
| Request::~Request() { |
| if (post_processor_) |
| MHD_destroy_post_processor(post_processor_); |
| GetTempFileManager()->DeleteRequestTempFiles(id_); |
| protocol_handler_->RemoveRequest(this); |
| } |
| |
| base::File Request::GetFileData(int file_id) { |
| base::File file; |
| if (file_id >= 0 && static_cast<size_t>(file_id) < file_info_.size()) { |
| file.Initialize(file_info_[file_id]->temp_file_name, |
| base::File::FLAG_OPEN | base::File::FLAG_READ); |
| } |
| return file; |
| } |
| |
| base::File Request::Complete( |
| int32_t status_code, |
| const std::vector<std::tuple<std::string, std::string>>& headers, |
| int64_t in_data_size) { |
| base::File file; |
| if (response_data_started_) |
| return file; |
| |
| response_status_code_ = status_code; |
| response_headers_.reserve(headers.size()); |
| for (const auto& tuple : headers) { |
| response_headers_.emplace_back(std::get<0>(tuple), std::get<1>(tuple)); |
| } |
| |
| // Create the pipe for response data. |
| int pipe_fds[2] = {-1, -1}; |
| CHECK_EQ(0, pipe(pipe_fds)); |
| file = base::File{pipe_fds[1]}; |
| CHECK(file.IsValid()); |
| response_data_stream_ = |
| brillo::FileStream::FromFileDescriptor(pipe_fds[0], true, nullptr); |
| CHECK(response_data_stream_); |
| |
| response_data_size_ = in_data_size; |
| response_data_started_ = true; |
| const MHD_ConnectionInfo* info = |
| MHD_get_connection_info(connection_, MHD_CONNECTION_INFO_CLIENT_ADDRESS); |
| |
| const sockaddr* client_addr = (info ? info->client_addr : nullptr); |
| LogManager::OnRequestCompleted(base::Time::Now(), client_addr, method_, url_, |
| version_, status_code, in_data_size); |
| protocol_handler_->ScheduleWork(); |
| return file; |
| } |
| |
| bool Request::Complete( |
| int32_t status_code, |
| const std::vector<std::tuple<std::string, std::string>>& /* headers */, |
| const std::string& mime_type, |
| const std::string& data) { |
| std::vector<std::tuple<std::string, std::string>> headers_copy; |
| headers_copy.emplace_back(brillo::http::response_header::kContentType, |
| mime_type); |
| base::File file = Complete(status_code, headers_copy, data.size()); |
| bool success = false; |
| if (file.IsValid()) { |
| const int size = data.size(); |
| success = (file.WriteAtCurrentPos(data.c_str(), size) == size); |
| } |
| return success; |
| } |
| |
| const std::string& Request::GetProtocolHandlerID() const { |
| return protocol_handler_->GetID(); |
| } |
| |
| int Request::GetBodyDataFileDescriptor() const { |
| int fd = dup(request_data_pipe_out_.GetPlatformFile()); |
| CHECK_GE(fd, 0); |
| return fd; |
| } |
| |
| bool Request::BeginRequestData() { |
| MHD_get_connection_values(connection_, MHD_HEADER_KIND, |
| &RequestHelper::ValueCallback, this); |
| MHD_get_connection_values(connection_, MHD_COOKIE_KIND, |
| &RequestHelper::ValueCallback, this); |
| MHD_get_connection_values(connection_, MHD_POSTDATA_KIND, |
| &RequestHelper::ValueCallback, this); |
| MHD_get_connection_values(connection_, MHD_GET_ARGUMENT_KIND, |
| &RequestHelper::ValueCallback, this); |
| // If we have POST processor, then we are parsing the request ourselves and |
| // we need to dispatch it to the handler only after all the data is parsed. |
| // Otherwise forward the request immediately and let the handler read the |
| // request data as needed. |
| if (!post_processor_) |
| ForwardRequestToHandler(); |
| return true; |
| } |
| |
| bool Request::AddRequestData(const void* data, size_t* size) { |
| if (!post_processor_) |
| return AddRawRequestData(data, size); |
| int result = |
| MHD_post_process(post_processor_, static_cast<const char*>(data), *size); |
| *size = 0; |
| return result == MHD_YES; |
| } |
| |
| void Request::EndRequestData() { |
| if (!request_data_finished_) { |
| if (request_data_stream_) |
| request_data_stream_->CloseBlocking(nullptr); |
| if (!request_forwarded_) |
| ForwardRequestToHandler(); |
| request_data_finished_ = true; |
| } |
| |
| if (response_data_started_ && !response_data_finished_) { |
| MHD_Response* resp = MHD_create_response_from_callback( |
| response_data_size_, 4096, &Request::ResponseDataCallback, this, |
| nullptr); |
| CHECK(resp); |
| for (const auto& pair : response_headers_) { |
| MHD_add_response_header(resp, pair.first.c_str(), pair.second.c_str()); |
| } |
| CHECK_EQ(MHD_YES, |
| MHD_queue_response(connection_, response_status_code_, resp)) |
| << "Failed to queue response"; |
| MHD_destroy_response(resp); // |resp| is ref-counted. |
| response_data_finished_ = true; |
| } |
| } |
| |
| void Request::ForwardRequestToHandler() { |
| request_forwarded_ = true; |
| if (!request_handler_id_.empty()) { |
| // Close all temporary file streams, if any. |
| for (auto& file : file_info_) |
| file->data_stream->CloseBlocking(nullptr); |
| |
| protocol_handler_->AddRequest(this); |
| auto p = protocol_handler_->request_handlers_.find(request_handler_id_); |
| CHECK(p != protocol_handler_->request_handlers_.end()); |
| const MHD_ConnectionInfo* info = MHD_get_connection_info( |
| connection_, MHD_CONNECTION_INFO_CLIENT_ADDRESS); |
| struct sockaddr_in6* sock_addr; |
| char src[INET6_ADDRSTRLEN] = {}; |
| if (info) { |
| sock_addr = reinterpret_cast<struct sockaddr_in6*>(info->client_addr); |
| inet_ntop(AF_INET6, &sock_addr->sin6_addr, src, INET6_ADDRSTRLEN); |
| } |
| // Send the request over D-Bus and await the response. |
| p->second.handler->HandleRequest(this, src); |
| } else { |
| // There was no handler found when request was made, respond with |
| // 404 Page Not Found. |
| Complete(brillo::http::status_code::NotFound, {}, |
| brillo::mime::text::kPlain, "Not Found"); |
| } |
| } |
| |
| bool Request::ProcessPostData(const char* key, |
| const char* filename, |
| const char* content_type, |
| const char* transfer_encoding, |
| const char* data, |
| uint64_t off, |
| size_t size) { |
| if (off > 0) |
| return AppendPostFieldData(key, data, size); |
| |
| return AddPostFieldData(key, filename, content_type, transfer_encoding, data, |
| size); |
| } |
| |
| bool Request::AddRawRequestData(const void* data, size_t* size) { |
| CHECK(*size); |
| CHECK(request_data_stream_) << "Data pipe hasn't been created."; |
| |
| size_t written = 0; |
| if (!request_data_stream_->WriteNonBlocking(data, *size, &written, nullptr)) |
| return false; |
| |
| CHECK_LE(written, *size); |
| |
| // If we didn't write all the data requested, we need to let libmicrohttpd do |
| // another write cycle. Schedule a DoWork() action here. |
| if (written != *size) |
| protocol_handler_->ScheduleWork(); |
| |
| *size -= written; |
| |
| // If written at least some data, we are good. We will be called again if more |
| // data is available. |
| if (written > 0 || waiting_for_data_) |
| return true; |
| |
| // Nothing has been written. The output pipe is full. Need to stop the data |
| // transfer on the connection and wait till some data is being read from the |
| // pipe by the request handler. |
| MHD_suspend_connection(connection_); |
| |
| // Now, just monitor the pipe and figure out when we can resume sending data |
| // over it. |
| waiting_for_data_ = request_data_stream_->WaitForData( |
| brillo::Stream::AccessMode::WRITE, |
| base::Bind(&Request::OnPipeAvailable, weak_ptr_factory_.GetWeakPtr()), |
| nullptr); |
| |
| if (!waiting_for_data_) |
| MHD_resume_connection(connection_); |
| |
| return waiting_for_data_; |
| } |
| |
| ssize_t Request::ResponseDataCallback(void* cls, |
| uint64_t /* pos */, |
| char* buf, |
| size_t max) { |
| Request* self = static_cast<Request*>(cls); |
| size_t read = 0; |
| bool eos = false; |
| if (!self->response_data_stream_->ReadNonBlocking(buf, max, &read, &eos, |
| nullptr)) { |
| return MHD_CONTENT_READER_END_WITH_ERROR; |
| } |
| |
| if (read > 0 || self->waiting_for_data_) |
| return read; |
| |
| if (eos) |
| return MHD_CONTENT_READER_END_OF_STREAM; |
| |
| // Nothing can be read. The input pipe is empty. Need to stop the data |
| // transfer on the connection and wait till some data is available from the |
| // pipe. |
| MHD_suspend_connection(self->connection_); |
| |
| self->waiting_for_data_ = self->response_data_stream_->WaitForData( |
| brillo::Stream::AccessMode::READ, |
| base::Bind(&Request::OnPipeAvailable, |
| self->weak_ptr_factory_.GetWeakPtr()), |
| nullptr); |
| |
| if (!self->waiting_for_data_) { |
| MHD_resume_connection(self->connection_); |
| return MHD_CONTENT_READER_END_WITH_ERROR; |
| } |
| return 0; |
| } |
| |
| void Request::OnPipeAvailable(brillo::Stream::AccessMode /* mode */) { |
| MHD_resume_connection(connection_); |
| waiting_for_data_ = false; |
| protocol_handler_->ScheduleWork(); |
| } |
| |
| bool Request::AddPostFieldData(const char* key, |
| const char* filename, |
| const char* content_type, |
| const char* transfer_encoding, |
| const char* data, |
| size_t size) { |
| if (filename) { |
| std::unique_ptr<FileInfo> file_info{ |
| new FileInfo{key, filename, content_type ? content_type : "", |
| transfer_encoding ? transfer_encoding : ""}}; |
| file_info->temp_file_name = GetTempFileManager()->CreateTempFileName(id_); |
| file_info->data_stream = brillo::FileStream::Open( |
| file_info->temp_file_name, brillo::Stream::AccessMode::READ_WRITE, |
| brillo::FileStream::Disposition::CREATE_ALWAYS, nullptr); |
| if (!file_info->data_stream || |
| !file_info->data_stream->WriteAllBlocking(data, size, nullptr)) { |
| return false; |
| } |
| file_info_.push_back(std::move(file_info)); |
| last_posted_data_was_file_ = true; |
| return true; |
| } |
| std::string value{data, size}; |
| post_data_.emplace_back(key, value); |
| last_posted_data_was_file_ = false; |
| return true; |
| } |
| |
| bool Request::AppendPostFieldData(const char* key, |
| const char* data, |
| size_t size) { |
| if (last_posted_data_was_file_) { |
| CHECK(!file_info_.empty()); |
| CHECK(file_info_.back()->field_name == key); |
| FileInfo* file_info = file_info_.back().get(); |
| return file_info->data_stream->WriteAllBlocking(data, size, nullptr); |
| } |
| |
| CHECK(!post_data_.empty()); |
| CHECK(post_data_.back().first == key); |
| post_data_.back().second.append(data, size); |
| return true; |
| } |
| |
| TempFileManager* Request::GetTempFileManager() { |
| return protocol_handler_->GetServer()->GetTempFileManager(); |
| } |
| |
| } // namespace webservd |