Repository navigation
Closed
Labels
bugConfirmed bugs or reports that are very likely to be bugs.Confirmed bugs or reports that are very likely to be bugs.
Description
Prerequisites
- [X ] Put an X between the brackets on this line if you have checked that your issue isn't already filed: https://github.com/search?l=&q=repo%3Aetr%2Flibhttpserver&type=Issues
Description
When uploading a file as part of a multipart form, the Content-Type, and filename are not accessible from the http_request object. I have tried get_args(), get_headers(), and get_footers(). However if i dump the body with get_content(), I can see the fields in the body, but they are not hoisted into higher level objects.
Example Code
#include <iostream>
#include <algorithm>
#include <httpserver.hpp>
using namespace httpserver;
class file_resource : public http_resource {
public :
const std::shared_ptr<http_response> render_POST(const http_request& req) {
//Loop through the headers map and output them
const auto& headers = req.get_headers();
std::for_each(headers.begin(), headers.end(), [](auto& p){
std::cout << p.first << " -> " << p.second << std::endl;
});
//Loop through the args map and output them
const auto& args = req.get_args();
std::for_each(args.begin(), args.end(), [](auto& p) {
std::cout << p.first << " -> " << p.second << std::endl;
});
std::cout << "Body: " << req.get_content() << std::endl;
return std::shared_ptr<http_response>(new string_response("OK"));
}
};
void custom_access_log(const std::string& url) {
std::cout << "Request to: " << url << std::endl;
}
void custom_error_log(const std::string& error) {
std::cout << "Error: " << error << std::endl;
}
int main() {
webserver ws = create_webserver(8080)
.log_access(custom_access_log)
.log_error(custom_error_log);
file_resource fr;
ws.register_resource("/file", &fr);
ws.start(true);
return 0;
}Steps to Reproduce
- Compile example code above
curl -X POST -F upload=@<some_file>.jpg localhost:8080/file- the "upload" argument will have the file data, but the Content-Type, and original filename are not accessible
- Looking at the output, you can see the get_contents() string contains the Content-Type and filename
Method: POST, Body: --------------------------ea9b4d3e74b2950b
Content-Disposition: form-data; name="upload"; filename="suite_and_tie.jpg"
Content-Type: image/jpeg
Expected behavior:
Some way to access the filename and Content-Type of the file
Actual behavior:
No way to access it easily
Reproduces how often: 100%
Reacted by Alexander Dahl
Metadata
Metadata
Assignees
Labels
bugConfirmed bugs or reports that are very likely to be bugs.Confirmed bugs or reports that are very likely to be bugs.
Activity
jmslocum commentedon Apr 10, 2021
I was able to work around the issue by parsing the file info from the body itself.
Actually, I guess this is more of a feature request.
LeSpocky commentedon Nov 12, 2021
I had a look into libhttpserver's code for this, basically because we need a way to upload files and store them (temporarily) on the server. libmicrohttpd assists here with these:
I think libhttpserver does the right thing by hooking into it, but it uses the same post processor for mime-types "application/x-www-form-urlencoded" and "multipart/form-data". I guess it works for the first case (a usual web form without file upload) and does not for the second (a web form with file upload) because a completely different parser would be required.
This could be fixed for libhttpserver by providing different post iterator methods per Content-Type. In a first step this would allow for extracting args from the post data, given someone comes up with a parser for multipart mime, leaving the question what to do with the file content aside. A proposal by @etr for that file handling would be nice then.
LeSpocky commentedon Nov 12, 2021
I was wrong. The key is (call this misleading or "not covered" by libmicrohttpd docs or my misunderstanding) this function prototype:
In src/webserver.cpp libhttpserver ignores
kind,filename,content_type,transfer_encoding, andoff.This is not needed, libmicrohttpd has a state machine based parser for "multipart/form-data", libhttpserver just does not make use of the additional fields set by libmicrohttpd, if the part is a file. I will look deeper again. Sorry for the noise.
etr commentedon Nov 12, 2021
You are correct though; the issue is that libhttpserver currently ignores those fields and pulls the whole content in a single blurb. The post iterator method needs to be modified to use those parameters appropriately and store the content of each file separately.
LeSpocky commentedon Nov 17, 2021
After reading and trying 7 Improved processing of POST data from the libmicrohttpd website and reviewing src/webserver.cpp again, I have a better picture now. My motivation is still to support uploading (potentially big) files through multipart/form-data and I spotted the following flaws in libhttpserver:
webserver::content_size_limit, otherwise my POST data might be truncated earlyhttp_request::grow_content()is always called and the whole body of the message is put intostd::string http_request::content, although some parts of the body may contain binary data. That means the whole body is put into applications memory which will make embedded targets with low memory (32 MiB here) crash, probably (did not try this yet)webserver::post_process_enabled(default set to true) the file data is put into memory a second time withhttp_request::set_arg()inwebserver::post_iterator(). However disabling the post processor is not smart, because you drop the whole multipart/form-data parser of libmicrohttpd then.Proposal to solve this:
http_request::grow_content()at all if a post processor is setThe last point is the way to solve this issue I guess? 😉
WIP: Draft new file upload handling
WIP: Draft new file upload handling
etr commentedon Nov 17, 2021
A few comments:
Makes sense to me. I think that using kind and content_type should basically tell us if the part needs to be processed by the post processor.
Bulking both these (as I think they are basically the same). I don't think this is necessary correct. We had use cases where we wanted to both pull the content and also set the content into memory. I think it is really a matter of leaving the choice to who uses the API. In this case, I would provide a rational default (or set of defaults) for the behavior, and then allow the consumers to customize it. Ideally, we would have a "content_sink" object that, for example, by default puts the content to memory (and/or another that pushes to file), and provide an interface to the consumers to create their own content_sink to manage the data differently. I think it would be reasonable to have three pre-baked implementations of content_sink, one that discards the data, one that puts the data to memory (some sort of map) and another one that puts it into files.
Whatever mechanism we use above, the grow_content will have to take into account the upload of distinct parts and store them separately for easy consumption.
LeSpocky commentedon Nov 18, 2021
Maybe let me rephrase to make my thoughts on the second point more clear: what would be the usecase for first storing the whole content unprocessed and additionally post process it in libhttpserver and store the parts a second time? Why storing the unprocessed content if the content is post processed and consumed anyway? 🤔
etr commentedon Nov 23, 2021
The practical use-case that this supported, at the time of writing the library, was a raw log (of the request). The processed content was still used by the application itself. To be fair, the "get_content" method existed with a semantic that meant "it returns the whole body of the request" before the get_args method started returning processed content from POST requests. I think that's beyond the point though, as the most important point is one of clarity/expectations to me.
From the clients point of view, for the "get_content" method, the simplest expectation is for it to return the body of the request. If we decide that it doesn't contain processed content, the expectations around the "get_content" method become quite weird; as in: "it will return you the content of the body request except for the parts that have been processed." I understand the efficiency argument, but I'd rather go for clarity over efficiency.
The argument would make more sense to me if we were proposing to eliminate the "get_content" method altogether, but we cannot really do that as it has clear use cases of its own.
ctomahogh commentedon Feb 1, 2022
As we are currently facing the very same problem (file upload via libhttpserver is quite slow and consumes much RAM), I would like to ask about the status of this issue.
I looked into the proposal for a solution and gave it a test. So far it works good, but it is obiously not finished yet.
As we would need a better way for file upload quite quickly (within the next 4 weeks) I would like to contribute and help with an implementation. Or improve/finalize the proposed patch.
I don't expect a solution to be approved and integrated in the official libhttpserver release within this time. But I want to avoid using an own patch (which I have to make nevertheless for the first moment) forever. So I would like to develop a patch, which is likely to be integrated into the official libhttserver eventually.
To avoid going into the wrong direction, it would be great to have some comments on the following, before I submit a possible patch:
webserver::post_iteratorto create the actual filepathThis check could be done via the
realpath()function, which resolves the pathname to its real location. The real location could then be checked, whether it would still be in the specified upload directory. If not, the upload should be rejected.mkstemp()) would be generated. This would prevent issues, if the same file would be uploaded twice in different threads. Also this would be safer for users, who directly put such filenames into command line calls (system()). This could in the worst case lead to command injections, SQL injections, etc. Yes, the security of the actual application is not up to the libhttpserver, but it would be (in my opinion) a nice to have.Regarding the get_content vs post_processor discussion:
As far as I understand the libmicrohttpd, the post processor can only be used for content of type
multipart/form-dataandapplication/x-www-form-urlencoded. For other content the libmicrohttpd won't create a post_processor. So to get the body of other content (i.e.application/json) you would still need theget_contentmethod. Removing the get_content completely might not be possible. Or at least another solution would be needed to retrieve the body of such calls.So I would propose the following
omit_processed_data_in_content. If active, this would do what is already in the proposal: If the content allows it, process it via post_processor. If not, put it into the content. If inactive the libhttpserver would act as it did until now: Stuff everything into the content and post_process the data additionally (according to the content type). The default setting would be disabled, so leaving the behaviour unchanged for current users of the lib.post_upload_dir. If the upload directory is not set, behave as now: put the content of uploaded files into the map. If the upload directory is given, create a unique filename in the given path and put the content directly into this file (with the additions mentioned above) and only put the filename into the map.Only thing, which I am not sure about:
If the filename would be a unique (random) name in the upload directory, there would be the need for two entries in the map (per file)
So the original key should be the unique name, but there would have to be a second one.
Whatever the name of this key might be, having some hardcoded key to be injected into the map could result in issues, if the same key would be used by an application.
So maybe this information (about uploaded files) should be put into a new map, which may hold the information about unique filename and original filename (and maybe additional information like file size).
Something like:
LeSpocky commentedon Feb 1, 2022
@ctomahogh I know what I proposed in #246 is somewhat incomplete, and I did not invest time to come up with a solution going in the direction outlined by @etr. Sorry about that, but I don't think I have time for that in the near future. I think I could have a look at other ideas and maybe test those however.
Let me add one more thing: you are right, the filename is potentially malicious user input. What I did in my app though was sanitizing the filename instead of generating a random one, and I followed the suggestions from https://wiki.sei.cmu.edu/confluence/display/c/STR02-C.+Sanitize+data+passed+to+complex+subsystems
The function I came up with for that, looks like this:
This might produced collisions on those sanitized filenames however, hope that helps anyways. 🙃
etr commentedon Feb 2, 2022
Thanks a lot for all the thinking you have put into this. I understand you have a urgency on this as well, so I'd be happy to help you iterate fast on reviews.
I would welcome changes from you if you are working already on a solution.
You are correct. I think that storing to disk shouldn't be the default behavior though. I am happy for the library to provide that as an option (as you suggest later in your post). For example, by having the upload path specified on server creation.
I think that a better solution is what you suggest later in your post. Generating a random filename that the library controls sounds like a better solution to me and should make this check redundant. I think this behavior should be optional (i.e. by adding a "generate_random_filename_on_upload" option) - if a library user wants to maintain the original filename they should be able to do so.
I think this is a good approach in general - see my answer to the previous point.
I am good with this.
Ideally I'd separate the option to specify the location from the option that activates the behavior. The option to activate the behavior should have 3 states: memory_only (current behavior), disk_only (stores on disk only), disk_and_memory (fills both the content in memory and on disk). if disk_only or disk_and_memory are provided, the location should be required.
Might need to see how it comes up in code, but the overall approach of the map seems sound to me. To simplify things, whatever value of the option "generate_random_filename_on_upload" we should just maintain the same map structure (if it is false, key and value would just simply be the same).
etr commentedon Mar 4, 2022
Closing this issue given it should be addressed by the latest change on head. You should expect a released version soon containing this change.