mirror of
https://github.com/Relintai/pandemonium_engine.git
synced 2024-12-27 06:07:14 +01:00
Added proper multi part form parsing support for the simple web server.
This commit is contained in:
parent
5489ace788
commit
b0da59764d
@ -13,6 +13,7 @@ sources = [
|
|||||||
"http_parser.cpp",
|
"http_parser.cpp",
|
||||||
"http_writer.cpp",
|
"http_writer.cpp",
|
||||||
"http_parser/http_parser.c",
|
"http_parser/http_parser.c",
|
||||||
|
"multipart_parser_c/multipart_parser.c",
|
||||||
]
|
]
|
||||||
|
|
||||||
if ARGUMENTS.get('custom_modules_shared', 'no') == 'yes':
|
if ARGUMENTS.get('custom_modules_shared', 'no') == 'yes':
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
#include "http_parser.h"
|
#include "http_parser.h"
|
||||||
|
|
||||||
#include "modules/web/http/web_server_request.h"
|
|
||||||
#include "./http_parser/http_parser.h"
|
#include "./http_parser/http_parser.h"
|
||||||
|
#include "./multipart_parser_c/multipart_parser.h"
|
||||||
|
|
||||||
|
#include "modules/web/http/web_server_request.h"
|
||||||
|
|
||||||
#include "simple_web_server_request.h"
|
#include "simple_web_server_request.h"
|
||||||
|
|
||||||
@ -45,8 +47,6 @@ int HTTPParser::read_from_buffer(const char *p_buffer, const int p_data_length)
|
|||||||
HTTPParser::HTTPParser() {
|
HTTPParser::HTTPParser() {
|
||||||
_is_ready = false;
|
_is_ready = false;
|
||||||
_content_type = REQUEST_CONTENT_URLENCODED;
|
_content_type = REQUEST_CONTENT_URLENCODED;
|
||||||
_in_multipart_boundary = false;
|
|
||||||
_in_boundary_header = false;
|
|
||||||
_multipart_form_is_file = false;
|
_multipart_form_is_file = false;
|
||||||
|
|
||||||
settings = memnew(http_parser_settings);
|
settings = memnew(http_parser_settings);
|
||||||
@ -65,8 +65,19 @@ HTTPParser::HTTPParser() {
|
|||||||
//parser = malloc(sizeof(http_parser));
|
//parser = malloc(sizeof(http_parser));
|
||||||
parser = memnew(http_parser);
|
parser = memnew(http_parser);
|
||||||
http_parser_init(parser, HTTP_REQUEST);
|
http_parser_init(parser, HTTP_REQUEST);
|
||||||
|
|
||||||
parser->data = this;
|
parser->data = this;
|
||||||
|
|
||||||
|
_multipart_parser_settings = memnew(multipart_parser_settings);
|
||||||
|
|
||||||
|
_multipart_parser_settings->on_header_field = _on_multipart_header_field_cb;
|
||||||
|
_multipart_parser_settings->on_header_value = _on_multipart_header_value_cb;
|
||||||
|
_multipart_parser_settings->on_part_data = _on_multipart_part_data_cb;
|
||||||
|
_multipart_parser_settings->on_part_data_begin = _on_multipart_part_data_begin_cb;
|
||||||
|
_multipart_parser_settings->on_headers_complete = _on_multipart_headers_complete_cb;
|
||||||
|
_multipart_parser_settings->on_part_data_end = _on_multipart_part_data_end_cb;
|
||||||
|
_multipart_parser_settings->on_body_end = _on_multipart_body_end_cb;
|
||||||
|
|
||||||
|
_multipart_parser = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
HTTPParser::~HTTPParser() {
|
HTTPParser::~HTTPParser() {
|
||||||
@ -91,126 +102,14 @@ String HTTPParser::chr_len_to_str(const char *at, size_t length) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
void HTTPParser::HTTPParser::process_multipart_data() {
|
int HTTPParser::HTTPParser::process_multipart_data(const char *at, size_t p_length) {
|
||||||
int iter = 0;
|
ERR_FAIL_COND_V(!_multipart_parser, p_length);
|
||||||
//process one element per loop
|
|
||||||
while (true) {
|
|
||||||
//first boundary -> ignore, with everything before it
|
|
||||||
if (!_in_multipart_boundary) {
|
|
||||||
int boundary_index = _partial_data.find(_multipart_boundary);
|
|
||||||
|
|
||||||
if (boundary_index == -1) {
|
return multipart_parser_execute(_multipart_parser, at, p_length);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
boundary_index += _multipart_boundary.size();
|
|
||||||
|
|
||||||
_partial_data = _partial_data.substr(boundary_index);
|
|
||||||
_in_multipart_boundary = true;
|
|
||||||
_in_boundary_header = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
//find the first \n\n -> process boundary_header
|
|
||||||
//cut it out from the string.
|
|
||||||
if (_in_boundary_header) {
|
|
||||||
int header_end_index = _partial_data.find("\r\n\r\n");
|
|
||||||
|
|
||||||
if (header_end_index != -1) {
|
|
||||||
String header = _partial_data.substr_index(0, header_end_index);
|
|
||||||
_partial_data = _partial_data.substr(header_end_index + 4);
|
|
||||||
|
|
||||||
header = header.strip_edges();
|
|
||||||
|
|
||||||
//ERR_PRINT("HEADER");
|
|
||||||
//ERR_PRINT(header);
|
|
||||||
|
|
||||||
_process_multipart_header(header);
|
|
||||||
_in_boundary_header = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Boundary header has not yet fully arrived, return
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Multipart body
|
|
||||||
int boundary_index = _partial_data.find(_multipart_boundary);
|
|
||||||
|
|
||||||
if (boundary_index == -1) {
|
|
||||||
//TODO
|
|
||||||
//if file-> append everything to the HTTPTempFile, except the last boundary.size() - 1 characters from the string.
|
|
||||||
//should probably only happen after a while to save on memory use like maybe a meg or two (should be configurable)
|
|
||||||
// Should probably also be configurable whether it happens or not at all
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//ERR_PRINT("BODY");
|
|
||||||
String data = _partial_data.substr_index(0, boundary_index - 4); //to strip the 2 \r\n from before the boundary
|
|
||||||
|
|
||||||
//ERR_PRINT(data);
|
|
||||||
|
|
||||||
if (_multipart_form_is_file) {
|
|
||||||
if (data == "") {
|
|
||||||
_in_boundary_header = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
CharString cs = data.ascii();
|
|
||||||
|
|
||||||
PoolByteArray file_data;
|
|
||||||
file_data.resize(cs.length());
|
|
||||||
PoolByteArray::Write w = file_data.write();
|
|
||||||
|
|
||||||
for (int i = 0; i < cs.length(); i++) {
|
|
||||||
w[i] = cs[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
w.release();
|
|
||||||
|
|
||||||
_request->add_file(_multipart_form_name, _multipart_form_filename, file_data);
|
|
||||||
} else {
|
|
||||||
_request->add_parameter(_multipart_form_name, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
boundary_index += _multipart_boundary.size();
|
|
||||||
_partial_data = _partial_data.substr(boundary_index);
|
|
||||||
|
|
||||||
if (_partial_data.begins_with("--")) {
|
|
||||||
//done
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_in_boundary_header = true;
|
|
||||||
|
|
||||||
//Safety for now
|
|
||||||
++iter;
|
|
||||||
ERR_FAIL_COND(iter == 10000);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void HTTPParser::_process_multipart_header(const String &header) {
|
void HTTPParser::_process_multipart_header_value(const String &val) {
|
||||||
_multipart_form_name = "";
|
if (_queued_multipart_header_field == "Content-Disposition") {
|
||||||
_multipart_form_filename = "";
|
|
||||||
_multipart_form_content_type = "";
|
|
||||||
_multipart_form_is_file = false;
|
|
||||||
|
|
||||||
int nlc = header.get_slice_count("\r\n");
|
|
||||||
|
|
||||||
for (int i = 0; i < nlc; ++i) {
|
|
||||||
String l = header.get_slice("\r\n", i);
|
|
||||||
|
|
||||||
int sc = l.get_slice_count(":");
|
|
||||||
|
|
||||||
if (sc != 2) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String key = l.get_slicec(':', 0);
|
|
||||||
String val = l.get_slicec(':', 1);
|
|
||||||
|
|
||||||
if (key == "Content-Disposition") {
|
|
||||||
int c = val.get_slice_count(";");
|
int c = val.get_slice_count(";");
|
||||||
|
|
||||||
for (int j = 0; j < c; ++j) {
|
for (int j = 0; j < c; ++j) {
|
||||||
@ -231,6 +130,7 @@ void HTTPParser::_process_multipart_header(const String &header) {
|
|||||||
}
|
}
|
||||||
} else if (kk == "filename") {
|
} else if (kk == "filename") {
|
||||||
_multipart_form_filename = vs.get_slicec('=', 1);
|
_multipart_form_filename = vs.get_slicec('=', 1);
|
||||||
|
_multipart_form_filename = _multipart_form_filename.replace("\"", "");
|
||||||
_multipart_form_is_file = true;
|
_multipart_form_is_file = true;
|
||||||
|
|
||||||
if (_multipart_form_name.length() >= 2 && _multipart_form_name.begins_with("\"") && _multipart_form_name.ends_with("\"")) {
|
if (_multipart_form_name.length() >= 2 && _multipart_form_name.begins_with("\"") && _multipart_form_name.ends_with("\"")) {
|
||||||
@ -240,12 +140,13 @@ void HTTPParser::_process_multipart_header(const String &header) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (key == "Content-Type") {
|
} else if (_queued_multipart_header_field == "Content-Type") {
|
||||||
_multipart_form_content_type = val;
|
_multipart_form_content_type = val;
|
||||||
} else {
|
} else {
|
||||||
//Shouldn't happen, should probably close connection
|
//Shouldn't happen, should probably close connection
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
_queued_multipart_header_field = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
void HTTPParser::process_urlenc_data() {
|
void HTTPParser::process_urlenc_data() {
|
||||||
@ -282,6 +183,10 @@ void HTTPParser::process_urlenc_data() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool HTTPParser::is_boundary_at(const char *at, size_t length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
#define MESSAGE_DEBUG 0
|
#define MESSAGE_DEBUG 0
|
||||||
|
|
||||||
int HTTPParser::on_message_begin() {
|
int HTTPParser::on_message_begin() {
|
||||||
@ -291,10 +196,6 @@ int HTTPParser::on_message_begin() {
|
|||||||
|
|
||||||
_in_header = true;
|
_in_header = true;
|
||||||
_content_type = REQUEST_CONTENT_URLENCODED;
|
_content_type = REQUEST_CONTENT_URLENCODED;
|
||||||
_multipart_boundary = "";
|
|
||||||
_in_multipart_boundary = false;
|
|
||||||
_in_multipart_boundary = false;
|
|
||||||
_in_boundary_header = false;
|
|
||||||
_multipart_form_is_file = false;
|
_multipart_form_is_file = false;
|
||||||
|
|
||||||
_request.instance();
|
_request.instance();
|
||||||
@ -401,15 +302,15 @@ int HTTPParser::on_header_value(const char *at, size_t length) {
|
|||||||
|
|
||||||
bs += 9; //skip ahead to the end of "boundary="
|
bs += 9; //skip ahead to the end of "boundary="
|
||||||
|
|
||||||
_multipart_boundary = s.substr(bs);
|
_multipart_boundary = "--" + s.substr(bs).strip_edges();
|
||||||
_multipart_boundary = _multipart_boundary.strip_edges();
|
//_multipart_boundary = _multipart_boundary.strip_edges();
|
||||||
|
|
||||||
//TODO can be inside quoted
|
//TODO can be inside quoted
|
||||||
//Append -- if it doesn't have it already
|
//Append -- if it doesn't have it already
|
||||||
//It shouldn't be longer that 70 chars
|
//It shouldn't be longer that 70 chars
|
||||||
//The CRLF preceeding could also be appended for simpler logic
|
//The CRLF preceeding could also be appended for simpler logic
|
||||||
|
|
||||||
if (_multipart_boundary == "") {
|
if (_multipart_boundary.empty()) {
|
||||||
//Error! TODO set an error variable and close the connection
|
//Error! TODO set an error variable and close the connection
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -447,6 +348,16 @@ int HTTPParser::on_headers_complete() {
|
|||||||
|
|
||||||
//Check content length, and send error if bigger than server limit (add)
|
//Check content length, and send error if bigger than server limit (add)
|
||||||
|
|
||||||
|
if (_content_type == REQUEST_CONTENT_MULTIPART_FORM_DATA) {
|
||||||
|
if (_multipart_parser) {
|
||||||
|
multipart_parser_free(_multipart_parser);
|
||||||
|
_multipart_parser = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
_multipart_parser = multipart_parser_init(_multipart_boundary.ascii().get_data(), _multipart_parser_settings);
|
||||||
|
multipart_parser_set_data(_multipart_parser, this);
|
||||||
|
}
|
||||||
|
|
||||||
_in_header = false;
|
_in_header = false;
|
||||||
_partial_data = "";
|
_partial_data = "";
|
||||||
|
|
||||||
@ -455,6 +366,26 @@ int HTTPParser::on_headers_complete() {
|
|||||||
int HTTPParser::on_body(const char *at, size_t length) {
|
int HTTPParser::on_body(const char *at, size_t length) {
|
||||||
ERR_FAIL_COND_V(!_request.is_valid(), 0);
|
ERR_FAIL_COND_V(!_request.is_valid(), 0);
|
||||||
|
|
||||||
|
if (_content_type == REQUEST_CONTENT_MULTIPART_FORM_DATA) {
|
||||||
|
int wofs = _queued_multipart_form_data.size();
|
||||||
|
_queued_multipart_form_data.resize(_queued_multipart_form_data.size() + length);
|
||||||
|
char *w = _queued_multipart_form_data.ptrw();
|
||||||
|
for (int i = 0; i < length; ++i) {
|
||||||
|
w[wofs++] = at[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
int processed = process_multipart_data(_queued_multipart_form_data.ptr(), _queued_multipart_form_data.size());
|
||||||
|
int size = _queued_multipart_form_data.size();
|
||||||
|
wofs = 0;
|
||||||
|
for (int i = processed; i < size; ++i) {
|
||||||
|
w[wofs++] = w[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
_queued_multipart_form_data.resize(_queued_multipart_form_data.size() - processed);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
String s = chr_len_to_str(at, length);
|
String s = chr_len_to_str(at, length);
|
||||||
|
|
||||||
#if MESSAGE_DEBUG
|
#if MESSAGE_DEBUG
|
||||||
@ -463,10 +394,6 @@ int HTTPParser::on_body(const char *at, size_t length) {
|
|||||||
|
|
||||||
_partial_data += s;
|
_partial_data += s;
|
||||||
|
|
||||||
if (_content_type == REQUEST_CONTENT_MULTIPART_FORM_DATA) {
|
|
||||||
process_multipart_data();
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -488,6 +415,19 @@ int HTTPParser::on_message_complete() {
|
|||||||
_requests.push_back(_request);
|
_requests.push_back(_request);
|
||||||
_request.unref();
|
_request.unref();
|
||||||
|
|
||||||
|
if (_multipart_parser) {
|
||||||
|
multipart_parser_free(_multipart_parser);
|
||||||
|
_multipart_parser = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
_multipart_boundary = "";
|
||||||
|
|
||||||
|
_queued_multipart_header_field = "";
|
||||||
|
|
||||||
|
_multipart_form_name = "";
|
||||||
|
_multipart_form_filename = "";
|
||||||
|
_multipart_form_content_type = "";
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
int HTTPParser::on_chunk_header() {
|
int HTTPParser::on_chunk_header() {
|
||||||
@ -549,3 +489,119 @@ int HTTPParser::_on_chunk_complete_cb(http_parser *parser) {
|
|||||||
HTTPParser *p = reinterpret_cast<HTTPParser *>(parser->data);
|
HTTPParser *p = reinterpret_cast<HTTPParser *>(parser->data);
|
||||||
return p->on_chunk_complete();
|
return p->on_chunk_complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define MULTIPART_MESSAGE_DEBUG 0
|
||||||
|
|
||||||
|
int HTTPParser::on_multipart_header_field_cb(const char *at, size_t length) {
|
||||||
|
String s = chr_len_to_str(at, length);
|
||||||
|
|
||||||
|
_queued_multipart_header_field = s;
|
||||||
|
|
||||||
|
#if MULTIPART_MESSAGE_DEBUG
|
||||||
|
ERR_PRINT("on_multipart_header_field_cb " + s);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int HTTPParser::on_multipart_header_value_cb(const char *at, size_t length) {
|
||||||
|
String s = chr_len_to_str(at, length);
|
||||||
|
|
||||||
|
_process_multipart_header_value(s);
|
||||||
|
|
||||||
|
#if MULTIPART_MESSAGE_DEBUG
|
||||||
|
ERR_PRINT("on_multipart_header_value_cb " + s);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int HTTPParser::on_multipart_part_data_cb(const char *at, size_t length) {
|
||||||
|
#if MULTIPART_MESSAGE_DEBUG
|
||||||
|
ERR_PRINT("on_multipart_part_data_cb");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
int l = static_cast<int>(length);
|
||||||
|
int mfdofs = _multipart_form_data.size();
|
||||||
|
_multipart_form_data.resize(mfdofs + length);
|
||||||
|
char *w = _multipart_form_data.ptrw();
|
||||||
|
for (int i = 0; i < l; ++i) {
|
||||||
|
w[mfdofs++] = at[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int HTTPParser::on_multipart_part_data_begin_cb() {
|
||||||
|
#if MULTIPART_MESSAGE_DEBUG
|
||||||
|
ERR_PRINT("on_multipart_part_data_begin_cb");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int HTTPParser::on_multipart_headers_complete_cb() {
|
||||||
|
#if MULTIPART_MESSAGE_DEBUG
|
||||||
|
ERR_PRINT("on_multipart_headers_complete_cb");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int HTTPParser::on_multipart_part_data_end_cb() {
|
||||||
|
#if MULTIPART_MESSAGE_DEBUG
|
||||||
|
ERR_PRINT("on_multipart_part_data_end_cb");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (_multipart_form_is_file) {
|
||||||
|
PoolByteArray file_data;
|
||||||
|
int len = _multipart_form_data.size();
|
||||||
|
file_data.resize(len);
|
||||||
|
PoolByteArray::Write w = file_data.write();
|
||||||
|
const char *r = _multipart_form_data.ptr();
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
w[i] = r[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
w.release();
|
||||||
|
|
||||||
|
_request->add_file(_multipart_form_name, _multipart_form_filename, file_data);
|
||||||
|
} else {
|
||||||
|
_request->add_parameter(_multipart_form_name, String(_multipart_form_data.ptr()));
|
||||||
|
}
|
||||||
|
|
||||||
|
_multipart_form_is_file = false;
|
||||||
|
_multipart_form_data.clear();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int HTTPParser::on_multipart_body_end_cb() {
|
||||||
|
#if MULTIPART_MESSAGE_DEBUG
|
||||||
|
ERR_PRINT("on_multipart_body_end_cb");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int HTTPParser::_on_multipart_header_field_cb(multipart_parser *parser, const char *at, size_t length) {
|
||||||
|
HTTPParser *p = reinterpret_cast<HTTPParser *>(multipart_parser_get_data(parser));
|
||||||
|
return p->on_multipart_header_field_cb(at, length);
|
||||||
|
}
|
||||||
|
int HTTPParser::_on_multipart_header_value_cb(multipart_parser *parser, const char *at, size_t length) {
|
||||||
|
HTTPParser *p = reinterpret_cast<HTTPParser *>(multipart_parser_get_data(parser));
|
||||||
|
return p->on_multipart_header_value_cb(at, length);
|
||||||
|
}
|
||||||
|
int HTTPParser::_on_multipart_part_data_cb(multipart_parser *parser, const char *at, size_t length) {
|
||||||
|
HTTPParser *p = reinterpret_cast<HTTPParser *>(multipart_parser_get_data(parser));
|
||||||
|
return p->on_multipart_part_data_cb(at, length);
|
||||||
|
}
|
||||||
|
int HTTPParser::_on_multipart_part_data_begin_cb(multipart_parser *parser) {
|
||||||
|
HTTPParser *p = reinterpret_cast<HTTPParser *>(multipart_parser_get_data(parser));
|
||||||
|
return p->on_multipart_part_data_begin_cb();
|
||||||
|
}
|
||||||
|
int HTTPParser::_on_multipart_headers_complete_cb(multipart_parser *parser) {
|
||||||
|
HTTPParser *p = reinterpret_cast<HTTPParser *>(multipart_parser_get_data(parser));
|
||||||
|
return p->on_multipart_headers_complete_cb();
|
||||||
|
}
|
||||||
|
int HTTPParser::_on_multipart_part_data_end_cb(multipart_parser *parser) {
|
||||||
|
HTTPParser *p = reinterpret_cast<HTTPParser *>(multipart_parser_get_data(parser));
|
||||||
|
return p->on_multipart_part_data_end_cb();
|
||||||
|
}
|
||||||
|
int HTTPParser::_on_multipart_body_end_cb(multipart_parser *parser) {
|
||||||
|
HTTPParser *p = reinterpret_cast<HTTPParser *>(multipart_parser_get_data(parser));
|
||||||
|
return p->on_multipart_body_end_cb();
|
||||||
|
}
|
||||||
|
@ -2,12 +2,15 @@
|
|||||||
#define HTTP_PARSER_H
|
#define HTTP_PARSER_H
|
||||||
|
|
||||||
#include "core/string/ustring.h"
|
#include "core/string/ustring.h"
|
||||||
|
#include "core/containers/vector.h"
|
||||||
|
|
||||||
#include "core/object/reference.h"
|
#include "core/object/reference.h"
|
||||||
|
|
||||||
class SimpleWebServerRequest;
|
class SimpleWebServerRequest;
|
||||||
struct http_parser;
|
struct http_parser;
|
||||||
struct http_parser_settings;
|
struct http_parser_settings;
|
||||||
|
struct multipart_parser;
|
||||||
|
struct multipart_parser_settings;
|
||||||
|
|
||||||
class HTTPParser : public Reference {
|
class HTTPParser : public Reference {
|
||||||
GDCLASS(HTTPParser, Reference);
|
GDCLASS(HTTPParser, Reference);
|
||||||
@ -44,12 +47,28 @@ protected:
|
|||||||
|
|
||||||
bool _is_ready;
|
bool _is_ready;
|
||||||
|
|
||||||
|
HTTPRequestContentType _content_type;
|
||||||
|
bool _in_header;
|
||||||
|
String _queued_header_field;
|
||||||
|
|
||||||
|
String _multipart_boundary;
|
||||||
|
|
||||||
|
String _queued_multipart_header_field;
|
||||||
|
Vector<char> _queued_multipart_form_data;
|
||||||
|
|
||||||
|
String _multipart_form_name;
|
||||||
|
String _multipart_form_filename;
|
||||||
|
String _multipart_form_content_type;
|
||||||
|
bool _multipart_form_is_file;
|
||||||
|
Vector<char> _multipart_form_data;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
String chr_len_to_str(const char *at, size_t length);
|
String chr_len_to_str(const char *at, size_t length);
|
||||||
|
|
||||||
void process_multipart_data();
|
int process_multipart_data(const char *at, size_t length);
|
||||||
void _process_multipart_header(const String &header);
|
void _process_multipart_header_value(const String &val);
|
||||||
void process_urlenc_data();
|
void process_urlenc_data();
|
||||||
|
bool is_boundary_at(const char *at, size_t length);
|
||||||
|
|
||||||
int on_message_begin();
|
int on_message_begin();
|
||||||
int on_url(const char *at, size_t length);
|
int on_url(const char *at, size_t length);
|
||||||
@ -73,20 +92,27 @@ private:
|
|||||||
static int _on_chunk_header_cb(http_parser *parser);
|
static int _on_chunk_header_cb(http_parser *parser);
|
||||||
static int _on_chunk_complete_cb(http_parser *parser);
|
static int _on_chunk_complete_cb(http_parser *parser);
|
||||||
|
|
||||||
|
int on_multipart_header_field_cb(const char *at, size_t length);
|
||||||
|
int on_multipart_header_value_cb(const char *at, size_t length);
|
||||||
|
int on_multipart_part_data_cb(const char *at, size_t length);
|
||||||
|
int on_multipart_part_data_begin_cb();
|
||||||
|
int on_multipart_headers_complete_cb();
|
||||||
|
int on_multipart_part_data_end_cb();
|
||||||
|
int on_multipart_body_end_cb();
|
||||||
|
|
||||||
|
static int _on_multipart_header_field_cb(multipart_parser *parser, const char *at, size_t length);
|
||||||
|
static int _on_multipart_header_value_cb(multipart_parser *parser, const char *at, size_t length);
|
||||||
|
static int _on_multipart_part_data_cb(multipart_parser *parser, const char *at, size_t length);
|
||||||
|
static int _on_multipart_part_data_begin_cb(multipart_parser *parser);
|
||||||
|
static int _on_multipart_headers_complete_cb(multipart_parser *parser);
|
||||||
|
static int _on_multipart_part_data_end_cb(multipart_parser *parser);
|
||||||
|
static int _on_multipart_body_end_cb(multipart_parser *parser);
|
||||||
|
|
||||||
http_parser *parser;
|
http_parser *parser;
|
||||||
http_parser_settings *settings;
|
http_parser_settings *settings;
|
||||||
|
|
||||||
HTTPRequestContentType _content_type;
|
multipart_parser *_multipart_parser;
|
||||||
String _multipart_boundary;
|
multipart_parser_settings *_multipart_parser_settings;
|
||||||
bool _in_header;
|
|
||||||
String _queued_header_field;
|
|
||||||
bool _in_multipart_boundary;
|
|
||||||
bool _in_boundary_header;
|
|
||||||
|
|
||||||
String _multipart_form_name;
|
|
||||||
String _multipart_form_filename;
|
|
||||||
String _multipart_form_content_type;
|
|
||||||
bool _multipart_form_is_file;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
98
modules/http_server_simple/multipart_parser_c/README.md
Normal file
98
modules/http_server_simple/multipart_parser_c/README.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
## Multipart form data parser
|
||||||
|
|
||||||
|
### Features
|
||||||
|
* No dependencies
|
||||||
|
* Works with chunks of a data - no need to buffer the whole request
|
||||||
|
* Almost no internal buffering. Buffer size doesn't exceed the size of the boundary (~60-70 bytes)
|
||||||
|
|
||||||
|
Tested as part of [Cosmonaut](https://github.com/iafonov/cosmonaut) HTTP server.
|
||||||
|
|
||||||
|
Implementation based on [node-formidable](https://github.com/felixge/node-formidable) by [Felix Geisendörfer](https://github.com/felixge).
|
||||||
|
|
||||||
|
Inspired by [http-parser](https://github.com/joyent/http-parser) by [Ryan Dahl](https://github.com/ry).
|
||||||
|
|
||||||
|
### Usage (C)
|
||||||
|
This parser library works with several callbacks, which the user may set up at application initialization time.
|
||||||
|
|
||||||
|
```c
|
||||||
|
multipart_parser_settings callbacks;
|
||||||
|
|
||||||
|
memset(&callbacks, 0, sizeof(multipart_parser_settings));
|
||||||
|
|
||||||
|
callbacks.on_header_field = read_header_name;
|
||||||
|
callbacks.on_header_value = read_header_value;
|
||||||
|
```
|
||||||
|
|
||||||
|
These functions must match the signatures defined in the multipart-parser header file. For this simple example, we'll just use two of the available callbacks to print all headers the library finds in multipart messages.
|
||||||
|
|
||||||
|
Returning a value other than 0 from the callbacks will abort message processing.
|
||||||
|
|
||||||
|
```c
|
||||||
|
int read_header_name(multipart_parser* p, const char *at, size_t length)
|
||||||
|
{
|
||||||
|
printf("%.*s: ", length, at);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int read_header_value(multipart_parser* p, const char *at, size_t length)
|
||||||
|
{
|
||||||
|
printf("%.*s\n", length, at);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When a message arrives, callers must parse the multipart boundary from the **Content-Type** header (see the [RFC](http://tools.ietf.org/html/rfc2387#section-5.1) for more information and examples), and then execute the parser.
|
||||||
|
|
||||||
|
```c
|
||||||
|
multipart_parser* parser = multipart_parser_init(boundary, &callbacks);
|
||||||
|
multipart_parser_execute(parser, body, length);
|
||||||
|
multipart_parser_free(parser);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage (C++)
|
||||||
|
In C++, when the callbacks are static member functions it may be helpful to pass the instantiated multipart consumer along as context. The following (abbreviated) class called `MultipartConsumer` shows how to pass `this` to callback functions in order to access non-static member data.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class MultipartConsumer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MultipartConsumer(const std::string& boundary)
|
||||||
|
{
|
||||||
|
memset(&m_callbacks, 0, sizeof(multipart_parser_settings));
|
||||||
|
m_callbacks.on_header_field = ReadHeaderName;
|
||||||
|
m_callbacks.on_header_value = ReadHeaderValue;
|
||||||
|
|
||||||
|
m_parser = multipart_parser_init(boundary.c_str(), &m_callbacks);
|
||||||
|
multipart_parser_set_data(m_parser, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
~MultipartConsumer()
|
||||||
|
{
|
||||||
|
multipart_parser_free(m_parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
int CountHeaders(const std::string& body)
|
||||||
|
{
|
||||||
|
multipart_parser_execute(m_parser, body.c_str(), body.size());
|
||||||
|
return m_headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static int ReadHeaderName(multipart_parser* p, const char *at, size_t length)
|
||||||
|
{
|
||||||
|
MultipartConsumer* me = (MultipartConsumer*)multipart_parser_get_data(p);
|
||||||
|
me->m_headers++;
|
||||||
|
}
|
||||||
|
|
||||||
|
multipart_parser* m_parser;
|
||||||
|
multipart_parser_settings m_callbacks;
|
||||||
|
int m_headers;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contributors
|
||||||
|
* [Daniel T. Wagner](http://www.danieltwagner.de/)
|
||||||
|
* [James McLaughlin](http://udp.github.com/)
|
||||||
|
* [Jay Miller](http://www.cryptofreak.org)
|
||||||
|
|
||||||
|
© 2012 [Igor Afonov](http://iafonov.github.com)
|
306
modules/http_server_simple/multipart_parser_c/multipart_parser.c
Normal file
306
modules/http_server_simple/multipart_parser_c/multipart_parser.c
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
/* Based on node-formidable by Felix Geisendörfer
|
||||||
|
* Igor Afonov - afonov@gmail.com - 2012
|
||||||
|
* MIT License - http://www.opensource.org/licenses/mit-license.php
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "multipart_parser.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static void multipart_log(const char * format, ...)
|
||||||
|
{
|
||||||
|
#ifdef DEBUG_MULTIPART
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
|
||||||
|
fprintf(stderr, "[HTTP_MULTIPART_PARSER] %s:%d: ", __FILE__, __LINE__);
|
||||||
|
vfprintf(stderr, format, args);
|
||||||
|
fprintf(stderr, "\n");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#define NOTIFY_CB(FOR) \
|
||||||
|
do { \
|
||||||
|
if (p->settings->on_##FOR) { \
|
||||||
|
if (p->settings->on_##FOR(p) != 0) { \
|
||||||
|
return i; \
|
||||||
|
} \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
#define EMIT_DATA_CB(FOR, ptr, len) \
|
||||||
|
do { \
|
||||||
|
if (p->settings->on_##FOR) { \
|
||||||
|
if (p->settings->on_##FOR(p, ptr, len) != 0) { \
|
||||||
|
return i; \
|
||||||
|
} \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
|
||||||
|
#define LF 10
|
||||||
|
#define CR 13
|
||||||
|
|
||||||
|
struct multipart_parser {
|
||||||
|
void * data;
|
||||||
|
|
||||||
|
size_t index;
|
||||||
|
size_t boundary_length;
|
||||||
|
|
||||||
|
unsigned char state;
|
||||||
|
|
||||||
|
const multipart_parser_settings* settings;
|
||||||
|
|
||||||
|
char* lookbehind;
|
||||||
|
char multipart_boundary[1];
|
||||||
|
};
|
||||||
|
|
||||||
|
enum state {
|
||||||
|
s_uninitialized = 1,
|
||||||
|
s_start,
|
||||||
|
s_start_boundary,
|
||||||
|
s_header_field_start,
|
||||||
|
s_header_field,
|
||||||
|
s_headers_almost_done,
|
||||||
|
s_header_value_start,
|
||||||
|
s_header_value,
|
||||||
|
s_header_value_almost_done,
|
||||||
|
s_part_data_start,
|
||||||
|
s_part_data,
|
||||||
|
s_part_data_almost_boundary,
|
||||||
|
s_part_data_boundary,
|
||||||
|
s_part_data_almost_end,
|
||||||
|
s_part_data_end,
|
||||||
|
s_part_data_final_hyphen,
|
||||||
|
s_end
|
||||||
|
};
|
||||||
|
|
||||||
|
multipart_parser* multipart_parser_init
|
||||||
|
(const char *boundary, const multipart_parser_settings* settings) {
|
||||||
|
|
||||||
|
multipart_parser* p = malloc(sizeof(multipart_parser) +
|
||||||
|
strlen(boundary) +
|
||||||
|
strlen(boundary) + 9);
|
||||||
|
|
||||||
|
strcpy(p->multipart_boundary, boundary);
|
||||||
|
p->boundary_length = strlen(boundary);
|
||||||
|
|
||||||
|
p->lookbehind = (p->multipart_boundary + p->boundary_length + 1);
|
||||||
|
|
||||||
|
p->index = 0;
|
||||||
|
p->state = s_start;
|
||||||
|
p->settings = settings;
|
||||||
|
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
void multipart_parser_free(multipart_parser* p) {
|
||||||
|
free(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
void multipart_parser_set_data(multipart_parser *p, void *data) {
|
||||||
|
p->data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void *multipart_parser_get_data(multipart_parser *p) {
|
||||||
|
return p->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t multipart_parser_execute(multipart_parser* p, const char *buf, size_t len) {
|
||||||
|
size_t i = 0;
|
||||||
|
size_t mark = 0;
|
||||||
|
char c, cl;
|
||||||
|
int is_last = 0;
|
||||||
|
|
||||||
|
while(i < len) {
|
||||||
|
c = buf[i];
|
||||||
|
is_last = (i == (len - 1));
|
||||||
|
switch (p->state) {
|
||||||
|
case s_start:
|
||||||
|
multipart_log("s_start");
|
||||||
|
p->index = 0;
|
||||||
|
p->state = s_start_boundary;
|
||||||
|
|
||||||
|
/* fallthrough */
|
||||||
|
case s_start_boundary:
|
||||||
|
multipart_log("s_start_boundary");
|
||||||
|
if (p->index == p->boundary_length) {
|
||||||
|
if (c != CR) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
p->index++;
|
||||||
|
break;
|
||||||
|
} else if (p->index == (p->boundary_length + 1)) {
|
||||||
|
if (c != LF) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
p->index = 0;
|
||||||
|
NOTIFY_CB(part_data_begin);
|
||||||
|
p->state = s_header_field_start;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (c != p->multipart_boundary[p->index]) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
p->index++;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case s_header_field_start:
|
||||||
|
multipart_log("s_header_field_start");
|
||||||
|
mark = i;
|
||||||
|
p->state = s_header_field;
|
||||||
|
|
||||||
|
/* fallthrough */
|
||||||
|
case s_header_field:
|
||||||
|
multipart_log("s_header_field");
|
||||||
|
if (c == CR) {
|
||||||
|
p->state = s_headers_almost_done;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c == ':') {
|
||||||
|
EMIT_DATA_CB(header_field, buf + mark, i - mark);
|
||||||
|
p->state = s_header_value_start;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cl = tolower(c);
|
||||||
|
if ((c != '-') && (cl < 'a' || cl > 'z')) {
|
||||||
|
multipart_log("invalid character in header name");
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
if (is_last)
|
||||||
|
EMIT_DATA_CB(header_field, buf + mark, (i - mark) + 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case s_headers_almost_done:
|
||||||
|
multipart_log("s_headers_almost_done");
|
||||||
|
if (c != LF) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
p->state = s_part_data_start;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case s_header_value_start:
|
||||||
|
multipart_log("s_header_value_start");
|
||||||
|
if (c == ' ') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
mark = i;
|
||||||
|
p->state = s_header_value;
|
||||||
|
|
||||||
|
/* fallthrough */
|
||||||
|
case s_header_value:
|
||||||
|
multipart_log("s_header_value");
|
||||||
|
if (c == CR) {
|
||||||
|
EMIT_DATA_CB(header_value, buf + mark, i - mark);
|
||||||
|
p->state = s_header_value_almost_done;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (is_last)
|
||||||
|
EMIT_DATA_CB(header_value, buf + mark, (i - mark) + 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case s_header_value_almost_done:
|
||||||
|
multipart_log("s_header_value_almost_done");
|
||||||
|
if (c != LF) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
p->state = s_header_field_start;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case s_part_data_start:
|
||||||
|
multipart_log("s_part_data_start");
|
||||||
|
NOTIFY_CB(headers_complete);
|
||||||
|
mark = i;
|
||||||
|
p->state = s_part_data;
|
||||||
|
|
||||||
|
/* fallthrough */
|
||||||
|
case s_part_data:
|
||||||
|
multipart_log("s_part_data");
|
||||||
|
if (c == CR) {
|
||||||
|
EMIT_DATA_CB(part_data, buf + mark, i - mark);
|
||||||
|
mark = i;
|
||||||
|
p->state = s_part_data_almost_boundary;
|
||||||
|
p->lookbehind[0] = CR;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (is_last)
|
||||||
|
EMIT_DATA_CB(part_data, buf + mark, (i - mark) + 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case s_part_data_almost_boundary:
|
||||||
|
multipart_log("s_part_data_almost_boundary");
|
||||||
|
if (c == LF) {
|
||||||
|
p->state = s_part_data_boundary;
|
||||||
|
p->lookbehind[1] = LF;
|
||||||
|
p->index = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
EMIT_DATA_CB(part_data, p->lookbehind, 1);
|
||||||
|
p->state = s_part_data;
|
||||||
|
mark = i --;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case s_part_data_boundary:
|
||||||
|
multipart_log("s_part_data_boundary");
|
||||||
|
if (p->multipart_boundary[p->index] != c) {
|
||||||
|
EMIT_DATA_CB(part_data, p->lookbehind, 2 + p->index);
|
||||||
|
p->state = s_part_data;
|
||||||
|
mark = i --;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
p->lookbehind[2 + p->index] = c;
|
||||||
|
if ((++ p->index) == p->boundary_length) {
|
||||||
|
NOTIFY_CB(part_data_end);
|
||||||
|
p->state = s_part_data_almost_end;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case s_part_data_almost_end:
|
||||||
|
multipart_log("s_part_data_almost_end");
|
||||||
|
if (c == '-') {
|
||||||
|
p->state = s_part_data_final_hyphen;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (c == CR) {
|
||||||
|
p->state = s_part_data_end;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
|
||||||
|
case s_part_data_final_hyphen:
|
||||||
|
multipart_log("s_part_data_final_hyphen");
|
||||||
|
if (c == '-') {
|
||||||
|
NOTIFY_CB(body_end);
|
||||||
|
p->state = s_end;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
|
||||||
|
case s_part_data_end:
|
||||||
|
multipart_log("s_part_data_end");
|
||||||
|
if (c == LF) {
|
||||||
|
p->state = s_header_field_start;
|
||||||
|
NOTIFY_CB(part_data_begin);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
|
||||||
|
case s_end:
|
||||||
|
multipart_log("s_end: %02X", (int) c);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
multipart_log("Multipart parser unrecoverable error");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
++ i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return len;
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
/* Based on node-formidable by Felix Geisendörfer
|
||||||
|
* Igor Afonov - afonov@gmail.com - 2012
|
||||||
|
* MIT License - http://www.opensource.org/licenses/mit-license.php
|
||||||
|
*/
|
||||||
|
#ifndef _multipart_parser_h
|
||||||
|
#define _multipart_parser_h
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
|
||||||
|
typedef struct multipart_parser multipart_parser;
|
||||||
|
typedef struct multipart_parser_settings multipart_parser_settings;
|
||||||
|
typedef struct multipart_parser_state multipart_parser_state;
|
||||||
|
|
||||||
|
typedef int (*multipart_data_cb) (multipart_parser*, const char *at, size_t length);
|
||||||
|
typedef int (*multipart_notify_cb) (multipart_parser*);
|
||||||
|
|
||||||
|
struct multipart_parser_settings {
|
||||||
|
multipart_data_cb on_header_field;
|
||||||
|
multipart_data_cb on_header_value;
|
||||||
|
multipart_data_cb on_part_data;
|
||||||
|
|
||||||
|
multipart_notify_cb on_part_data_begin;
|
||||||
|
multipart_notify_cb on_headers_complete;
|
||||||
|
multipart_notify_cb on_part_data_end;
|
||||||
|
multipart_notify_cb on_body_end;
|
||||||
|
};
|
||||||
|
|
||||||
|
multipart_parser* multipart_parser_init
|
||||||
|
(const char *boundary, const multipart_parser_settings* settings);
|
||||||
|
|
||||||
|
void multipart_parser_free(multipart_parser* p);
|
||||||
|
|
||||||
|
size_t multipart_parser_execute(multipart_parser* p, const char *buf, size_t len);
|
||||||
|
|
||||||
|
void multipart_parser_set_data(multipart_parser* p, void* data);
|
||||||
|
void * multipart_parser_get_data(multipart_parser* p);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
} /* extern "C" */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
Loading…
Reference in New Issue
Block a user