diff --git a/net/cgi-io/Makefile b/net/cgi-io/Makefile new file mode 100644 index 000000000..c36231a12 --- /dev/null +++ b/net/cgi-io/Makefile @@ -0,0 +1,44 @@ +# +# Copyright (C) 2015 OpenWrt.org +# +# This is free software, licensed under the GNU General Public License v2. +# See /LICENSE for more information. +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=cgi-io +PKG_RELEASE:=1 + +PKG_LICENSE:=GPL-2.0+ + +PKG_MAINTAINER:=John Crispin + +include $(INCLUDE_DIR)/package.mk +include $(INCLUDE_DIR)/cmake.mk + +define Package/cgi-io + SECTION:=net + CATEGORY:=Network + SUBMENU:=Web Servers/Proxies + DEPENDS:=+libubox +libubus + TITLE:=CGI utility for handling up/downloading of files +endef + +define Package/cgi-io/description + This package contains an cgi utility that is useful for up/downloading files +endef + +define Build/Prepare + mkdir -p $(PKG_BUILD_DIR) + $(CP) ./src/* $(PKG_BUILD_DIR)/ +endef + +define Package/cgi-io/install + $(INSTALL_DIR) $(1)/usr/libexec $(1)/www/cgi-bin/ + $(INSTALL_BIN) $(PKG_BUILD_DIR)/cgi-io $(1)/usr/libexec + $(LN) ../../usr/libexec/cgi-io $(1)/www/cgi-bin/cgi-upload + $(LN) ../../usr/libexec/cgi-io $(1)/www/cgi-bin/cgi-download +endef + +$(eval $(call BuildPackage,cgi-io)) diff --git a/net/cgi-io/src/CMakeLists.txt b/net/cgi-io/src/CMakeLists.txt new file mode 100644 index 000000000..8a4ca192c --- /dev/null +++ b/net/cgi-io/src/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 2.6) + +PROJECT(cgi-io C) + +INCLUDE(CheckFunctionExists) + +ADD_DEFINITIONS(-Os -Wall -Werror --std=gnu99 -g3 -Wmissing-declarations) + +SET(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "") + +IF(APPLE) + INCLUDE_DIRECTORIES(/opt/local/include) + LINK_DIRECTORIES(/opt/local/lib) +ENDIF() + +ADD_EXECUTABLE(cgi-io main.c multipart_parser.c) +TARGET_LINK_LIBRARIES(cgi-io ubox ubus) + +INSTALL(TARGETS cgi-io RUNTIME DESTINATION sbin) diff --git a/net/cgi-io/src/main.c b/net/cgi-io/src/main.c new file mode 100644 index 000000000..985831c11 --- /dev/null +++ b/net/cgi-io/src/main.c @@ -0,0 +1,644 @@ +/* + * cgi-io - LuCI non-RPC helper + * + * Copyright (C) 2013 Jo-Philipp Wich + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "multipart_parser.h" + + +enum part { + PART_UNKNOWN, + PART_SESSIONID, + PART_FILENAME, + PART_FILEMODE, + PART_FILEDATA +}; + +const char *parts[] = { + "(bug)", + "sessionid", + "filename", + "filemode", + "filedata", +}; + +struct state +{ + bool is_content_disposition; + enum part parttype; + char *sessionid; + char *filename; + bool filedata; + int filemode; + int filefd; + int tempfd; +}; + +enum { + SES_ACCESS, + __SES_MAX, +}; + +static const struct blobmsg_policy ses_policy[__SES_MAX] = { + [SES_ACCESS] = { .name = "access", .type = BLOBMSG_TYPE_BOOL }, +}; + + +static struct state st; + +static void +session_access_cb(struct ubus_request *req, int type, struct blob_attr *msg) +{ + struct blob_attr *tb[__SES_MAX]; + bool *allow = (bool *)req->priv; + + if (!msg) + return; + + blobmsg_parse(ses_policy, __SES_MAX, tb, blob_data(msg), blob_len(msg)); + + if (tb[SES_ACCESS]) + *allow = blobmsg_get_bool(tb[SES_ACCESS]); +} + +static bool +session_access(const char *sid, const char *obj, const char *func) +{ + uint32_t id; + bool allow = false; + struct ubus_context *ctx; + static struct blob_buf req; + + ctx = ubus_connect(NULL); + + if (!ctx || ubus_lookup_id(ctx, "session", &id)) + goto out; + + blob_buf_init(&req, 0); + blobmsg_add_string(&req, "ubus_rpc_session", sid); + blobmsg_add_string(&req, "scope", "cgi-io"); + blobmsg_add_string(&req, "object", obj); + blobmsg_add_string(&req, "function", func); + + ubus_invoke(ctx, id, "access", req.head, session_access_cb, &allow, 500); + +out: + if (ctx) + ubus_free(ctx); + + return allow; +} + +static char * +md5sum(const char *file) +{ + pid_t pid; + int fds[2]; + static char md5[33]; + + if (pipe(fds)) + return NULL; + + switch ((pid = fork())) + { + case -1: + return NULL; + + case 0: + uloop_done(); + + dup2(fds[1], 1); + + close(0); + close(2); + close(fds[0]); + close(fds[1]); + + if (execl("/bin/busybox", "/bin/busybox", "md5sum", file, NULL)); + return NULL; + + break; + + default: + memset(md5, 0, sizeof(md5)); + read(fds[0], md5, 32); + waitpid(pid, NULL, 0); + close(fds[0]); + close(fds[1]); + } + + return md5; +} + +static char * +datadup(const void *in, size_t len) +{ + char *out = malloc(len + 1); + + if (!out) + return NULL; + + memcpy(out, in, len); + + *(out + len) = 0; + + return out; +} + +static bool +urldecode(char *buf) +{ + char *c, *p; + + if (!buf || !*buf) + return true; + +#define hex(x) \ + (((x) <= '9') ? ((x) - '0') : \ + (((x) <= 'F') ? ((x) - 'A' + 10) : \ + ((x) - 'a' + 10))) + + for (c = p = buf; *p; c++) + { + if (*p == '%') + { + if (!isxdigit(*(p + 1)) || !isxdigit(*(p + 2))) + return false; + + *c = (char)(16 * hex(*(p + 1)) + hex(*(p + 2))); + + p += 3; + } + else if (*p == '+') + { + *c = ' '; + p++; + } + else + { + *c = *p++; + } + } + + *c = 0; + + return true; +} + +static bool +postdecode(char **fields, int n_fields) +{ + char *p; + const char *var; + static char buf[1024]; + int i, len, field, found = 0; + + var = getenv("CONTENT_TYPE"); + + if (!var || strncmp(var, "application/x-www-form-urlencoded", 33)) + return false; + + memset(buf, 0, sizeof(buf)); + + if ((len = read(0, buf, sizeof(buf) - 1)) > 0) + { + for (p = buf, i = 0; i <= len; i++) + { + if (buf[i] == '=') + { + buf[i] = 0; + + for (field = 0; field < (n_fields * 2); field += 2) + { + if (!strcmp(p, fields[field])) + { + fields[field + 1] = buf + i + 1; + found++; + } + } + } + else if (buf[i] == '&' || buf[i] == '\0') + { + buf[i] = 0; + + if (found >= n_fields) + break; + + p = buf + i + 1; + } + } + } + + for (field = 0; field < (n_fields * 2); field += 2) + if (!urldecode(fields[field + 1])) + return false; + + return (found >= n_fields); +} + +static int +response(bool success, const char *message) +{ + char *md5; + struct stat s; + + printf("Status: 200 OK\r\n"); + printf("Content-Type: text/plain\r\n\r\n{\n"); + + if (success) + { + if (!stat(st.filename, &s) && (md5 = md5sum(st.filename)) != NULL) + printf("\t\"size\": %u,\n\t\"checksum\": \"%s\"\n", + (unsigned int)s.st_size, md5); + } + else + { + if (message) + printf("\t\"message\": \"%s\",\n", message); + + printf("\t\"failure\": [ %u, \"%s\" ]\n", errno, strerror(errno)); + + if (st.filefd > -1) + unlink(st.filename); + } + + printf("}\n"); + + return -1; +} + +static int +failure(int e, const char *message) +{ + printf("Status: 500 Internal Server failure\r\n"); + printf("Content-Type: text/plain\r\n\r\n"); + printf("%s", message); + + if (e) + printf(": %s", strerror(e)); + + return -1; +} + +static int +filecopy(void) +{ + int len; + char buf[4096]; + + if (!st.filedata) + { + close(st.tempfd); + errno = EINVAL; + return response(false, "No file data received"); + } + + if (lseek(st.tempfd, 0, SEEK_SET) < 0) + { + close(st.tempfd); + return response(false, "Failed to rewind temp file"); + } + + st.filefd = open(st.filename, O_CREAT | O_TRUNC | O_WRONLY, 0600); + + if (st.filefd < 0) + { + close(st.tempfd); + return response(false, "Failed to open target file"); + } + + while ((len = read(st.tempfd, buf, sizeof(buf))) > 0) + { + if (write(st.filefd, buf, len) != len) + { + close(st.tempfd); + close(st.filefd); + return response(false, "I/O failure while writing target file"); + } + } + + close(st.tempfd); + close(st.filefd); + + if (chmod(st.filename, st.filemode)) + return response(false, "Failed to chmod target file"); + + return 0; +} + +static int +header_field(multipart_parser *p, const char *data, size_t len) +{ + st.is_content_disposition = !strncasecmp(data, "Content-Disposition", len); + return 0; +} + +static int +header_value(multipart_parser *p, const char *data, size_t len) +{ + int i, j; + + if (!st.is_content_disposition) + return 0; + + if (len < 10 || strncasecmp(data, "form-data", 9)) + return 0; + + for (data += 9, len -= 9; *data == ' ' || *data == ';'; data++, len--); + + if (len < 8 || strncasecmp(data, "name=\"", 6)) + return 0; + + for (data += 6, len -= 6, i = 0; i <= len; i++) + { + if (*(data + i) != '"') + continue; + + for (j = 1; j < sizeof(parts) / sizeof(parts[0]); j++) + if (!strncmp(data, parts[j], i)) + st.parttype = j; + + break; + } + + return 0; +} + +static int +data_begin_cb(multipart_parser *p) +{ + char tmpname[24] = "/tmp/luci-upload.XXXXXX"; + + if (st.parttype == PART_FILEDATA) + { + if (!st.sessionid) + return response(false, "File data without session"); + + if (!st.filename) + return response(false, "File data without name"); + + st.tempfd = mkstemp(tmpname); + + if (st.tempfd < 0) + return response(false, "Failed to create temporary file"); + + unlink(tmpname); + } + + return 0; +} + +static int +data_cb(multipart_parser *p, const char *data, size_t len) +{ + switch (st.parttype) + { + case PART_SESSIONID: + st.sessionid = datadup(data, len); + break; + + case PART_FILENAME: + st.filename = datadup(data, len); + break; + + case PART_FILEMODE: + st.filemode = strtoul(data, NULL, 8); + break; + + case PART_FILEDATA: + if (write(st.tempfd, data, len) != len) + { + close(st.tempfd); + return response(false, "I/O failure while writing temporary file"); + } + + if (!st.filedata) + st.filedata = !!len; + + break; + + default: + break; + } + + return 0; +} + +static int +data_end_cb(multipart_parser *p) +{ + if (st.parttype == PART_SESSIONID) + { + if (!session_access(st.sessionid, "upload", "write")) + { + errno = EPERM; + return response(false, "Upload permission denied"); + } + } + else if (st.parttype == PART_FILEDATA) + { + if (st.tempfd < 0) + return response(false, "Internal program failure"); + +#if 0 + /* prepare directory */ + for (ptr = st.filename; *ptr; ptr++) + { + if (*ptr == '/') + { + *ptr = 0; + + if (mkdir(st.filename, 0755)) + { + unlink(st.tmpname); + return response(false, "Failed to create destination directory"); + } + + *ptr = '/'; + } + } +#endif + + if (filecopy()) + return -1; + + return response(true, NULL); + } + + st.parttype = PART_UNKNOWN; + return 0; +} + +static multipart_parser * +init_parser(void) +{ + char *boundary; + const char *var; + + multipart_parser *p; + static multipart_parser_settings s = { + .on_part_data = data_cb, + .on_headers_complete = data_begin_cb, + .on_part_data_end = data_end_cb, + .on_header_field = header_field, + .on_header_value = header_value + }; + + var = getenv("CONTENT_TYPE"); + + if (!var || strncmp(var, "multipart/form-data;", 20)) + return NULL; + + for (var += 20; *var && *var != '='; var++); + + if (*var++ != '=') + return NULL; + + boundary = malloc(strlen(var) + 3); + + if (!boundary) + return NULL; + + strcpy(boundary, "--"); + strcpy(boundary + 2, var); + + st.tempfd = -1; + st.filefd = -1; + st.filemode = 0600; + + p = multipart_parser_init(boundary, &s); + + free(boundary); + + return p; +} + +static int +main_upload(int argc, char *argv[]) +{ + int rem, len; + char buf[4096]; + multipart_parser *p; + + p = init_parser(); + + if (!p) + { + errno = EINVAL; + return response(false, "Invalid request"); + } + + while ((len = read(0, buf, sizeof(buf))) > 0) + { + rem = multipart_parser_execute(p, buf, len); + + if (rem < len) + break; + } + + multipart_parser_free(p); + + /* read remaining post data */ + while ((len = read(0, buf, sizeof(buf))) > 0); + + return 0; +} + +static int +main_backup(int argc, char **argv) +{ + pid_t pid; + time_t now; + int len; + int fds[2]; + char buf[4096]; + char datestr[16] = { 0 }; + char hostname[64] = { 0 }; + char *fields[] = { "sessionid", NULL }; + + if (!postdecode(fields, 1) || !session_access(fields[1], "backup", "read")) + return failure(0, "Backup permission denied"); + + if (pipe(fds)) + return failure(errno, "Failed to spawn pipe"); + + switch ((pid = fork())) + { + case -1: + return failure(errno, "Failed to fork process"); + + case 0: + dup2(fds[1], 1); + + close(0); + close(2); + close(fds[0]); + close(fds[1]); + + chdir("/"); + + execl("/sbin/sysupgrade", "/sbin/sysupgrade", + "--create-backup", "-", NULL); + + return -1; + + default: + now = time(NULL); + strftime(datestr, sizeof(datestr) - 1, "%Y-%m-%d", localtime(&now)); + + if (gethostname(hostname, sizeof(hostname) - 1)) + sprintf(hostname, "OpenWrt"); + + printf("Status: 200 OK\r\n"); + printf("Content-Type: application/x-targz\r\n"); + printf("Content-Disposition: attachment; " + "filename=\"backup-%s-%s.tar.gz\"\r\n\r\n", hostname, datestr); + + while ((len = read(fds[0], buf, sizeof(buf))) > 0) + fwrite(buf, len, 1, stdout); + + waitpid(pid, NULL, 0); + + close(fds[0]); + close(fds[1]); + + return 0; + } +} + +int main(int argc, char **argv) +{ + if (strstr(argv[0], "cgi-upload")) + return main_upload(argc, argv); + else if (strstr(argv[0], "cgi-backup")) + return main_backup(argc, argv); + + return -1; +} diff --git a/net/cgi-io/src/multipart_parser.c b/net/cgi-io/src/multipart_parser.c new file mode 100644 index 000000000..ee82c82c8 --- /dev/null +++ b/net/cgi-io/src/multipart_parser.c @@ -0,0 +1,309 @@ +/* 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 +#include +#include + +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 == '-') { + break; + } + + if (c == ':') { + EMIT_DATA_CB(header_field, buf + mark, i - mark); + p->state = s_header_value_start; + break; + } + + cl = tolower(c); + if (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; + } + 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; +} diff --git a/net/cgi-io/src/multipart_parser.h b/net/cgi-io/src/multipart_parser.h new file mode 100644 index 000000000..87e67f41b --- /dev/null +++ b/net/cgi-io/src/multipart_parser.h @@ -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 +#include + +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