3

I have a HTML form put in a CGI file in C with fprintf statements like this:

<form action="cgi-bin/upload.cgi" method="post" enctype="multipart/form-data"> 
    <p>Photo to Upload: <input type="file" name="photo" /></p> 
    <p>Your Email Address: <input type="text" name="email_address" /></p> 
    <p><input type="submit" name="Submit" value="Submit Form" /></p> 
</form>

I was wondering if it's possible to grab the file input (the values requested from the form with a multipart/form-data body), convert it to C code (process the values by obtaining the url data), and upload the file from there? BTW, I'm not allowed to use PHP as the machine I'm currently working with has very limited space.

Sean C
  • 45
  • 1
  • 7
  • 1
    "*grab the file input, convert it to C code*" <- I don't understand **this**, please explain. –  Jul 11 '17 at 07:02
  • This is still a bit unclear, but maybe you have a wrong idea how HTTP uploads actually work? The browser won't send you an URL (think about it, you can't access files on the client system from the server) but it will issue a request with a `multipart/form-data` body where one of the parts is the uploaded file, probably base64 encoded. –  Jul 11 '17 at 07:17
  • You are right, I started learning about servers not too long ago, so all is still confusing. I was wondering if you knew a way to upload a file to a server without using PHP? – Sean C Jul 11 '17 at 07:23
  • 2
    With CGI, you will get the client's request on `stdin`. You will also get an environment variable `CONTENT_LENGTH` that's set to the original `Content-Length:` header value of the client, so you know how many bytes you have to read from `stdin`. From there, you're on your own, you have to *parse* that data as `multipart/form-data`. There are probably libraries for this. –  Jul 11 '17 at 07:23
  • thank you for sharing your knowledge @FelixPalmen – Sean C Jul 11 '17 at 07:28
  • Note that CGI is not exactly *efficient*. The server has to spawn a process for your program for *every single request*. Using e.g. apache's `mod_php` would be more efficient. For C, you could look into using for example a [`FastCGI`](https://fastcgi-archives.github.io/) library –  Jul 11 '17 at 07:29
  • @user2371524, what is the best library for parsing multipart/form-data in C? And how to realize it using fcgi2? – Грузчик Jul 29 '22 at 12:17

2 Answers2

3

This example does not use any non-standard library:

#include <stdio.h>
#include <stdlib.h>

#define MAXLEN 80
#define EXTRA 5
/* 4 for field name "data", 1 for "=" */
#define MAXINPUT MAXLEN + EXTRA + 2
/* 1 for added line break, 1 for trailing NUL */
#define DATAFILE "../data/data.txt"

void unencode(char * src, char * last, char * dest) {
    for (; src != last; src++, dest++)
        if ( * src == '+')
            * dest = ' ';
        else if ( * src == '%') {
        int code;
        if (sscanf(src + 1, "%2x", & code) != 1) code = '?'; * dest = code;
        src += 2;
    } else
        *dest = * src; * dest = '\n'; * ++dest = '\0';
}

int main(void) {
    char * lenstr;
    char input[MAXINPUT], data[MAXINPUT];
    long len;
    printf("%s%c%c\n",
        "Content-Type:text/html;charset=iso-8859-1", 13, 10);
    printf("<TITLE>Response</TITLE>\n");
    lenstr = getenv("CONTENT_LENGTH");
    if (lenstr == NULL || sscanf(lenstr, "%ld", & len) != 1 || len > MAXLEN)
        printf("<P>Error in invocation - wrong FORM probably.");
    else {
        FILE * f;
        fgets(input, len + 1, stdin);
        unencode(input + EXTRA, input + len, data);
        f = fopen(DATAFILE, "a");
        if (f == NULL)
            printf("<P>Sorry, cannot store your data.");
        else
            fputs(data, f);
        fclose(f);
        printf("<P>Thank you! The following contribution of yours has \
been stored:<BR>%s", data);
    }
    return 0;
}
2

Here's a simple example how you could handle file uploads through CGI. This has many shortcomings, for example it doesn't do any checks on the file size, name and type -- e.g. just concatenating the filename provided by the client is dangerous, it could contain ... Don't use the code shown here in production!

This example uses a Multipart form data parser I found on github. You would need some more code to actually parse the other form fields. You might want to start by looking at this answer including an example how a request body with multipart-form-data looks like.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "multipart_parser.h"

struct parsedata
{
    int inContentDisposition;  // flag for the right header to look for fields
    char *partname;            // field name
    char *filename;            // file name for an upload field
    FILE *saveto;              // file to save contents to
};

