Line data Source code
1 : #include "form.hpp"
2 : #include "../server/mime_types.hpp"
3 : #include "../util/url.hpp"
4 : #include <fstream>
5 : #include <sstream>
6 : #include <random>
7 : #include <algorithm>
8 :
9 : namespace thinger::http {
10 :
11 : namespace {
12 :
13 : // A field name or filename within the quotes of a Content-Disposition, as browsers (and
14 : // curl) write it: '"', CR and LF percent-encoded, so neither can end the parameter or the
15 : // header line (WHATWG HTML, multipart/form-data encoding)
16 40 : std::string escape_quoted(const std::string& value) {
17 40 : std::string result;
18 40 : result.reserve(value.size());
19 358 : for (char c : value) {
20 318 : if (c == '"') result += "%22";
21 310 : else if (c == '\r') result += "%0D";
22 308 : else if (c == '\n') result += "%0A";
23 306 : else result += c;
24 : }
25 40 : return result;
26 0 : }
27 :
28 : }
29 :
30 : // ============================================
31 : // Field methods
32 : // ============================================
33 :
34 36 : form& form::field(std::string name, std::string value) {
35 36 : fields_.emplace_back(std::move(name), std::move(value));
36 36 : return *this;
37 : }
38 :
39 2 : form& form::fields(std::initializer_list<std::pair<std::string, std::string>> pairs) {
40 8 : for (const auto& [name, value] : pairs) {
41 6 : fields_.emplace_back(name, value);
42 : }
43 2 : return *this;
44 : }
45 :
46 : // ============================================
47 : // File methods
48 : // ============================================
49 :
50 0 : form& form::file(std::string name, const std::filesystem::path& path) {
51 0 : return file(std::move(name), path, mime_type(path));
52 : }
53 :
54 0 : form& form::file(std::string name, const std::filesystem::path& path,
55 : std::string content_type) {
56 : // Read file content
57 0 : std::ifstream ifs(path, std::ios::binary);
58 0 : if (!ifs) {
59 0 : return *this;
60 : }
61 :
62 0 : std::ostringstream oss;
63 0 : oss << ifs.rdbuf();
64 :
65 0 : files_.push_back({
66 0 : std::move(name),
67 0 : path.filename().string(),
68 0 : std::move(content_type),
69 : oss.str()
70 : });
71 :
72 0 : return *this;
73 0 : }
74 :
75 14 : form& form::file(std::string name, std::string content,
76 : std::string filename, std::string content_type) {
77 14 : files_.push_back({
78 14 : std::move(name),
79 14 : std::move(filename),
80 14 : std::move(content_type),
81 14 : std::move(content)
82 : });
83 14 : return *this;
84 14 : }
85 :
86 2 : form& form::file(std::string name, const void* data, size_t size,
87 : std::string filename, std::string content_type) {
88 2 : files_.push_back({
89 2 : std::move(name),
90 2 : std::move(filename),
91 2 : std::move(content_type),
92 : std::string(static_cast<const char*>(data), size)
93 : });
94 2 : return *this;
95 6 : }
96 :
97 : // ============================================
98 : // Content-Type and body generation
99 : // ============================================
100 :
101 13 : std::string form::content_type() const {
102 13 : if (is_multipart()) {
103 7 : return "multipart/form-data; boundary=" + get_boundary();
104 : }
105 12 : return "application/x-www-form-urlencoded";
106 : }
107 :
108 21 : std::string form::body() const {
109 21 : if (is_multipart()) {
110 9 : return build_multipart();
111 : }
112 12 : return build_urlencoded();
113 : }
114 :
115 12 : std::string form::build_urlencoded() const {
116 12 : std::string result;
117 34 : for (const auto& [name, value] : fields_) {
118 22 : if (!result.empty()) {
119 10 : result += '&';
120 : }
121 22 : result += url_encode(name) + '=' + url_encode(value);
122 : }
123 12 : return result;
124 0 : }
125 :
126 9 : std::string form::build_multipart() const {
127 9 : const std::string& boundary = get_boundary();
128 9 : std::ostringstream oss;
129 :
130 : // Add text fields
131 25 : for (const auto& [name, value] : fields_) {
132 16 : oss << "--" << boundary << "\r\n";
133 16 : oss << "Content-Disposition: form-data; name=\"" << escape_quoted(name) << "\"\r\n";
134 16 : oss << "\r\n";
135 16 : oss << value << "\r\n";
136 : }
137 :
138 : // Add files
139 21 : for (const auto& file : files_) {
140 12 : oss << "--" << boundary << "\r\n";
141 0 : oss << "Content-Disposition: form-data; name=\"" << escape_quoted(file.name) << "\"; "
142 12 : << "filename=\"" << escape_quoted(file.filename) << "\"\r\n";
143 12 : oss << "Content-Type: " << file.content_type << "\r\n";
144 12 : oss << "\r\n";
145 12 : oss << file.content << "\r\n";
146 : }
147 :
148 : // Closing boundary
149 9 : oss << "--" << boundary << "--\r\n";
150 :
151 18 : return oss.str();
152 9 : }
153 :
154 16 : const std::string& form::get_boundary() const {
155 16 : if (boundary_.empty()) {
156 11 : boundary_ = generate_boundary();
157 : }
158 16 : return boundary_;
159 : }
160 :
161 11 : std::string form::generate_boundary() {
162 : static const char chars[] =
163 : "0123456789"
164 : "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
165 : "abcdefghijklmnopqrstuvwxyz";
166 :
167 11 : std::random_device rd;
168 11 : std::mt19937 gen(rd());
169 11 : std::uniform_int_distribution<> dis(0, sizeof(chars) - 2);
170 :
171 11 : std::string boundary = "----ThingerFormBoundary";
172 187 : for (int i = 0; i < 16; ++i) {
173 176 : boundary += chars[dis(gen)];
174 : }
175 :
176 22 : return boundary;
177 11 : }
178 :
179 : // ============================================
180 : // URL encoding/decoding (delegates to util::url)
181 : // ============================================
182 :
183 58 : std::string form::url_encode(const std::string& str) {
184 : // application/x-www-form-urlencoded uses '+' for space instead of '%20'
185 : static constexpr char hex[] = "0123456789ABCDEF";
186 58 : std::string result;
187 58 : result.reserve(str.size());
188 412 : for (unsigned char c : str) {
189 354 : if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
190 314 : result += static_cast<char>(c);
191 40 : } else if (c == ' ') {
192 4 : result += '+';
193 : } else {
194 36 : result += '%';
195 36 : result += hex[c >> 4];
196 36 : result += hex[c & 0x0F];
197 : }
198 : }
199 58 : return result;
200 0 : }
201 :
202 6 : std::string form::url_decode(const std::string& str) {
203 6 : return util::url::url_decode(str);
204 : }
205 :
206 : // ============================================
207 : // MIME type detection
208 : // ============================================
209 :
210 32 : std::string form::mime_type(const std::filesystem::path& path) {
211 : // The library's MIME type lookup: application/octet-stream if the extension is unknown or
212 : // missing, as curl and browsers send such a file
213 32 : return mime_types::extension_to_type(path.extension().string());
214 : }
215 :
216 : } // namespace thinger::http
|