blob: 8e2a6d5ae9dc59c41848b1102f9ec8813979349a [file] [log] [blame]
// Copyright 2020 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 "http_util.h"
#include <algorithm>
#include <vector>
#include <base/strings/string_split.h>
#include <base/strings/string_util.h>
namespace {
const std::string kHttpRequestEnd = "\r\n\r\n"; // NOLINT(runtime/string)
const std::string kHttpLineEnd = "\r\n"; // NOLINT(runtime/string)
// Determines if |message| starts with the string |target|.
bool StartsWith(const SmartBuffer& message, const std::string& target) {
return message.FindFirstOccurrence(target) == 0;
}
bool MessageContains(const SmartBuffer& message, const std::string& s) {
ssize_t pos = message.FindFirstOccurrence(s);
return pos != -1;
}
base::Optional<HttpHeaders> ParseHttpHeaders(const std::string& headers) {
std::vector<std::string> lines = base::SplitStringUsingSubstr(
headers, kHttpLineEnd, base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
const std::string header_split = ":";
HttpHeaders parsed_headers;
for (const std::string& line : lines) {
size_t split = line.find(header_split);
if (split == std::string::npos) {
LOG(ERROR) << "Malformed header: '" << line << "'";
return base::nullopt;
}
parsed_headers.emplace(
line.substr(0, split),
base::TrimString(line.substr(split + header_split.size()), " ",
base::TRIM_LEADING));
}
return parsed_headers;
}
bool ValidateHttpVersion(const std::string& version) {
std::vector<std::string> tokens = base::SplitString(
version, "/", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
if (tokens.size() != 2 || tokens[0] != "HTTP") {
LOG(ERROR) << "Malformed HTTP version: '" << version << "'";
return false;
}
// 1.0 support is prohibited by IPP spec for HTTP transport.
if (tokens[1] == "1.0") {
LOG(ERROR) << "HTTP version 1.0 is not supported";
return false;
}
return true;
}
} // namespace
// static
base::Optional<HttpRequest> HttpRequest::Deserialize(SmartBuffer* message) {
ssize_t request_end = message->FindFirstOccurrence(kHttpRequestEnd);
if (request_end == -1) {
LOG(ERROR) << "Message does not contain end of header marker";
return base::nullopt;
}
ssize_t request_line_end = message->FindFirstOccurrence(kHttpLineEnd);
if (request_line_end == -1) {
LOG(ERROR) << "Message does not contain end of line marker";
return base::nullopt;
}
if (request_line_end == 0) {
LOG(ERROR) << "Request line is empty";
return base::nullopt;
}
// First parse the first line of the request, which should look something like
// "GET /ipp/print HTTP/1.1" or "POST /eSCL/ScannerCapabilities HTTP/1.1".
std::string request_line(message->data(), message->data() + request_line_end);
std::vector<std::string> request_line_tokens = base::SplitString(
request_line, " ", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
if (request_line_tokens.size() != 3) {
LOG(ERROR) << "Malformed request line: '" << request_line << "'";
return base::nullopt;
}
std::string http_version = request_line_tokens[2];
if (!ValidateHttpVersion(http_version))
return base::nullopt;
HttpRequest request;
request.method = request_line_tokens[0];
request.uri = request_line_tokens[1];
// Now, parse the rest of the HTTP request after the request line as headers.
// In case that there are no headers, make sure when we skip the kHttpLineEnd
// we aren't actually skipping past the kHttpRequestEnd marker.
size_t headers_start =
std::min<size_t>(request_line_end + kHttpLineEnd.size(), request_end);
std::string request_headers(message->data() + headers_start,
message->data() + request_end);
base::Optional<HttpHeaders> headers = ParseHttpHeaders(request_headers);
if (!headers) {
LOG(ERROR) << "Failed to parse request headers";
return base::nullopt;
}
request.headers = headers.value();
// Erase the data we just parsed from |message|.
message->Erase(0, request_end + kHttpRequestEnd.size());
return request;
}
bool HttpRequest::IsChunkedMessage() const {
auto encoding = headers.find("Transfer-Encoding");
return encoding != headers.end() && encoding->second == "chunked";
}
bool IsHttpChunkedMessage(const SmartBuffer& message) {
ssize_t i = message.FindFirstOccurrence("Transfer-Encoding: chunked");
return i != -1;
}
bool ContainsHttpHeader(const SmartBuffer& message) {
return MessageContains(message, "POST /ipp/print HTTP");
}
bool ContainsHttpBody(const SmartBuffer& message) {
// We are making the assumption that if |message| does not contain an HTTP
// header then |message| is the body of an HTTP message.
if (!ContainsHttpHeader(message)) {
return true;
}
// If |message| contains an HTTP header, check to see if there's anything
// immediately following it.
ssize_t pos = message.FindFirstOccurrence("\r\n\r\n");
CHECK_GE(pos, 0) << "HTTP header does not contain end-of-header marker";
size_t start = pos + 4;
return start < message.size();
}
size_t ExtractChunkSize(const SmartBuffer& message) {
ssize_t end = message.FindFirstOccurrence("\r\n");
if (end == -1) {
return 0;
}
std::string hex_string;
const std::vector<uint8_t>& contents = message.contents();
for (size_t i = 0; i < end; ++i) {
hex_string += static_cast<char>(contents[i]);
}
return std::stoll(hex_string, 0, 16);
}
SmartBuffer ParseHttpChunkedMessage(SmartBuffer* message) {
CHECK_NE(message, nullptr) << "Given null message";
// If |message| starts with the trailing CRLF end-of-chunk indicator from the
// previous chunk then erase it.
if (StartsWith(*message, "\r\n")) {
message->Erase(0, 2);
}
size_t chunk_size = ExtractChunkSize(*message);
LOG(INFO) << "Chunk size: " << chunk_size;
ssize_t start = message->FindFirstOccurrence("\r\n");
SmartBuffer ret(0);
if (start == -1) {
return ret;
}
ret.Add(*message, start + 2, chunk_size);
// In case |message| contains multiple chunks, we remove the chunk which was
// just parsed.
//
// The length of the prefix to be deleted is calculated as follows:
// start - represents the hex-encoded length value.
// chunk_size - the number of bytes read out for the message body.
// 2 - the CRLF characters which trail the length.
size_t to_erase_length = start + chunk_size + 2;
CHECK_GE(message->size(), to_erase_length);
message->Erase(0, to_erase_length);
// If |message| also contains the trailing CRLF end-of-chunk indicator, then
// erase it.
if (StartsWith(*message, "\r\n")) {
message->Erase(0, 2);
}
return ret;
}
bool ContainsFinalChunk(const SmartBuffer& message) {
ssize_t i = message.FindFirstOccurrence("0\r\n\r\n");
return i > 0 && message.size() == i + 5;
}
bool ProcessMessageChunks(SmartBuffer* message) {
CHECK(message != nullptr) << "Process - Given null message";
if (IsHttpChunkedMessage(*message)) {
// If |message| contains an HTTP header then we discard it.
int start = message->FindFirstOccurrence("\r\n\r\n");
CHECK_GT(start, 0) << "Failed to process HTTP chunked message";
message->Erase(0, start + 4);
}
SmartBuffer chunk;
while (message->size() > 0) {
chunk = ParseHttpChunkedMessage(message);
}
return chunk.size() != 0;
}
bool RemoveHttpHeader(SmartBuffer* message) {
return HttpRequest::Deserialize(message).has_value();
}
SmartBuffer ExtractIppMessage(SmartBuffer* message) {
CHECK_NE(message, nullptr) << "Received null message";
return ParseHttpChunkedMessage(message);
}
SmartBuffer MergeDocument(SmartBuffer* message) {
CHECK_NE(message, nullptr) << "Received null message";
SmartBuffer document;
while (message->size() > 0) {
SmartBuffer chunk = ParseHttpChunkedMessage(message);
document.Add(chunk);
}
return document;
}
std::string GetHttpResponseHeader(size_t size) {
return "HTTP/1.1 200 OK\r\n"
"Server: localhost:0\r\n"
"Content-Type: application/ipp\r\n"
"Content-Length: " +
std::to_string(size) +
"\r\n"
"Connection: close\r\n\r\n";
}