void showForm(void)
{
    puts("<form action=\"upload.cgi\" method=\"post\" "
            "enctype=\"multipart/form-data\">"
        "<p>Photo to Upload: <input type=\"file\" name=\"photo\" /></p>"
        "<p>Your Email Address: <input type=\"text\" "
            "name=\"email_address\" /></p>"
        "<p><input type=\"submit\" name=\"Submit\" value=\"Submit Form\""
        "/></p></form>");
}

int handle_headername(multipart_parser *parser, const char *at, size_t length)
{
    struct parsedata *data = multipart_parser_get_data(parser);
    data->inContentDisposition = !strncmp(at, "Content-Disposition", length);
    return 0;
}

int handle_headervalue(multipart_parser *parser, const char *at, size_t length)
{
    char localfilename[1024];

    struct parsedata *data = multipart_parser_get_data(parser);
    if (data->inContentDisposition)
    {
        char *hdrval = calloc(1, length + 1);
        strncpy(hdrval, at, length);
        if (strtok(hdrval, "; "))
        {
            char *tok;
            while ((tok = strtok(0, "; ")))
            {
                char *rquot;
                if (!strncmp(tok, "name=\"", 6) && 
                        ((rquot = strrchr(tok, '"')) > tok + 6))
                {
                    *rquot = 0;
                    free(data->partname);
                    data->partname = malloc(strlen(tok + 6) + 1);
                    strcpy(data->partname, tok + 6);
                }
                else if (!strncmp(tok, "filename=\"", 10) &&
                        ((rquot = strrchr(tok, '"')) > tok + 10))
                {
                    *rquot = 0;
                    free(data->filename);
                    data->filename = malloc(strlen(tok + 10) + 1);
                    strcpy(data->filename, tok + 10);
                    if (data->saveto) fclose(data->saveto);

                    // determine local location, adapt to your needs:
                    // for production code, ADD SANITY CHECKS, the following code
                    // allows an attacker to write any location of your server
                    // with a filename containing relative paths!
                    snprintf(localfilename, 1024, "uploads/%s", data->filename);
                    data->saveto = fopen(localfilename, "w");
                }
            }
        }
        free(hdrval);
    }
    return 0;
}

int handle_contentdata(multipart_parser *parser, const char *at, size_t length)
{
    struct parsedata *data = multipart_parser_get_data(parser);

    // only handle file upload of field "photo"
    // you have to extend this to get the values of other form fields
    if (data->partname && data->filename && !strcmp(data->partname, "photo"))
    {
        fwrite(at, length, 1, data->saveto);
    }

    return 0;
}

char *upload(void)
{
    // can only upload with POST
    const char *method = getenv("REQUEST_METHOD");
    if (!method || strcmp(method, "POST")) return 0;

    // check for multipart/form-data and extract boundary if present
    char boundary[128] = "--"; // boundary starts with double dash
    const char *conttype = getenv("CONTENT_TYPE");
    if (!conttype || sscanf(conttype,
                "multipart/form-data; boundary=%125s", boundary+2)
            < 1) return 0;

    // see https://github.com/iafonov/multipart-parser-c
    multipart_parser_settings callbacks = {0};
    callbacks.on_header_field = handle_headername;
    callbacks.on_header_value = handle_headervalue;
    callbacks.on_part_data = handle_contentdata;

    struct parsedata data = {0};

    multipart_parser *parser = multipart_parser_init(boundary, &callbacks);
    multipart_parser_set_data(parser, &data);

    // read body from stdin:
    char reqdata[64 * 1024];
    size_t length;
    while ((length = fread(reqdata, 1, 64 * 1024, stdin)) > 0)
    {
        // and feed it to the parser:
        multipart_parser_execute(parser, reqdata, length);
    }

    multipart_parser_free(parser);

    free(data.partname);
    if (data.filename && data.saveto)
    {
        fclose(data.saveto);
        return data.filename;
    }
    free(data.filename);

    return 0;
}

int main(void)
{
    char *uploaded = upload();

    puts("Content-Type: text/html\n");
    puts("<html><head><title>Test</title></head><body>");

    if (uploaded)
    {
        printf("<b>%s</b> uploaded.", uploaded);
        free(uploaded);
    }
    else
    {
        showForm();
    }

    puts("</body></html>");
}
  • @SeanC you can use the same multipart parser code in a better solution being self-hosted (e.g. with `libevent`) or using FastCGI. Or maybe you find some better library that also takes the burdens of actually checking the posted files, the size, type, etc. You should have an idea now how much work e.g. PHP already does for you parsing a request containing an uploaded file. –  Jul 12 '17 at 09:37
  • @user2371524, the library has a known bug https://github.com/iafonov/multipart-parser-c/issues/20 – Грузчик Jul 29 '22 at 12:18