// Copyright 2014 The Crashpad Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "util/net/http_multipart_builder.h"

#include <string.h>
#include <sys/types.h>

#include <utility>
#include <vector>

#include "base/check.h"
#include "base/rand_util.h"
#include "base/strings/stringprintf.h"
#include "util/net/http_body.h"
#include "util/net/http_body_gzip.h"

namespace crashpad {

namespace {

constexpr char kCRLF[] = "\r\n";

constexpr char kBoundaryCRLF[] = "\r\n\r\n";

// Generates a random string suitable for use as a multipart boundary.
std::string GenerateBoundaryString() {
  // RFC 2046 §5.1.1 says that the boundary string may be 1 to 70 characters
  // long, choosing from the set of alphanumeric characters along with
  // characters from the set “'()+_,-./:=? ”, and not ending in a space.
  // However, some servers have been observed as dealing poorly with certain
  // nonalphanumeric characters. See
  // blink/Source/platform/network/FormDataEncoder.cpp
  // blink::FormDataEncoder::GenerateUniqueBoundaryString().
  //
  // This implementation produces a 56-character string with over 190 bits of
  // randomness (62^32 > 2^190).
  std::string boundary_string = "---MultipartBoundary-";
  for (int index = 0; index < 32; ++index) {
    static constexpr char kCharacters[] =
        "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    int random_value =
        base::RandInt(0, static_cast<int>(strlen(kCharacters)) - 1);
    boundary_string += kCharacters[random_value];
  }
  boundary_string += "---";
  return boundary_string;
}

// Escapes the specified name to be suitable for the name field of a
// form-data part.
std::string EncodeMIMEField(const std::string& name) {
  // This URL-escapes the quote character and newline characters, per Blink. See
  // blink/Source/platform/network/FormDataEncoder.cpp
  // blink::AppendQuotedString(). %-encoding is endorsed by RFC 7578 §2, with
  // approval for otherwise unencoded UTF-8 given by RFC 7578 §5.1. Blink does
  // not escape the '%' character, but it seems appropriate to do so in order to
  // be able to decode the string properly.
  std::string encoded;
  for (char character : name) {
    switch (character) {
      case '\r':
      case '\n':
      case '"':
      case '%':
        encoded += base::StringPrintf("%%%02x", character);
        break;
      default:
        encoded += character;
        break;
    }
  }

  return encoded;
}

// Returns a string, formatted with a multipart boundary and a field name,
// after which the contents of the part at |name| can be appended.
std::string GetFormDataBoundary(const std::string& boundary,
                                const std::string& name) {
  return base::StringPrintf(
      "--%s%sContent-Disposition: form-data; name=\"%s\"",
      boundary.c_str(),
      kCRLF,
      EncodeMIMEField(name).c_str());
}

void AssertSafeMIMEType(const std::string& string) {
  for (size_t i = 0; i < string.length(); ++i) {
    char c = string[i];
    CHECK((c >= 'a' && c <= 'z') ||
          (c >= 'A' && c <= 'Z') ||
          (c >= '0' && c <= '9') ||
          c == '/' ||
          c == '.' ||
          c == '_' ||
          c == '+' ||
          c == '-');
  }
}

}  // namespace

HTTPMultipartBuilder::HTTPMultipartBuilder()
    : boundary_(GenerateBoundaryString()),
      form_data_(),
      file_attachments_(),
      gzip_enabled_(false) {}

HTTPMultipartBuilder::~HTTPMultipartBuilder() {
}

void HTTPMultipartBuilder::SetGzipEnabled(bool gzip_enabled) {
  gzip_enabled_ = gzip_enabled;
}

void HTTPMultipartBuilder::SetFormData(const std::string& key,
                                       const std::string& value) {
  EraseKey(key);
  form_data_[key] = value;
}

void HTTPMultipartBuilder::SetFileAttachment(
    const std::string& key,
    const std::string& upload_file_name,
    FileReaderInterface* reader,
    const std::string& content_type) {
  EraseKey(upload_file_name);

  FileAttachment attachment;
  attachment.filename = EncodeMIMEField(upload_file_name);
  attachment.reader = reader;

  if (content_type.empty()) {
    attachment.content_type = "application/octet-stream";
  } else {
    AssertSafeMIMEType(content_type);
    attachment.content_type = content_type;
  }

  file_attachments_[key] = attachment;
}

std::unique_ptr<HTTPBodyStream> HTTPMultipartBuilder::GetBodyStream() {
  // The objects inserted into this vector will be owned by the returned
  // CompositeHTTPBodyStream. Take care to not early-return without deleting
  // this memory.
  std::vector<HTTPBodyStream*> streams;

  for (const auto& pair : form_data_) {
    std::string field = GetFormDataBoundary(boundary_, pair.first);
    field += kBoundaryCRLF;
    field += pair.second;
    field += kCRLF;
    streams.push_back(new StringHTTPBodyStream(field));
  }

  for (const auto& pair : file_attachments_) {
    const FileAttachment& attachment = pair.second;
    std::string header = GetFormDataBoundary(boundary_, pair.first);
    header += base::StringPrintf("; filename=\"%s\"%s",
        attachment.filename.c_str(), kCRLF);
    header += base::StringPrintf("Content-Type: %s%s",
        attachment.content_type.c_str(), kBoundaryCRLF);

    streams.push_back(new StringHTTPBodyStream(header));
    streams.push_back(new FileReaderHTTPBodyStream(attachment.reader));
    streams.push_back(new StringHTTPBodyStream(kCRLF));
  }

  streams.push_back(
      new StringHTTPBodyStream("--"  + boundary_ + "--" + kCRLF));

  auto composite =
      std::unique_ptr<HTTPBodyStream>(new CompositeHTTPBodyStream(streams));
  if (gzip_enabled_) {
    return std::unique_ptr<HTTPBodyStream>(
        new GzipHTTPBodyStream(std::move(composite)));
  }
  return composite;
}

void HTTPMultipartBuilder::PopulateContentHeaders(
    HTTPHeaders* http_headers) const {
  std::string content_type =
      base::StringPrintf("multipart/form-data; boundary=%s", boundary_.c_str());
  (*http_headers)[kContentType] = content_type;

  if (gzip_enabled_) {
    (*http_headers)[kContentEncoding] = "gzip";
  }
}

void HTTPMultipartBuilder::EraseKey(const std::string& key) {
  auto data_it = form_data_.find(key);
  if (data_it != form_data_.end())
    form_data_.erase(data_it);

  auto file_it = file_attachments_.find(key);
  if (file_it != file_attachments_.end())
    file_attachments_.erase(file_it);
}

}  // namespace crashpad
