diff options
Diffstat (limited to 'doc/chapters/largerpost.inc')
-rw-r--r-- | doc/chapters/largerpost.inc | 319 |
1 files changed, 319 insertions, 0 deletions
diff --git a/doc/chapters/largerpost.inc b/doc/chapters/largerpost.inc new file mode 100644 index 00000000..1f60028f --- /dev/null +++ b/doc/chapters/largerpost.inc @@ -0,0 +1,319 @@ +The previous chapter introduced a way to upload data to the server, but the developed example program +has some shortcomings, such as not being able to handle larger chunks of data. In this chapter, we +are going to discuss a more advanced server program that allows clients to upload a file in order to +have it stored on the server's filesystem. The server shall also watch and limit the number of +clients concurrently uploading, responding with a proper busy message if necessary. + + +@heading Prepared answers +We choose to operate the server with the @code{SELECT_INTERNALLY} method. This makes it easier to +synchronize the global states at the cost of possible delays for other connections if the processing +of a request is too slow. One of these variables that needs to be shared for all connections is the +total number of clients that are uploading. + +@verbatim +#define MAXCLIENTS 2 +static unsigned int nr_of_uploading_clients = 0; +@end verbatim +@noindent + +If there are too many clients uploading, we want the server to respond to all requests with a busy +message. +@verbatim +const char* busypage = + "<html><body>This server is busy, please try again later.</body></html>"; +@end verbatim +@noindent + +Otherwise, the server will send a @emph{form} that informs the user of the current number of uploading clients, +and ask her to pick a file on her local filesystem which is to be uploaded. +@verbatim +const char* askpage = "<html><body>\n\ + Upload a file, please!<br>\n\ + There are %u clients uploading at the moment.<br>\n\ + <form action=\"/filepost\" method=\"post\" \ + enctype=\"multipart/form-data\">\n\ + <input name=\"file\" type=\"file\">\n\ + <input type=\"submit\" value=\" Send \"></form>\n\ + </body></html>"; +@end verbatim +@noindent + +If the upload has succeeded, the server will respond with a message saying so. +@verbatim +const char* completepage = "<html><body>The upload has been completed.</body></html>"; +@end verbatim +@noindent + +We want the server to report internal errors, such as memory shortage or file access problems, +adequately. +@verbatim +const char* servererrorpage + = "<html><body>An internal server error has occured.</body></html>"; +const char* fileexistspage + = "<html><body>This file already exists.</body></html>"; +@end verbatim +@noindent + +It would be tolerable to send all these responses undifferentiated with a @code{200 HTTP_OK} +status code but in order to improve the @code{HTTP} conformance of our server a bit, we extend the +@code{send_page} function so that it accepts individual status codes. + +@verbatim +static int +send_page (struct MHD_Connection *connection, + const char* page, int status_code) +{ + int ret; + struct MHD_Response *response; + + response = MHD_create_response_from_buffer (strlen (page), (void*) page, + MHD_RESPMEM_MUST_COPY); + if (!response) return MHD_NO; + + ret = MHD_queue_response (connection, status_code, response); + MHD_destroy_response (response); + + return ret; +} +@end verbatim +@noindent + +Note how we ask @emph{MHD} to make its own copy of the message data. The reason behind this will +become clear later. + + +@heading Connection cycle +The decision whether the server is busy or not is made right at the beginning of the connection. To +do that at this stage is especially important for @emph{POST} requests because if no response is +queued at this point, and @code{MHD_YES} returned, @emph{MHD} will not sent any queued messages until +a postprocessor has been created and the post iterator is called at least once. + +@verbatim +static int +answer_to_connection (void *cls, struct MHD_Connection *connection, + const char *url, + const char *method, const char *version, + const char *upload_data, + size_t *upload_data_size, void **con_cls) +{ + if (NULL == *con_cls) + { + struct connection_info_struct *con_info; + + if (nr_of_uploading_clients >= MAXCLIENTS) + return send_page(connection, busypage, MHD_HTTP_SERVICE_UNAVAILABLE); +@end verbatim +@noindent + +If the server is not busy, the @code{connection_info} structure is initialized as usual, with +the addition of a filepointer for each connection. + +@verbatim + con_info = malloc (sizeof (struct connection_info_struct)); + if (NULL == con_info) return MHD_NO; + con_info->fp = 0; + + if (0 == strcmp (method, "POST")) + { + ... + } + else con_info->connectiontype = GET; + + *con_cls = (void*) con_info; + + return MHD_YES; + } +@end verbatim +@noindent + +For @emph{POST} requests, the postprocessor is created and we register a new uploading client. From +this point on, there are many possible places for errors to occur that make it necessary to interrupt +the uploading process. We need a means of having the proper response message ready at all times. +Therefore, the @code{connection_info} structure is extended to hold the most current response +message so that whenever a response is sent, the client will get the most informative message. Here, +the structure is initialized to "no error". +@verbatim + if (0 == strcmp (method, "POST")) + { + con_info->postprocessor + = MHD_create_post_processor (connection, POSTBUFFERSIZE, + iterate_post, (void*) con_info); + + if (NULL == con_info->postprocessor) + { + free (con_info); + return MHD_NO; + } + + nr_of_uploading_clients++; + + con_info->connectiontype = POST; + con_info->answercode = MHD_HTTP_OK; + con_info->answerstring = completepage; + } + else con_info->connectiontype = GET; +@end verbatim +@noindent + +If the connection handler is called for the second time, @emph{GET} requests will be answered with +the @emph{form}. We can keep the buffer under function scope, because we asked @emph{MHD} to make its +own copy of it for as long as it is needed. +@verbatim + if (0 == strcmp (method, "GET")) + { + int ret; + char buffer[1024]; + + sprintf (buffer, askpage, nr_of_uploading_clients); + return send_page (connection, buffer, MHD_HTTP_OK); + } +@end verbatim +@noindent + + +The rest of the @code{answer_to_connection} function is very similar to the @code{simplepost.c} +example, except the more flexible content of the responses. The @emph{POST} data is processed until +there is none left and the execution falls through to return an error page if the connection +constituted no expected request method. +@verbatim + if (0 == strcmp (method, "POST")) + { + struct connection_info_struct *con_info = *con_cls; + + if (0 != *upload_data_size) + { + MHD_post_process (con_info->postprocessor, + upload_data, *upload_data_size); + *upload_data_size = 0; + + return MHD_YES; + } + else + return send_page (connection, con_info->answerstring, + con_info->answercode); + } + + return send_page(connection, errorpage, MHD_HTTP_BAD_REQUEST); +} +@end verbatim +@noindent + + +@heading Storing to data +Unlike the @code{simplepost.c} example, here it is to be expected that post iterator will be called +several times now. This means that for any given connection (there might be several concurrent of them) +the posted data has to be written to the correct file. That is why we store a file handle in every +@code{connection_info}, so that the it is preserved between successive iterations. +@verbatim +static int +iterate_post (void *coninfo_cls, enum MHD_ValueKind kind, + const char *key, + const char *filename, const char *content_type, + const char *transfer_encoding, const char *data, + uint64_t off, size_t size) +{ + struct connection_info_struct *con_info = coninfo_cls; +@end verbatim +@noindent + +Because the following actions depend heavily on correct file processing, which might be error prone, +we default to reporting internal errors in case anything will go wrong. + +@verbatim +con_info->answerstring = servererrorpage; +con_info->answercode = MHD_HTTP_INTERNAL_SERVER_ERROR; +@end verbatim +@noindent + +In the "askpage" @emph{form}, we told the client to label its post data with the "file" key. Anything else +would be an error. + +@verbatim + if (0 != strcmp (key, "file")) return MHD_NO; +@end verbatim +@noindent + +If the iterator is called for the first time, no file will have been opened yet. The @code{filename} +string contains the name of the file (without any paths) the user selected on his system. We want to +take this as the name the file will be stored on the server and make sure no file of that name exists +(or is being uploaded) before we create one (note that the code below technically contains a +race between the two "fopen" calls, but we will overlook this for portability sake). +@verbatim + if (!con_info->fp) + { + if (NULL != (fp = fopen (filename, "rb")) ) + { + fclose (fp); + con_info->answerstring = fileexistspage; + con_info->answercode = MHD_HTTP_FORBIDDEN; + return MHD_NO; + } + + con_info->fp = fopen (filename, "ab"); + if (!con_info->fp) return MHD_NO; + } +@end verbatim +@noindent + + +Occasionally, the iterator function will be called even when there are 0 new bytes to process. The +server only needs to write data to the file if there is some. +@verbatim +if (size > 0) + { + if (!fwrite (data, size, sizeof(char), con_info->fp)) + return MHD_NO; + } +@end verbatim +@noindent + +If this point has been reached, everything worked well for this iteration and the response can +be set to success again. If the upload has finished, this iterator function will not be called again. +@verbatim + con_info->answerstring = completepage; + con_info->answercode = MHD_HTTP_OK; + + return MHD_YES; +} +@end verbatim +@noindent + + +The new client was registered when the postprocessor was created. Likewise, we unregister the client +on destroying the postprocessor when the request is completed. +@verbatim +void request_completed (void *cls, struct MHD_Connection *connection, + void **con_cls, + enum MHD_RequestTerminationCode toe) +{ + struct connection_info_struct *con_info = *con_cls; + + if (NULL == con_info) return; + + if (con_info->connectiontype == POST) + { + if (NULL != con_info->postprocessor) + { + MHD_destroy_post_processor (con_info->postprocessor); + nr_of_uploading_clients--; + } + + if (con_info->fp) fclose (con_info->fp); + } + + free (con_info); + *con_cls = NULL; +} +@end verbatim +@noindent + + +This is essentially the whole example @code{largepost.c}. + + +@heading Remarks +Now that the clients are able to create files on the server, security aspects are becoming even more +important than before. Aside from proper client authentication, the server should always make sure +explicitly that no files will be created outside of a dedicated upload directory. In particular, +filenames must be checked to not contain strings like "../". |