1// Copyright 2014 The Chromium OS Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include <brillo/http/http_form_data.h>
6
7#include <set>
8
9#include <base/files/file_util.h>
10#include <base/files/scoped_temp_dir.h>
11#include <brillo/mime_utils.h>
12#include <brillo/streams/file_stream.h>
13#include <brillo/streams/input_stream_set.h>
14#include <gtest/gtest.h>
15
16namespace brillo {
17namespace http {
18namespace {
19std::string GetFormFieldData(FormField* field) {
20  std::vector<StreamPtr> streams;
21  CHECK(field->ExtractDataStreams(&streams));
22  StreamPtr stream = InputStreamSet::Create(std::move(streams), nullptr);
23
24  std::vector<uint8_t> data(stream->GetSize());
25  EXPECT_TRUE(stream->ReadAllBlocking(data.data(), data.size(), nullptr));
26  return std::string{data.begin(), data.end()};
27}
28}  // anonymous namespace
29
30TEST(HttpFormData, TextFormField) {
31  TextFormField form_field{"field1", "abcdefg", mime::text::kPlain, "7bit"};
32  const char expected_header[] =
33      "Content-Disposition: form-data; name=\"field1\"\r\n"
34      "Content-Type: text/plain\r\n"
35      "Content-Transfer-Encoding: 7bit\r\n"
36      "\r\n";
37  EXPECT_EQ(expected_header, form_field.GetContentHeader());
38  EXPECT_EQ("abcdefg", GetFormFieldData(&form_field));
39}
40
41TEST(HttpFormData, FileFormField) {
42  base::ScopedTempDir dir;
43  ASSERT_TRUE(dir.CreateUniqueTempDir());
44  std::string file_content{"text line1\ntext line2\n"};
45  base::FilePath file_name = dir.path().Append("sample.txt");
46  ASSERT_EQ(file_content.size(),
47            static_cast<size_t>(base::WriteFile(
48                file_name, file_content.data(), file_content.size())));
49
50  StreamPtr stream = FileStream::Open(file_name, Stream::AccessMode::READ,
51                                      FileStream::Disposition::OPEN_EXISTING,
52                                      nullptr);
53  ASSERT_NE(nullptr, stream);
54  FileFormField form_field{"test_file",
55                           std::move(stream),
56                           "sample.txt",
57                           content_disposition::kFormData,
58                           mime::text::kPlain,
59                           ""};
60  const char expected_header[] =
61      "Content-Disposition: form-data; name=\"test_file\";"
62      " filename=\"sample.txt\"\r\n"
63      "Content-Type: text/plain\r\n"
64      "\r\n";
65  EXPECT_EQ(expected_header, form_field.GetContentHeader());
66  EXPECT_EQ(file_content, GetFormFieldData(&form_field));
67}
68
69TEST(HttpFormData, MultiPartFormField) {
70  base::ScopedTempDir dir;
71  ASSERT_TRUE(dir.CreateUniqueTempDir());
72  std::string file1{"text line1\ntext line2\n"};
73  base::FilePath filename1 = dir.path().Append("sample.txt");
74  ASSERT_EQ(file1.size(),
75            static_cast<size_t>(
76                base::WriteFile(filename1, file1.data(), file1.size())));
77  std::string file2{"\x01\x02\x03\x04\x05"};
78  base::FilePath filename2 = dir.path().Append("test.bin");
79  ASSERT_EQ(file2.size(),
80            static_cast<size_t>(
81                base::WriteFile(filename2, file2.data(), file2.size())));
82
83  MultiPartFormField form_field{"foo", mime::multipart::kFormData, "Delimiter"};
84  form_field.AddTextField("name", "John Doe");
85  EXPECT_TRUE(form_field.AddFileField("file1",
86                                      filename1,
87                                      content_disposition::kFormData,
88                                      mime::text::kPlain,
89                                      nullptr));
90  EXPECT_TRUE(form_field.AddFileField("file2",
91                                      filename2,
92                                      content_disposition::kFormData,
93                                      mime::application::kOctet_stream,
94                                      nullptr));
95  const char expected_header[] =
96      "Content-Disposition: form-data; name=\"foo\"\r\n"
97      "Content-Type: multipart/form-data; boundary=\"Delimiter\"\r\n"
98      "\r\n";
99  EXPECT_EQ(expected_header, form_field.GetContentHeader());
100  const char expected_data[] =
101      "--Delimiter\r\n"
102      "Content-Disposition: form-data; name=\"name\"\r\n"
103      "\r\n"
104      "John Doe\r\n"
105      "--Delimiter\r\n"
106      "Content-Disposition: form-data; name=\"file1\";"
107      " filename=\"sample.txt\"\r\n"
108      "Content-Type: text/plain\r\n"
109      "Content-Transfer-Encoding: binary\r\n"
110      "\r\n"
111      "text line1\ntext line2\n\r\n"
112      "--Delimiter\r\n"
113      "Content-Disposition: form-data; name=\"file2\";"
114      " filename=\"test.bin\"\r\n"
115      "Content-Type: application/octet-stream\r\n"
116      "Content-Transfer-Encoding: binary\r\n"
117      "\r\n"
118      "\x01\x02\x03\x04\x05\r\n"
119      "--Delimiter--";
120  EXPECT_EQ(expected_data, GetFormFieldData(&form_field));
121}
122
123TEST(HttpFormData, MultiPartBoundary) {
124  const int count = 10;
125  std::set<std::string> boundaries;
126  for (int i = 0; i < count; i++) {
127    MultiPartFormField field{""};
128    std::string boundary = field.GetBoundary();
129    boundaries.insert(boundary);
130    // Our generated boundary must be 16 character long and contain lowercase
131    // hexadecimal digits only.
132    EXPECT_EQ(16u, boundary.size());
133    EXPECT_EQ(std::string::npos,
134              boundary.find_first_not_of("0123456789abcdef"));
135  }
136  // Now make sure the boundary strings were generated at random, so we should
137  // get |count| unique boundary strings. However since the strings are random,
138  // there is a very slim change of generating the same string twice, so
139  // expect at least 90% of unique strings. 90% is picked arbitrarily here.
140  int expected_min_unique = count * 9 / 10;
141  EXPECT_GE(boundaries.size(), expected_min_unique);
142}
143
144TEST(HttpFormData, FormData) {
145  base::ScopedTempDir dir;
146  ASSERT_TRUE(dir.CreateUniqueTempDir());
147  std::string file1{"text line1\ntext line2\n"};
148  base::FilePath filename1 = dir.path().Append("sample.txt");
149  ASSERT_EQ(file1.size(),
150            static_cast<size_t>(
151                base::WriteFile(filename1, file1.data(), file1.size())));
152  std::string file2{"\x01\x02\x03\x04\x05"};
153  base::FilePath filename2 = dir.path().Append("test.bin");
154  ASSERT_EQ(file2.size(),
155            static_cast<size_t>(
156                base::WriteFile(filename2, file2.data(), file2.size())));
157
158  FormData form_data{"boundary1"};
159  form_data.AddTextField("name", "John Doe");
160  std::unique_ptr<MultiPartFormField> files{
161      new MultiPartFormField{"files", "", "boundary2"}};
162  EXPECT_TRUE(files->AddFileField(
163      "", filename1, content_disposition::kFile, mime::text::kPlain, nullptr));
164  EXPECT_TRUE(files->AddFileField("",
165                                  filename2,
166                                  content_disposition::kFile,
167                                  mime::application::kOctet_stream,
168                                  nullptr));
169  form_data.AddCustomField(std::move(files));
170  EXPECT_EQ("multipart/form-data; boundary=\"boundary1\"",
171            form_data.GetContentType());
172
173  StreamPtr stream = form_data.ExtractDataStream();
174  std::vector<uint8_t> data(stream->GetSize());
175  EXPECT_TRUE(stream->ReadAllBlocking(data.data(), data.size(), nullptr));
176  const char expected_data[] =
177      "--boundary1\r\n"
178      "Content-Disposition: form-data; name=\"name\"\r\n"
179      "\r\n"
180      "John Doe\r\n"
181      "--boundary1\r\n"
182      "Content-Disposition: form-data; name=\"files\"\r\n"
183      "Content-Type: multipart/mixed; boundary=\"boundary2\"\r\n"
184      "\r\n"
185      "--boundary2\r\n"
186      "Content-Disposition: file; filename=\"sample.txt\"\r\n"
187      "Content-Type: text/plain\r\n"
188      "Content-Transfer-Encoding: binary\r\n"
189      "\r\n"
190      "text line1\ntext line2\n\r\n"
191      "--boundary2\r\n"
192      "Content-Disposition: file; filename=\"test.bin\"\r\n"
193      "Content-Type: application/octet-stream\r\n"
194      "Content-Transfer-Encoding: binary\r\n"
195      "\r\n"
196      "\x01\x02\x03\x04\x05\r\n"
197      "--boundary2--\r\n"
198      "--boundary1--";
199  EXPECT_EQ(expected_data, (std::string{data.begin(), data.end()}));
200}
201}  // namespace http
202}  // namespace brillo
203