Logo Pico-Framework A web-first embedded framework for C++
Loading...
Searching...
No Matches
MultipartParser.cpp
Go to the documentation of this file.
1
27#include "framework_config.h" // Must be included before DebugTrace.h to ensure framework_config.h is processed first
28#include "DebugTrace.h"
30
31#include "http/MultipartParser.h"
32#include <lwip/sockets.h>
33#include <iostream>
34#include <cstring>
35#include <sstream>
36#include <fstream>
37#include <algorithm>
38#include <cctype>
39//#include <filesystem>
40#include <FreeRTOS.h>
41#include <task.h>
42
45
47
54
55void MultipartParser::setBoundaryFromContentType(const std::string& contentType){
56 size_t boundaryPos = contentType.find("boundary=");
57 if (boundaryPos != std::string::npos) {
58 boundary = contentType.substr(boundaryPos + 9); // 9 is the length of "boundary="
59 // Remove any leading/trailing whitespace
60 boundary.erase(0, boundary.find_first_not_of(" \t\r\n"));
61 boundary.erase(boundary.find_last_not_of(" \t\r\n") + 1);
62 TRACE("Boundary set to: '%s'\n", boundary.c_str());
63 } else {
64 boundary.clear();
65 TRACE("No boundary found in Content-Type header\n");
66 }
67}
68
70{
71 tcp = req.getTcp(); // extract here — don't store in ctor
72
73 std::string contentType = req.getHeader("Content-Type");
74 setBoundaryFromContentType(contentType);
75
76 if (boundary.empty()) {
77 res.status(400).send("Missing boundary");
78 return false;
79 }
80
81 char buf[1460];
82 int len;
83
85
86 // Handle any body data included with the initial request
87 const std::string &initialBody = req.getBody();
88 if (!initialBody.empty())
89 {
90 std::string chunk = initialBody;
91 if (!handleChunk(chunk))
92 {
93 TRACE("Error handling chunk\n");
94 return false;
95 }
96 }
97
98 // Stream remaining data from socket
99 while ((len = tcp->recv(buf, sizeof(buf) - 1, HTTP_RECEIVE_TIMEOUT)) > 0)
100 {
101 buf[len] = '\0';
102 std::string chunk(buf, len);
103
104 if (!handleChunk(chunk))
105 {
106 sendHttpResponse(400, "Upload incomplete or failed");
107 TRACE("Error handling chunk\n");
108 return false;
109 }
110
111 if (currentState == COMPLETE)
112 break;
113
114 vTaskDelay(pdMS_TO_TICKS(10)); // yield to allow other tasks to run
115 }
116
117 if (currentState != COMPLETE)
118 {
119 sendHttpResponse(400, "Upload incomplete or failed");
120 return false;
121 }
122
123 // Only send 200 when we're truly done
124 TRACE("Multipart: Successfully received file '%s'\n", filename.c_str());
125 std::string filenameOnly = filename.substr(filename.find_last_of('/') + 1);
126 sendHttpResponse(200, filenameOnly);
127 return true;
128}
129
131bool MultipartParser::handleChunk(std::string &chunkData)
132{
133 TRACE("Handling chunk, size: %zu bytes\n", chunkData.size());
134 const std::string boundaryPrefix = "--" + boundary;
135 const std::string finalBoundary = boundaryPrefix + "--";
136
137 while (!chunkData.empty() && currentState != COMPLETE)
138 {
139 switch (currentState)
140 {
142 {
143 size_t boundaryPos = chunkData.find(boundaryPrefix);
144 if (boundaryPos != std::string::npos)
145 {
146 size_t skip = boundaryPos + boundaryPrefix.length();
147 if (chunkData.substr(skip, 2) == "\r\n")
148 skip += 2;
149 chunkData = chunkData.substr(skip);
151 TRACE("Found initial boundary, buffer size now: %zu bytes\n", chunkData.size());
152 continue;
153 }
154 return true; // Wait for more data
155 }
156
157 case FOUND_BOUNDARY:
158 {
159 size_t headersEnd = chunkData.find("\r\n\r\n");
160 if (headersEnd == std::string::npos)
161 return true; // wait for more data
162
163 std::string headers = chunkData.substr(0, headersEnd);
164 std::istringstream stream(headers);
165 std::string line;
166 bool gotFilename = false;
167
168 while (std::getline(stream, line))
169 {
170 line.erase(std::remove(line.begin(), line.end(), '\r'), line.end());
171 if (line.find("Content-Disposition:") != std::string::npos)
172 {
173 gotFilename = extractFilename(line);
174 if (gotFilename)
175 {
176 TRACE("Filename extracted: %s\n", filename.c_str());
177 }
178 }
179 }
180
181 if (!gotFilename)
182 {
183 sendHttpResponse(400, "Invalid upload: no filename or filename exists already");
185 return false;
186 }
187
188 chunkData = chunkData.substr(headersEnd + 4); // skip headers
190 continue;
191 }
192
193 case FOUND_DATA_START:
194 {
195 size_t finalPos = chunkData.find(finalBoundary);
196 size_t nextBoundaryPos = chunkData.find(boundaryPrefix);
197
198 size_t boundaryPos = std::string::npos;
199 bool isFinal = false;
200
201 if (finalPos != std::string::npos &&
202 (nextBoundaryPos == std::string::npos || finalPos < nextBoundaryPos))
203 {
204 boundaryPos = finalPos;
205 isFinal = true;
206 }
207 else if (nextBoundaryPos != std::string::npos)
208 {
209 boundaryPos = nextBoundaryPos;
210 }
211
212 if (boundaryPos == std::string::npos)
213 {
214 // No boundary, stream all data
215 if (processFileData(chunkData))
216 {
217 chunkData.clear();
218 return true;
219 }
220 return false; // Error processing data;
221 }
222
223 // Process data up to boundary
224 size_t dataEnd = boundaryPos;
225 if (dataEnd >= 2 && chunkData.substr(dataEnd - 2, 2) == "\r\n")
226 dataEnd -= 2;
227
228 std::string fileData = chunkData.substr(0, dataEnd);
229 processFileData(fileData);
230
231 // Advance past boundary
232 size_t skip = boundaryPos + boundaryPrefix.length();
233 isFinal = (chunkData.substr(skip, 2) == "--");
234 if (isFinal)
235 skip += 2;
236
237 if (chunkData.substr(skip, 2) == "\r\n")
238 skip += 2;
239
240 chunkData = chunkData.substr(skip);
241
242 TRACE("Final boundary reached — exiting multipart parser\n");
244 break;
245
246 TRACE("Final boundary reached — exiting multipart parser\n");
247 break;
248 }
249
250 default:
251 return false;
252 }
253 }
254
255 return true;
256}
257
259bool MultipartParser::extractFilename(const std::string &contentDisposition)
260{
261 std::size_t start = contentDisposition.find("filename=\"");
262 if (start == std::string::npos)
263 return false;
264
265 start += 10;
266 std::size_t end = contentDisposition.find("\"", start);
267 if (end == std::string::npos)
268 return false;
269 // Ensure upload directory exists
270 auto storage = AppContext::get<StorageManager>();
271 if(!storage) {
272 printf("[MultipartParser] StorageManager service not available\n");
273 sendHttpResponse(500, "StorageManager service not available");
274 return false;
275 }
276 storage->mount(); // ensure mounted
277 std::string uploads(MULTIPART_UPLOAD_PATH);
278 TRACE("Checking if upload directory exists: %s\n", uploads.c_str());
279 if (!storage->exists(uploads)) {
280 TRACE("Upload directory does not exist, creating: %s\n", uploads.c_str());
281 if(!(storage->createDirectory(uploads))) {
282 TRACE("Failed to create upload directory: %s\n", uploads.c_str());
283 sendHttpResponse(500, "Failed to create upload directory");
284 return false;
285 }
286 }
287 filename = std::string(MULTIPART_UPLOAD_PATH) + "/" + contentDisposition.substr(start, end - start);
288 TRACE("checking if filename: %s exists\n", filename.c_str());
290 {
291 printf("[MultipartParser] File already exists: %s\n", filename.c_str());
292 return false;
293 }
294 return true;
295}
296
298bool MultipartParser::processFileData(const std::string &fileData)
299{
300 TRACE("Processing file data, size: %zu bytes\n", fileData.size());
301 auto *storage = AppContext::get<StorageManager>();
302 if (!storage->appendToFile(filename, (uint8_t *)fileData.c_str(), fileData.size()))
303 {
304 if (!storage->isMounted())
305 {
306 TRACE("SD card not mounted — cannot handle multipart upload\n");
307 sendHttpResponse(500, "SD card not mounted");
308 }
309 else
310 {
311 TRACE("Failed to append file during multipart upload\n");
312 sendHttpResponse(500, "Failed to write file data");
313 }
314 return false;
315 }
316 return true;
317}
318
320int MultipartParser::file_exists(const std::string& filename)
321{
323 TRACE("[MultipartParser] Checking if file exists: %s\n", filename.c_str());
324 return storage->exists(filename);
325}
326
328int MultipartParser::findDataStart(std::string &currentData)
329{
330 std::size_t pos = currentData.find("\r\n\r\n");
331 return (pos != std::string::npos) ? static_cast<int>(pos + 4) : -1;
332}
333
335bool MultipartParser::handleFinalBoundary(std::string &fileData)
336{
337 std::size_t finalBoundaryPos = fileData.find(boundary + "--");
338 if (finalBoundaryPos != std::string::npos)
339 {
340 fileData = fileData.substr(0, finalBoundaryPos);
341 return true;
342 }
343 return false;
344}
345
347void MultipartParser::sendHttpResponse(int statusCode, const std::string &messageOrFilename) {
348 std::ostringstream oss;
349 std::string body;
350
351 if (statusCode >= 200 && statusCode < 300) {
352 // Success: treat message as filename
353 body = R"({"success":true,"image":")" + messageOrFilename + R"("})";
354 } else {
355 std::string code = std::to_string(statusCode);
356 body = R"({"success":false,"error":{"code":")" + code + R"(","message":")" + messageOrFilename + R"("}})";
357 }
358
359 oss << "HTTP/1.1 " << statusCode << (statusCode == 200 ? " OK" : " Bad Request") << "\r\n"
360 << "Content-Type: application/json\r\n"
361 << "Content-Length: " << body.length() << "\r\n"
362 << "Connection: close\r\n\r\n"
363 << body;
364
365 std::string response = oss.str();
366 if (!tcp || !tcp->isConnected()) {
367 panic("Attempted to send on invalid socket");
368 }
369
370 tcp->send(response.c_str(), response.length());
371 vTaskDelay(pdMS_TO_TICKS(50));
372}
Macro-based debug trace system with optional SD file logging.
#define TRACE_INIT(MODULE_NAME)
Declare trace usage in a source file for a given module.
Definition DebugTrace.h:170
#define TRACE(...)
Default trace (INFO level).
Definition DebugTrace.h:187
@ COMPLETE
@ FOUND_BOUNDARY
@ FOUND_DATA_START
@ SEARCHING_FOR_BOUNDARY
Abstract interface for file and directory storage backends.
static constexpr std::uintptr_t getTypeKey()
Definition AppContext.h:91
Forward declaration for potential routing needs.
Definition HttpRequest.h:32
const std::string & getBody() const
Get the request body (copy).
std::string getHeader(const std::string &field) const
Get a specific header field (case-insensitive).
Tcp * getTcp() const
Represents an HTTP response object.
void send(const std::string &body)
Send a full response including headers and body.
HttpResponse & status(int code)
Set the HTTP status code.
Parses and processes multipart/form-data uploads over HTTP.
std::string filename
Tcp * tcp
The client TCP connection.
void sendHttpResponse(int statusCode, const std::string &message)
Send a simple HTTP response to the client.
bool processFileData(const std::string &fileData)
Append a chunk of file data to the output file.
void setBoundaryFromContentType(const std::string &contentType)
sets the boundary from the Content-Type header.
MultipartParser()
Construct a new Multipart Parser object.
bool handleFinalBoundary(std::string &fileData)
Detect and handle the final multipart boundary.
int findDataStart(std::string &currentData)
Find the start of the file content in a chunk (after headers).
std::string boundary
bool handleChunk(std::string &chunkData)
Handle an incoming chunk of multipart data.
bool handleMultipart(HttpRequest &req, HttpResponse &res)
Begin processing the multipart upload from the socket.
int file_exists(const std::string &filename)
Check if a file already exists in storage.
static State currentState
bool extractFilename(const std::string &contentDisposition)
Extract the filename from a Content-Disposition header.
Abstract base class for storage access and file operations.
virtual bool exists(const std::string &path)=0
Check whether a file or directory exists at the given path.
bool isConnected() const
Check if the socket is connected.
Definition Tcp.h:119
int send(const char *buffer, size_t size)
Send data over the connection.
Definition Tcp.cpp:279
int recv(char *buffer, size_t size, uint32_t timeout_ms)
Receive data from the connection.
Definition Tcp.cpp:389
Delegates to user or system configuration.
#define MULTIPART_UPLOAD_PATH
This setting defines where uploads go The default is "/uploads" This is used by the MultipartParser c...
#define HTTP_RECEIVE_TIMEOUT
Timeout for receiving HTTP data in milliseconds.