New type and methods for working with multipart HTTP bodies (eg,
authorDan Winship <danw@src.gnome.org>
Wed, 1 Oct 2008 21:53:26 +0000 (21:53 +0000)
committerDan Winship <danw@src.gnome.org>
Wed, 1 Oct 2008 21:53:26 +0000 (21:53 +0000)
* libsoup/soup-multipart.c: New type and methods for working with
multipart HTTP bodies (eg, multipart/form-data and
multipart/byte-ranges)

* libsoup/soup-message-headers.c (soup_message_headers_get_ranges)
(soup_message_headers_set_ranges)
(soup_message_headers_set_range)
(soup_message_headers_get_content_range)
(soup_message_headers_set_content_range): New methods for dealing
with the Range and Content-Range headers.

* libsoup/soup-form.h (SOUP_FORM_MIME_TYPE_URLENCODED)
(SOUP_FORM_MIME_TYPE_MULTIPART): #define these MIME types here

* libsoup/soup-form.c (soup_form_decode_multipart): new utility
for parsing multipart/form-data forms.
(soup_form_request_new_from_multipart): new utility for
constructing multipart/form-data forms

* libsoup/soup-headers.c (soup_headers_parse): this is now
non-static, for use by soup-multipart

* libsoup/soup-message-server-io.c (get_response_headers)
(handle_partial_get): if the client requested a partial GET, and
the SoupServer is returning the full body, rebuild the response to
include only the requested range instead

* tests/forms-test.c: renamed from query-test and updated to do
both application/x-www-form-urlencoded and multipart/form-data
tests

* tests/range-test.c: test of Range/Content-Range functionality

svn path=/trunk/; revision=1176

19 files changed:
ChangeLog
docs/reference/libsoup-2.4-docs.sgml
docs/reference/libsoup-2.4-sections.txt
libsoup/Makefile.am
libsoup/soup-form.c
libsoup/soup-form.h
libsoup/soup-headers.c
libsoup/soup-headers.h
libsoup/soup-message-headers.c
libsoup/soup-message-headers.h
libsoup/soup-message-server-io.c
libsoup/soup-multipart.c [new file with mode: 0644]
libsoup/soup-multipart.h [new file with mode: 0644]
libsoup/soup.h
tests/Makefile.am
tests/forms-test.c [new file with mode: 0644]
tests/header-parsing.c
tests/query-test.c [deleted file]
tests/range-test.c [new file with mode: 0644]

index 7f689f7..b3f30ee 100644 (file)
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,40 @@
 2008-10-01  Dan Winship  <danw@gnome.org>
 
+       * libsoup/soup-multipart.c: New type and methods for working with
+       multipart HTTP bodies (eg, multipart/form-data and
+       multipart/byte-ranges)
+
+       * libsoup/soup-message-headers.c (soup_message_headers_get_ranges)
+       (soup_message_headers_set_ranges)
+       (soup_message_headers_set_range)
+       (soup_message_headers_get_content_range)
+       (soup_message_headers_set_content_range): New methods for dealing
+       with the Range and Content-Range headers.
+
+       * libsoup/soup-form.h (SOUP_FORM_MIME_TYPE_URLENCODED)
+       (SOUP_FORM_MIME_TYPE_MULTIPART): #define these MIME types here
+
+       * libsoup/soup-form.c (soup_form_decode_multipart): new utility
+       for parsing multipart/form-data forms.
+       (soup_form_request_new_from_multipart): new utility for
+       constructing multipart/form-data forms
+
+       * libsoup/soup-headers.c (soup_headers_parse): this is now
+       non-static, for use by soup-multipart
+
+       * libsoup/soup-message-server-io.c (get_response_headers)
+       (handle_partial_get): if the client requested a partial GET, and
+       the SoupServer is returning the full body, rebuild the response to
+       include only the requested range instead
+
+       * tests/forms-test.c: renamed from query-test and updated to do
+       both application/x-www-form-urlencoded and multipart/form-data
+       tests
+
+       * tests/range-test.c: test of Range/Content-Range functionality
+
+2008-10-01  Dan Winship  <danw@gnome.org>
+
        * libsoup/soup-headers.c (soup_header_parse_param_list)
        (soup_header_parse_semi_param_list): Update these to deal with
        RFC2231-encoded UTF-8 header params
index d4c3239..25cd2af 100644 (file)
@@ -14,8 +14,7 @@
   </chapter>
 
   <chapter>
-    <title>API Reference</title>
-    <xi:include href="xml/soup-address.xml"/>
+    <title>Core API</title>
     <xi:include href="xml/soup-auth.xml"/>
     <xi:include href="xml/soup-auth-domain.xml"/>
     <xi:include href="xml/soup-auth-domain-basic.xml"/>
     <xi:include href="xml/soup-message-headers.xml"/>
     <xi:include href="xml/soup-message-body.xml"/>
     <xi:include href="xml/soup-method.xml"/>
+    <xi:include href="xml/soup-multipart.xml"/>
     <xi:include href="xml/soup-server.xml"/>
     <xi:include href="xml/soup-session.xml"/>
     <xi:include href="xml/soup-session-async.xml"/>
     <xi:include href="xml/soup-session-sync.xml"/>
-    <xi:include href="xml/soup-socket.xml"/>
     <xi:include href="xml/soup-status.xml"/>
     <xi:include href="xml/soup-uri.xml"/>
-    <xi:include href="xml/soup-value-utils.xml"/>
-    <xi:include href="xml/soup-xmlrpc.xml"/>
     <xi:include href="xml/soup-misc.xml"/>
   </chapter>
 
+  <chapter>
+    <title>Web Services APIs</title>
+    <xi:include href="xml/soup-forms.xml"/>
+    <xi:include href="xml/soup-xmlrpc.xml"/>
+    <xi:include href="xml/soup-value-utils.xml"/>
+  </chapter>
+
+  <chapter>
+    <title>Low-level Networking API</title>
+    <xi:include href="xml/soup-address.xml"/>
+    <xi:include href="xml/soup-socket.xml"/>
+  </chapter>
+
   <index>
     <title>Index</title>
   </index>
index 43944ec..dea6b6c 100644 (file)
@@ -113,6 +113,14 @@ soup_message_headers_set_content_type
 <SUBSECTION>
 soup_message_headers_get_content_disposition
 soup_message_headers_set_content_disposition
+<SUBSECTION>
+SoupRange
+soup_message_headers_get_ranges
+soup_message_headers_set_ranges
+soup_message_headers_set_range
+soup_message_headers_free_ranges
+soup_message_headers_get_content_range
+soup_message_headers_set_content_range
 <SUBSECTION Standard>
 SOUP_TYPE_MESSAGE_HEADERS
 soup_message_headers_get_type
@@ -569,18 +577,10 @@ soup_date_to_timeval
 soup_date_is_past
 soup_date_free
 <SUBSECTION>
-soup_form_decode
-soup_form_encode
-soup_form_encode_datalist
-soup_form_encode_hash
-soup_form_encode_valist
-soup_form_request_new
-soup_form_request_new_from_datalist
-soup_form_request_new_from_hash
-<SUBSECTION>
 soup_headers_parse_request
 soup_headers_parse_response
 soup_headers_parse_status_line
+soup_headers_parse
 <SUBSECTION>
 soup_header_parse_list
 soup_header_parse_quality_list
@@ -610,6 +610,24 @@ soup_form_encode_urlencoded_list
 </SECTION>
 
 <SECTION>
+<FILE>soup-forms</FILE>
+<TITLE>HTML Form Support</TITLE>
+<SUBSECTION>
+SOUP_FORM_MIME_TYPE_MULTIPART
+SOUP_FORM_MIME_TYPE_URLENCODED
+soup_form_decode
+soup_form_decode_multipart
+soup_form_encode
+soup_form_encode_datalist
+soup_form_encode_hash
+soup_form_encode_valist
+soup_form_request_new
+soup_form_request_new_from_datalist
+soup_form_request_new_from_hash
+soup_form_request_new_from_multipart
+</SECTION>
+
+<SECTION>
 <FILE>soup-xmlrpc</FILE>
 <TITLE>XMLRPC Support</TITLE>
 <SUBSECTION>
@@ -740,3 +758,22 @@ SOUP_IS_COOKIE_JAR_CLASS
 SOUP_TYPE_COOKIE_JAR
 soup_cookie_jar_get_type
 </SECTION>
+
+<SECTION>
+<FILE>soup-multipart</FILE>
+<TITLE>SoupMultipart</TITLE>
+SoupMultipart
+soup_multipart_new
+soup_multipart_new_from_message
+soup_multipart_free
+<SUBSECTION>
+soup_multipart_get_length
+soup_multipart_get_part
+soup_multipart_append_part
+soup_multipart_append_form_string
+soup_multipart_append_form_file
+soup_multipart_to_message
+<SUBSECTION Standard>
+SOUP_TYPE_MULTIPART
+soup_multipart_get_type
+</SECTION>
index 97f3bf6..894e79c 100644 (file)
@@ -62,6 +62,7 @@ soup_headers =                        \
        soup-message-headers.h  \
        soup-method.h           \
        soup-misc.h             \
+       soup-multipart.h        \
        soup-portability.h      \
        soup-server.h           \
        soup-session.h          \
@@ -131,6 +132,7 @@ libsoup_2_4_la_SOURCES =            \
        soup-message-server-io.c        \
        soup-method.c                   \
        soup-misc.c                     \
+       soup-multipart.c                \
        soup-nossl.c                    \
        soup-path-map.h                 \
        soup-path-map.c                 \
index 0984ad8..3ef9da6 100644 (file)
 #include "soup-message.h"
 #include "soup-uri.h"
 
+/**
+ * SECTION:soup-form
+ * @short_description: HTML form handling
+ * @see_also: #SoupMultipart
+ *
+ * libsoup contains several help methods for processing HTML forms as
+ * defined by <ulink
+ * url="http://www.w3.org/TR/html401/interact/forms.html#h-17.13">the
+ * HTML 4.01 specification</ulink>.
+ **/
+
+/**
+ * SOUP_FORM_MIME_TYPE_URLENCODED:
+ *
+ * A macro containing the value
+ * <literal>"application/x-www-form-urlencoded"</literal>; the default
+ * MIME type for POSTing HTML form data.
+ **/
+
+/**
+ * SOUP_FORM_MIME_TYPE_MULTIPART:
+ *
+ * A macro containing the value
+ * <literal>"multipart/form-data"</literal>; the MIME type used for
+ * posting form data that contains files to be uploaded.
+ **/
+
 #define XDIGIT(c) ((c) <= '9' ? (c) - '0' : ((c) & 0x4F) - 'A' + 10)
 #define HEXCHAR(s) ((XDIGIT (s[1]) << 4) + XDIGIT (s[2]))
 
@@ -80,6 +107,98 @@ soup_form_decode (const char *encoded_form)
        return form_data_set;
 }
 
+/**
+ * soup_form_decode_multipart:
+ * @msg: a #SoupMessage containing a "multipart/form-data" request body
+ * @file_control_name: the name of the HTML file upload control, or %NULL
+ * @filename: return location for the name of the uploaded file
+ * @content_type: return location for the MIME type of the uploaded file
+ * @file: return location for the uploaded file data
+ *
+ * Decodes the "multipart/form-data" request in @msg; this is a
+ * convenience method for the case when you have a single file upload
+ * control in a form. (Or when you don't have any file upload
+ * controls, but are still using "multipart/form-data" anyway.) Pass
+ * the name of the file upload control in @file_control_name, and
+ * soup_form_decode_multipart() will extract the uploaded file data
+ * into @filename, @content_type, and @file. All of the other form
+ * control data will be returned (as strings, as with
+ * soup_form_decode()) in the returned #GHashTable.
+ *
+ * You may pass %NULL for @filename and/or @content_type if you do not
+ * care about those fields. soup_form_decode_multipart() may also
+ * return %NULL in those fields if the client did not provide that
+ * information. You must free the returned filename and content-type
+ * with g_free(), and the returned file data with soup_buffer_free().
+ *
+ * If you have a form with more than one file upload control, you will
+ * need to decode it manually, using soup_multipart_new_from_message()
+ * and soup_multipart_get_part().
+ *
+ * Return value: a hash table containing the name/value pairs (other
+ * than @file_control_name) from @msg, which you can free with
+ * g_hash_table_destroy(). On error, it will return %NULL.
+ **/
+GHashTable *
+soup_form_decode_multipart (SoupMessage *msg, const char *file_control_name,
+                           char **filename, char **content_type,
+                           SoupBuffer **file)
+{
+       SoupMultipart *multipart;
+       GHashTable *form_data_set, *params;
+       SoupMessageHeaders *part_headers;
+       SoupBuffer *part_body;
+       char *disposition, *name;
+       int i;
+
+       multipart = soup_multipart_new_from_message (msg->request_headers,
+                                                    msg->request_body);
+       if (!multipart)
+               return NULL;
+
+       if (filename)
+               *filename = NULL;
+       if (content_type)
+               *content_type = NULL;
+       *file = NULL;
+
+       form_data_set = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                              g_free, g_free);
+       for (i = 0; i < soup_multipart_get_length (multipart); i++) {
+               soup_multipart_get_part (multipart, i, &part_headers, &part_body);
+               if (!soup_message_headers_get_content_disposition (
+                           part_headers, &disposition, &params))
+                       continue;
+               name = g_hash_table_lookup (params, "name");
+               if (g_ascii_strcasecmp (disposition, "form-data") != 0 ||
+                   !name) {
+                       g_free (disposition);
+                       g_hash_table_destroy (params);
+                       continue;
+               }
+
+               if (!strcmp (name, file_control_name)) {
+                       if (filename)
+                               *filename = g_strdup (g_hash_table_lookup (params, "filename"));
+                       if (content_type)
+                               *content_type = g_strdup (soup_message_headers_get_content_type (part_headers, NULL));
+                       if (file)
+                               *file = soup_buffer_copy (part_body);
+               } else {
+                       g_hash_table_insert (form_data_set,
+                                            g_strdup (name),
+                                            g_strndup (part_body->data,
+                                                       part_body->length));
+               }
+
+               g_free (disposition);
+               g_hash_table_destroy (params);
+       }
+
+       soup_multipart_free (multipart);
+       return form_data_set;
+}
+
 static void
 append_form_encoded (GString *str, const char *in)
 {
@@ -243,7 +362,7 @@ soup_form_request_for_data (const char *method, const char *uri_string,
 
        if (!strcmp (method, "POST")) {
                soup_message_set_request (
-                       msg, "application/x-www-form-urlencoded",
+                       msg, SOUP_FORM_MIME_TYPE_URLENCODED,
                        SOUP_MEMORY_TAKE,
                        form_data, strlen (form_data));
                form_data = NULL;
@@ -323,3 +442,35 @@ soup_form_request_new_from_datalist (const char *method, const char *uri,
        return soup_form_request_for_data (
                method, uri, soup_form_encode_datalist (form_data_set));
 }
+
+/**
+ * soup_form_request_new_from_multipart:
+ * @uri: the URI to send the form data to
+ * @multipart: a "multipart/form-data" #SoupMultipart
+ *
+ * Creates a new %SoupMessage and sets it up to send @multipart to
+ * @uri via POST.
+ *
+ * To send a <literal>"multipart/form-data"</literal> POST, first
+ * create a #SoupMultipart, using %SOUP_FORM_MIME_TYPE_MULTIPART as
+ * the MIME type. Then use soup_multipart_append_form_string() and
+ * soup_multipart_append_form_file() to add the value of each form
+ * control to the multipart. (These are just convenience methods, and
+ * you can use soup_multipart_append_part() if you need greater
+ * control over the part headers.) Finally, call
+ * soup_form_request_new_from_multipart() to serialize the multipart
+ * structure and create a #SoupMessage.
+ *
+ * Return value: the new %SoupMessage
+ **/
+SoupMessage *
+soup_form_request_new_from_multipart (const char *uri,
+                                     SoupMultipart *multipart)
+{
+       SoupMessage *msg;
+
+       msg = soup_message_new ("POST", uri);
+       soup_multipart_to_message (multipart, msg->request_headers,
+                                  msg->request_body);
+       return msg;
+}
index 9db5abf..f2be1bc 100644 (file)
@@ -7,17 +7,26 @@
 #define  SOUP_FORM_H 1
 
 #include <libsoup/soup-types.h>
+#include <libsoup/soup-multipart.h>
 
 G_BEGIN_DECLS
 
-GHashTable  *soup_form_decode          (const char  *encoded_form);
+#define SOUP_FORM_MIME_TYPE_URLENCODED "application/x-www-form-urlencoded"
+#define SOUP_FORM_MIME_TYPE_MULTIPART  "multipart/form-data"
 
-char        *soup_form_encode          (const char  *first_field,
-                                       ...) G_GNUC_NULL_TERMINATED;
-char        *soup_form_encode_hash     (GHashTable  *form_data_set);
-char        *soup_form_encode_datalist (GData      **form_data_set);
-char        *soup_form_encode_valist   (const char  *first_field,
-                                       va_list      args);
+GHashTable  *soup_form_decode           (const char   *encoded_form);
+GHashTable  *soup_form_decode_multipart (SoupMessage  *msg,
+                                        const char   *file_control_name,
+                                        char        **filename,
+                                        char        **content_type,
+                                        SoupBuffer  **file);
+
+char        *soup_form_encode           (const char   *first_field,
+                                        ...) G_GNUC_NULL_TERMINATED;
+char        *soup_form_encode_hash      (GHashTable   *form_data_set);
+char        *soup_form_encode_datalist  (GData       **form_data_set);
+char        *soup_form_encode_valist    (const char   *first_field,
+                                        va_list       args);
 
 #ifndef LIBSOUP_DISABLE_DEPRECATED
 /* Compatibility with libsoup 2.3.0 */
@@ -26,16 +35,18 @@ char        *soup_form_encode_valist   (const char  *first_field,
 #define soup_form_encode_urlencoded_list soup_form_encode_datalist
 #endif
 
-SoupMessage *soup_form_request_new               (const char  *method,
-                                                 const char  *uri,
-                                                 const char  *first_field,
-                                                 ...) G_GNUC_NULL_TERMINATED;
-SoupMessage *soup_form_request_new_from_hash     (const char  *method,
-                                                 const char  *uri,
-                                                 GHashTable  *form_data_set);
-SoupMessage *soup_form_request_new_from_datalist (const char  *method,
-                                                 const char  *uri,
-                                                 GData      **form_data_set);
+SoupMessage *soup_form_request_new                (const char     *method,
+                                                  const char     *uri,
+                                                  const char     *first_field,
+                                                  ...) G_GNUC_NULL_TERMINATED;
+SoupMessage *soup_form_request_new_from_hash      (const char     *method,
+                                                  const char     *uri,
+                                                  GHashTable     *form_data_set);
+SoupMessage *soup_form_request_new_from_datalist  (const char     *method,
+                                                  const char     *uri,
+                                                  GData         **form_data_set);
+SoupMessage *soup_form_request_new_from_multipart (const char     *uri,
+                                                  SoupMultipart  *multipart);
 
 G_END_DECLS
 
index 788a921..9dcc162 100644 (file)
 #include "soup-misc.h"
 #include "soup-uri.h"
 
-static gboolean
+/**
+ * soup_headers_parse:
+ * @str: the header string (including the Request-Line or Status-Line,
+ * and the trailing blank line)
+ * @len: length of @str up to (but not including) the terminating blank line.
+ * @dest: #SoupMessageHeaders to store the header values in
+ *
+ * Parses the headers of an HTTP request or response in @str and
+ * stores the results in @dest. Beware that @dest may be modified even
+ * on failure.
+ *
+ * This is a low-level method; normally you would use
+ * soup_headers_parse_request() or soup_headers_parse_response().
+ *
+ * Return value: success or failure
+ **/
+gboolean
 soup_headers_parse (const char *str, int len, SoupMessageHeaders *dest)
 {
        const char *headers_start;
index 158c248..59f0e78 100644 (file)
@@ -13,6 +13,10 @@ G_BEGIN_DECLS
 
 /* HTTP Header Parsing */
 
+gboolean    soup_headers_parse              (const char          *str,
+                                            int                  len,
+                                            SoupMessageHeaders  *dest);
+
 guint       soup_headers_parse_request      (const char          *str,
                                             int                  len,
                                             SoupMessageHeaders  *req_headers,
index 2da1b16..5656f3a 100644 (file)
@@ -507,8 +507,8 @@ soup_message_headers_get_encoding (SoupMessageHeaders *hdrs)
                        return hdrs->encoding;
        }
 
-       hdrs->encoding = (hdrs->type == SOUP_MESSAGE_HEADERS_REQUEST) ?
-               SOUP_ENCODING_NONE : SOUP_ENCODING_EOF;
+       hdrs->encoding = (hdrs->type == SOUP_MESSAGE_HEADERS_RESPONSE) ?
+               SOUP_ENCODING_EOF : SOUP_ENCODING_NONE;
        return hdrs->encoding;
 }
 
@@ -661,6 +661,264 @@ soup_message_headers_set_expectations (SoupMessageHeaders *hdrs,
                soup_message_headers_remove (hdrs, "Expect");
 }
 
+/**
+ * SoupRange:
+ * @start: the start of the range
+ * @end: the end of the range
+ *
+ * Represents a byte range as used in the Range header.
+ *
+ * If @end is non-negative, then @start and @end represent the bounds
+ * of of the range, counting from %0. (Eg, the first 500 bytes would be
+ * represented as @start = %0 and @end = %499.)
+ *
+ * If @end is %-1 and @start is non-negative, then this represents a
+ * range starting at @start and ending with the last byte of the
+ * requested resource body. (Eg, all but the first 500 bytes would be
+ * @start = %500, and @end = %-1.)
+ *
+ * If @end is %-1 and @start is negative, then it represents a "suffix
+ * range", referring to the last -@start bytes of the resource body.
+ * (Eg, the last 500 bytes would be @start = %-500 and @end = %-1.)
+ **/
+
+/**
+ * soup_message_headers_get_ranges:
+ * @hdrs: a #SoupMessageHeaders
+ * @total_length: the total_length of the response body
+ * @ranges: return location for an array of #SoupRange
+ * @length: the length of the returned array
+ *
+ * Parses @hdrs's Range header and returns an array of the requested
+ * byte ranges. The returned array must be freed with
+ * soup_message_headers_free_ranges().
+ *
+ * If @total_length is non-0, its value will be used to adjust the
+ * returned ranges to have explicit start and end values. If
+ * @total_length is 0, then some ranges may have an end value of -1,
+ * as described under #SoupRange.
+ *
+ * Return value: %TRUE if @hdrs contained a "Range" header containing
+ * byte ranges which could be parsed, %FALSE otherwise (in which case
+ * @range and @length will not be set).
+ **/
+gboolean
+soup_message_headers_get_ranges (SoupMessageHeaders  *hdrs,
+                                goffset              total_length,
+                                SoupRange          **ranges,
+                                int                 *length)
+{
+       const char *range = soup_message_headers_get (hdrs, "Range");
+       GSList *range_list, *r;
+       GArray *array;
+       SoupRange cur;
+       char *spec, *end;
+
+       if (!range || strncmp (range, "bytes", 5) != 0)
+               return FALSE;
+
+       range += 5;
+       while (g_ascii_isspace (*range))
+               range++;
+       if (*range++ != '=')
+               return FALSE;
+       while (g_ascii_isspace (*range))
+               range++;
+
+       range_list = soup_header_parse_list (range);
+       if (!range_list)
+               return FALSE;
+
+       array = g_array_new (FALSE, FALSE, sizeof (SoupRange));
+       for (r = range_list; r; r = r->next) {
+               spec = r->data;
+               if (*spec == '-') {
+                       cur.start = g_ascii_strtoll (spec, &end, 10) + total_length;
+                       cur.end = total_length - 1;
+               } else {
+                       cur.start = g_ascii_strtoull (spec, &end, 10);
+                       if (*end == '-')
+                               end++;
+                       if (*end)
+                               cur.end = g_ascii_strtoull (end, &end, 10);
+                       else
+                               cur.end = total_length - 1;
+               }
+               if (*end) {
+                       g_array_free (array, TRUE);
+                       soup_header_free_list (range_list);
+                       return FALSE;
+               }
+
+               g_array_append_val (array, cur);
+       }
+
+       soup_header_free_list (range_list);
+       *ranges = (SoupRange *)array->data;
+       *length = array->len;
+       g_array_free (array, FALSE);
+       return TRUE;
+}
+
+/**
+ * soup_message_headers_free_ranges:
+ * @hdrs: a #SoupMessageHeaders
+ * @ranges: an array of #SoupRange
+ *
+ * Frees the array of ranges returned from soup_message_headers_get_ranges().
+ **/
+void
+soup_message_headers_free_ranges (SoupMessageHeaders  *hdrs,
+                                 SoupRange           *ranges)
+{
+       g_free (ranges);
+}
+
+/**
+ * soup_message_headers_set_ranges:
+ * @hdrs: a #SoupMessageHeaders
+ * @ranges: an array of #SoupRange
+ * @length: the length of @range
+ *
+ * Sets @hdrs's Range header to request the indicated ranges. (If you
+ * only want to request a single range, you can use
+ * soup_message_headers_set_range().)
+ **/
+void
+soup_message_headers_set_ranges (SoupMessageHeaders  *hdrs,
+                                SoupRange           *ranges,
+                                int                  length)
+{
+       GString *header;
+       int i;
+
+       header = g_string_new ("bytes=");
+       for (i = 0; i < length; i++) {
+               if (i > 0)
+                       g_string_append_c (header, ',');
+               if (ranges[i].end >= 0) {
+                       g_string_append_printf (header, "%" G_GINT64_FORMAT "-%" G_GINT64_FORMAT,
+                                               ranges[i].start, ranges[i].end);
+               } else if (ranges[i].start >= 0) {
+                       g_string_append_printf (header,"%" G_GINT64_FORMAT "-",
+                                               ranges[i].start);
+               } else {
+                       g_string_append_printf (header, "%" G_GINT64_FORMAT,
+                                               ranges[i].start);
+               }
+       }
+
+       soup_message_headers_replace (hdrs, "Range", header->str);
+       g_string_free (header, TRUE);
+}
+
+/**
+ * soup_message_headers_set_range:
+ * @hdrs: a #SoupMessageHeaders
+ * @start: the start of the range to request
+ * @end: the end of the range to request
+ *
+ * Sets @hdrs's Range header to request the indicated range.
+ * @start and @end are interpreted as in a #SoupRange.
+ *
+ * If you need to request multiple ranges, use
+ * soup_message_headers_set_ranges().
+ **/
+void
+soup_message_headers_set_range (SoupMessageHeaders  *hdrs,
+                               goffset              start,
+                               goffset              end)
+{
+       SoupRange range;
+
+       range.start = start;
+       range.end = end;
+       soup_message_headers_set_ranges (hdrs, &range, 1);
+}
+
+/**
+ * soup_message_headers_get_content_range:
+ * @hdrs: a #SoupMessageHeaders
+ * @start: return value for the start of the range
+ * @end: return value for the end of the range
+ * @total_length: return value for the total length of the resource,
+ * or %NULL if you don't care.
+ *
+ * Parses @hdrs's Content-Range header and returns it in @start,
+ * @end, and @total_length. If the total length field in the header
+ * was specified as "*", then @total_length will be set to -1.
+ *
+ * Return value: %TRUE if @hdrs contained a "Content-Range" header
+ * containing a byte range which could be parsed, %FALSE otherwise.
+ **/
+gboolean
+soup_message_headers_get_content_range (SoupMessageHeaders  *hdrs,
+                                       goffset             *start,
+                                       goffset             *end,
+                                       goffset             *total_length)
+{
+       const char *header = soup_message_headers_get (hdrs, "Content-Range");
+       goffset length;
+       char *p;
+
+       if (!header || strncmp (header, "bytes ", 6) != 0)
+               return FALSE;
+
+       header += 6;
+       while (g_ascii_isspace (*header))
+               header++;
+       if (!g_ascii_isdigit (*header))
+               return FALSE;
+
+       *start = g_ascii_strtoull (header, &p, 10);
+       if (*p != '-')
+               return FALSE;
+       *end = g_ascii_strtoull (p + 1, &p, 10);
+       if (*p != '/')
+               return FALSE;
+       p++;
+       if (*p == '*') {
+               length = -1;
+               p++;
+       } else
+               length = g_ascii_strtoull (p, &p, 10);
+
+       if (total_length)
+               *total_length = length;
+       return *p == '\0';
+}
+
+/**
+ * soup_message_headers_set_content_range:
+ * @hdrs: a #SoupMessageHeaders
+ * @start: the start of the range
+ * @end: the end of the range
+ * @total_length: the total length of the resource, or -1 if unknown
+ *
+ * Sets @hdrs's Content-Range header according to the given values.
+ * (Note that @total_length is the total length of the entire resource
+ * that this is a range of, not simply @end - @start + 1.)
+ **/
+void
+soup_message_headers_set_content_range (SoupMessageHeaders  *hdrs,
+                                       goffset              start,
+                                       goffset              end,
+                                       goffset              total_length)
+{
+       char *header;
+
+       if (total_length >= 0) {
+               header = g_strdup_printf ("bytes %" G_GINT64_FORMAT "-%"
+                                         G_GINT64_FORMAT "/%" G_GINT64_FORMAT,
+                                         start, end, total_length);
+       } else {
+               header = g_strdup_printf ("bytes %" G_GINT64_FORMAT "-%"
+                                         G_GINT64_FORMAT "/*", start, end);
+       }
+       soup_message_headers_replace (hdrs, "Content-Range", header);
+       g_free (header);
+}
+
 static gboolean
 parse_content_foo (SoupMessageHeaders *hdrs, const char *header_name,
                   char **foo, GHashTable **params)
@@ -795,6 +1053,10 @@ soup_message_headers_set_content_type (SoupMessageHeaders  *hdrs,
  * it down to just the final path component, so you do not need to
  * test this yourself.)
  *
+ * Content-Disposition is also used in "multipart/form-data", however
+ * this is handled automatically by #SoupMultipart and the associated
+ * form methods.
+ *
  * Return value: %TRUE if @hdrs contains a "Content-Disposition"
  * header, %FALSE if not (in which case *@disposition and *@params
  * will be unchanged).
index 212fd5c..6572f39 100644 (file)
@@ -14,7 +14,8 @@ GType soup_message_headers_get_type (void);
 
 typedef enum {
        SOUP_MESSAGE_HEADERS_REQUEST,
-       SOUP_MESSAGE_HEADERS_RESPONSE
+       SOUP_MESSAGE_HEADERS_RESPONSE,
+       SOUP_MESSAGE_HEADERS_MULTIPART
 } SoupMessageHeadersType;
 
 SoupMessageHeaders *soup_message_headers_new      (SoupMessageHeadersType type);
@@ -82,6 +83,34 @@ SoupExpectation soup_message_headers_get_expectations    (SoupMessageHeaders *hd
 void            soup_message_headers_set_expectations    (SoupMessageHeaders *hdrs,
                                                          SoupExpectation     expectations);
 
+typedef struct {
+       goffset start;
+       goffset end;
+} SoupRange;
+
+gboolean        soup_message_headers_get_ranges          (SoupMessageHeaders  *hdrs,
+                                                         goffset              total_length,
+                                                         SoupRange          **ranges,
+                                                         int                 *length);
+void            soup_message_headers_free_ranges         (SoupMessageHeaders  *hdrs,
+                                                         SoupRange           *ranges);
+void            soup_message_headers_set_ranges          (SoupMessageHeaders  *hdrs,
+                                                         SoupRange           *ranges,
+                                                         int                  length);
+void            soup_message_headers_set_range           (SoupMessageHeaders  *hdrs,
+                                                         goffset              start,
+                                                         goffset              end);
+
+gboolean        soup_message_headers_get_content_range   (SoupMessageHeaders  *hdrs,
+                                                         goffset             *start,
+                                                         goffset             *end,
+                                                         goffset             *total_length);
+void            soup_message_headers_set_content_range   (SoupMessageHeaders  *hdrs,
+                                                         goffset              start,
+                                                         goffset              end,
+                                                         goffset              total_length);
+
+
 const char *soup_message_headers_get_content_type     (SoupMessageHeaders  *hdrs,
                                                       GHashTable         **params);
 void        soup_message_headers_set_content_type     (SoupMessageHeaders  *hdrs,
index bb02f01..06225f2 100644 (file)
@@ -16,6 +16,7 @@
 #include "soup-address.h"
 #include "soup-auth.h"
 #include "soup-headers.h"
+#include "soup-multipart.h"
 #include "soup-server.h"
 #include "soup-socket.h"
 
@@ -93,6 +94,98 @@ parse_request_headers (SoupMessage *msg, char *headers, guint headers_len,
 }
 
 static void
+handle_partial_get (SoupMessage *msg)
+{
+       SoupRange *ranges;
+       int nranges;
+       SoupBuffer *full_response;
+
+       /* Make sure the message is set up right for us to return a
+        * partial response; it has to be a GET, the status must be
+        * 200 OK (and in particular, NOT already 206 Partial
+        * Content), and the SoupServer must have already filled in
+        * the response body
+        */
+       if (msg->method != SOUP_METHOD_GET ||
+           msg->status_code != SOUP_STATUS_OK ||
+           soup_message_headers_get_encoding (msg->response_headers) !=
+           SOUP_ENCODING_CONTENT_LENGTH ||
+           msg->response_body->length == 0)
+               return;
+
+       /* Oh, and there has to have been a valid Range header on the
+        * request, of course.
+        */
+       if (!soup_message_headers_get_ranges (msg->request_headers,
+                                             msg->response_body->length,
+                                             &ranges, &nranges))
+               return;
+
+       full_response = soup_message_body_flatten (msg->response_body);
+       if (!full_response)
+               return;
+
+       soup_message_set_status (msg, SOUP_STATUS_PARTIAL_CONTENT);
+       soup_message_body_truncate (msg->response_body);
+
+       if (nranges == 1) {
+               SoupBuffer *range_buf;
+
+               /* Single range, so just set Content-Range and fix the body. */
+
+               soup_message_headers_set_content_range (msg->response_headers,
+                                                       ranges[0].start,
+                                                       ranges[0].end,
+                                                       full_response->length);
+               range_buf = soup_buffer_new_subbuffer (full_response,
+                                                      ranges[0].start,
+                                                      ranges[0].end - ranges[0].start + 1);
+               soup_message_body_append_buffer (msg->response_body, range_buf);
+               soup_buffer_free (range_buf);
+       } else {
+               SoupMultipart *multipart;
+               SoupMessageHeaders *part_headers;
+               SoupBuffer *part_body;
+               const char *content_type;
+               int i;
+
+               /* Multiple ranges, so build a multipart/byteranges response
+                * to replace msg->response_body with.
+                */
+
+               multipart = soup_multipart_new ("multipart/byteranges");
+               content_type = soup_message_headers_get (msg->response_headers,
+                                                        "Content-Type");
+               for (i = 0; i < nranges; i++) {
+                       part_headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_MULTIPART);
+                       if (content_type) {
+                               soup_message_headers_append (part_headers,
+                                                            "Content-Type",
+                                                            content_type);
+                       }
+                       soup_message_headers_set_content_range (part_headers,
+                                                               ranges[i].start,
+                                                               ranges[i].end,
+                                                               full_response->length);
+                       part_body = soup_buffer_new_subbuffer (full_response,
+                                                              ranges[i].start,
+                                                              ranges[i].end - ranges[i].start + 1);
+                       soup_multipart_append_part (multipart, part_headers,
+                                                   part_body);
+                       soup_message_headers_free (part_headers);
+                       soup_buffer_free (part_body);
+               }
+
+               soup_multipart_to_message (multipart, msg->response_headers,
+                                          msg->response_body);
+               soup_multipart_free (multipart);
+       }
+
+       soup_buffer_free (full_response);
+       soup_message_headers_free_ranges (msg->request_headers, ranges);
+}
+
+static void
 get_response_headers (SoupMessage *msg, GString *headers,
                      SoupEncoding *encoding, gpointer user_data)
 {
@@ -100,6 +193,8 @@ get_response_headers (SoupMessage *msg, GString *headers,
        SoupMessageHeadersIter iter;
        const char *name, *value;
 
+       handle_partial_get (msg);
+
        g_string_append_printf (headers, "HTTP/1.%c %d %s\r\n",
                                soup_message_get_http_version (msg) == SOUP_HTTP_1_0 ? '0' : '1',
                                msg->status_code, msg->reason_phrase);
diff --git a/libsoup/soup-multipart.c b/libsoup/soup-multipart.c
new file mode 100644 (file)
index 0000000..b7059af
--- /dev/null
@@ -0,0 +1,482 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * soup-multipart.c: multipart HTTP message bodies
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ */
+
+#include <string.h>
+
+#include "soup-multipart.h"
+#include "soup-headers.h"
+
+/**
+ * SECTION:soup-multipart
+ * @short_description: multipart HTTP message bodies
+ * @see_also: #SoupMessageBody, #SoupMessageHeaders
+ *
+ **/
+
+/**
+ * SoupMultipart:
+ *
+ * Represents a multipart HTTP message body, parsed according to the
+ * syntax of RFC 2046. Of particular interest to HTTP are
+ * <literal>multipart/byte-ranges</literal> and
+ * <literal>multipart/form-data</literal>.
+ *
+ * Although the headers of a #SoupMultipart body part will contain the
+ * full headers from that body part, libsoup does not interpret them
+ * according to MIME rules. For example, each body part is assumed to
+ * have "binary" Content-Transfer-Encoding, even if its headers
+ * explicitly state otherwise. In other words, don't try to use
+ * #SoupMultipart for handling real MIME multiparts.
+ **/
+
+struct SoupMultipart {
+       char *mime_type, *boundary;
+       GPtrArray *headers, *bodies;
+};
+
+static SoupMultipart *
+soup_multipart_new_internal (char *mime_type, char *boundary)
+{
+       SoupMultipart *multipart;
+
+       multipart = g_slice_new (SoupMultipart);
+       multipart->mime_type = mime_type;
+       multipart->boundary = boundary;
+       multipart->headers = g_ptr_array_new ();
+       multipart->bodies = g_ptr_array_new ();
+
+       return multipart;
+}
+
+static char *
+generate_boundary (void)
+{
+       static int counter;
+       struct {
+               GTimeVal timeval;
+               int counter;
+       } data;
+
+       g_get_current_time (&data.timeval);
+       data.counter = counter++;
+
+       /* The maximum boundary string length is 69 characters, and a
+        * stringified SHA256 checksum is 64 bytes long.
+        */
+       return g_compute_checksum_for_data (G_CHECKSUM_SHA256,
+                                           (const guchar *)&data,
+                                           sizeof (data));
+}
+
+/**
+ * soup_multipart_new:
+ * @mime_type: the MIME type of the multipart to create.
+ *
+ * Creates a new empty #SoupMultipart with a randomly-generated
+ * boundary string. Note that @mime_type must be the full MIME type,
+ * including "multipart/".
+ *
+ * Return value: a new empty #SoupMultipart of the given @mime_type
+ **/
+SoupMultipart *
+soup_multipart_new (const char *mime_type)
+{
+       return soup_multipart_new_internal (g_strdup (mime_type),
+                                           generate_boundary ());
+}
+
+static const char *
+find_boundary (const char *start, const char *boundary, int boundary_len)
+{
+       const char *b, *end;
+
+       end = start + 2;
+       while ((b = strstr (end, boundary))) {
+               end = b + boundary_len;
+               if (b[-1] == '-' && b[-2] == '-' &&
+                   (b == start + 2 || (b[-3] == '\n' && b[-4] == '\r'))) {
+                       if ((end[0] == '-' && end[1] == '-') ||
+                           (end[0] == '\r' && end[1] == '\n'))
+                               return b - 2;
+               }
+       }
+       return NULL;
+}
+
+/**
+ * soup_multipart_new_from_message:
+ * @headers: the headers of the HTTP message to parse
+ * @body: the body of the HTTP message to parse
+ *
+ * Parses @headers and @body to form a new #SoupMultipart
+ *
+ * Return value: a new #SoupMultipart (or %NULL if the message couldn't
+ * be parsed or wasn't multipart).
+ **/
+SoupMultipart *
+soup_multipart_new_from_message (SoupMessageHeaders *headers,
+                                SoupMessageBody *body)
+{
+       SoupMultipart *multipart;
+       const char *content_type, *boundary;
+       GHashTable *params;
+       int boundary_len;
+       SoupBuffer *flattened;
+       const char *start, *split, *end;
+       SoupMessageHeaders *part_headers;
+       SoupBuffer *part_body;
+
+       content_type = soup_message_headers_get_content_type (headers, &params);
+       if (!content_type)
+               return NULL;
+
+       boundary = g_hash_table_lookup (params, "boundary");
+       if (strncmp (content_type, "multipart/", 10) != 0 || !boundary) {
+               g_hash_table_destroy (params);
+               return NULL;
+       }
+
+       multipart = soup_multipart_new_internal (
+               g_strdup (content_type), g_strdup (boundary));
+       g_hash_table_destroy (params);
+
+       flattened = soup_message_body_flatten (body);
+       boundary = multipart->boundary;
+       boundary_len = strlen (boundary);
+
+       /* skip preamble */
+       start = find_boundary (flattened->data, boundary, boundary_len);
+       if (!start) {
+               soup_multipart_free (multipart);
+               return NULL;
+       }
+
+       while (start[2 + boundary_len] != '-') {
+               end = find_boundary (start + 2 + boundary_len, boundary, boundary_len);
+               if (!end) {
+                       soup_multipart_free (multipart);
+                       return NULL;
+               }
+
+               split = strstr (start, "\r\n\r\n");
+               if (!split || split > end) {
+                       soup_multipart_free (multipart);
+                       return NULL;
+               }
+               split += 4;
+
+               /* @start points to the start of the boundary line
+                * preceding this part, and @split points to the end
+                * of the headers / start of the body.
+                *
+                * We tell soup_headers_parse() to start parsing at
+                * @start, because it skips the first line of the
+                * input anyway (expecting it to be either a
+                * Request-Line or Status-Line).
+                */
+               part_headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_MULTIPART);
+               g_ptr_array_add (multipart->headers, part_headers);
+               if (!soup_headers_parse (start, split - 2 - start,
+                                        part_headers)) {
+                       soup_multipart_free (multipart);
+                       return NULL;
+               }
+
+               /* @split, as previously mentioned, points to the
+                * start of the body, and @end points to the start of
+                * the following boundary line, which is to say 2 bytes
+                * after the end of the body.
+                */
+               part_body = soup_buffer_new_subbuffer (flattened,
+                                                      split - flattened->data,
+                                                      end - 2 - split);
+               g_ptr_array_add (multipart->bodies, part_body);
+
+               start = end;
+       }
+
+       soup_buffer_free (flattened);
+       return multipart;
+}
+
+/**
+ * soup_multipart_get_length:
+ * @multipart: a #SoupMultipart
+ *
+ * Gets the number of body parts in @multipart
+ *
+ * Return value: the number of body parts in @multipart
+ **/
+int
+soup_multipart_get_length (SoupMultipart *multipart)
+{
+       return multipart->bodies->len;
+}
+
+/**
+ * soup_multipart_get_part:
+ * @multipart: a #SoupMultipart
+ * @part: the part number to get (counting from 0)
+ * @headers: return location for the MIME part headers
+ * @body: return location for the MIME part body
+ *
+ * Gets the indicated body part from @multipart.
+ *
+ * Return value: %TRUE on success, %FALSE if @part is out of range (in
+ * which case @headers and @body won't be set)
+ **/
+gboolean
+soup_multipart_get_part (SoupMultipart *multipart, int part,
+                        SoupMessageHeaders **headers, SoupBuffer **body)
+{
+       if (part < 0 || part >= multipart->bodies->len)
+               return FALSE;
+       *headers = multipart->headers->pdata[part];
+       *body = multipart->bodies->pdata[part];
+       return TRUE;
+}
+
+/**
+ * soup_multipart_append_part:
+ * @multipart: a #SoupMultipart
+ * @headers: the MIME part headers
+ * @body: the MIME part body
+ *
+ * Adds a new MIME part to @multipart with the given headers and body.
+ * (The multipart will make its own copies of @headers and @body, so
+ * you should free your copies if you are not using them for anything
+ * else.)
+ **/
+void
+soup_multipart_append_part (SoupMultipart      *multipart,
+                           SoupMessageHeaders *headers,
+                           SoupBuffer         *body)
+{
+       SoupMessageHeaders *headers_copy;
+       SoupMessageHeadersIter iter;
+       const char *name, *value;
+
+       /* Copying @headers is annoying, but the alternatives seem
+        * worse:
+        *
+        * 1) We don't want to use g_boxed_copy, because
+        *    SoupMessageHeaders actually implements that as just a
+        *    ref, which would be confusing since SoupMessageHeaders
+        *    is mutable and the caller might modify @headers after
+        *    appending it.
+        *
+        * 2) We can't change SoupMessageHeaders to not just do a ref
+        *    from g_boxed_copy, because that would break language
+        *    bindings (which need to be able to hold a ref on
+        *    msg->request_headers, but don't want to duplicate it).
+        *
+        * 3) We don't want to steal the reference to @headers,
+        *    because then we'd have to either also steal the
+        *    reference to @body (which would be inconsistent with
+        *    other SoupBuffer methods), or NOT steal the reference to
+        *    @body, in which case there'd be inconsistency just
+        *    between the two arguments of this method!
+        */
+       headers_copy = soup_message_headers_new (SOUP_MESSAGE_HEADERS_MULTIPART);
+       soup_message_headers_iter_init (&iter, headers);
+       while (soup_message_headers_iter_next (&iter, &name, &value))
+               soup_message_headers_append (headers_copy, name, value);
+
+       g_ptr_array_add (multipart->headers, headers_copy);
+       g_ptr_array_add (multipart->bodies, soup_buffer_copy (body));
+}
+
+/**
+ * soup_multipart_append_form_string:
+ * @multipart: a multipart (presumably of type "multipart/form-data")
+ * @control_name: the name of the control associated with @data
+ * @data: the body data
+ *
+ * Adds a new MIME part containing @data to @multipart, using
+ * "Content-Disposition: form-data", as per the HTML forms
+ * specification. See soup_form_request_new_from_multipart() for more
+ * details.
+ **/ 
+void
+soup_multipart_append_form_string (SoupMultipart *multipart,
+                                  const char *control_name, const char *data)
+{
+       SoupBuffer *body;
+
+       body = soup_buffer_new (SOUP_MEMORY_COPY, data, strlen (data));
+       soup_multipart_append_form_file (multipart, control_name,
+                                        NULL, NULL, body);
+       soup_buffer_free (body);
+}
+
+/**
+ * soup_multipart_append_form_file:
+ * @multipart: a multipart (presumably of type "multipart/form-data")
+ * @control_name: the name of the control associated with this file
+ * @filename: the name of the file, or %NULL if not known
+ * @content_type: the MIME type of the file, or %NULL if not known
+ * @body: the file data
+ *
+ * Adds a new MIME part containing @body to @multipart, using
+ * "Content-Disposition: form-data", as per the HTML forms
+ * specification. See soup_form_request_new_from_multipart() for more
+ * details.
+ **/ 
+void
+soup_multipart_append_form_file (SoupMultipart *multipart,
+                                const char *control_name, const char *filename,
+                                const char *content_type, SoupBuffer *body)
+{
+       SoupMessageHeaders *headers;
+       GString *disposition;
+
+       headers = soup_message_headers_new (SOUP_MESSAGE_HEADERS_MULTIPART);
+       disposition = g_string_new ("form-data; ");
+       soup_header_g_string_append_param (disposition, "name", control_name);
+       if (filename) {
+               g_string_append (disposition, "; ");
+               soup_header_g_string_append_param (disposition, "filename", filename);
+       }
+       soup_message_headers_append (headers, "Content-Disposition",
+                                    disposition->str);
+       g_string_free (disposition, TRUE);
+
+       if (content_type) {
+               soup_message_headers_append (headers, "Content-Type",
+                                            content_type);
+       }
+
+       /* The HTML spec says we need to set Content-Transfer-Encoding
+        * if the data is not 7bit. It probably doesn't actually
+        * matter...
+        */
+       if (content_type && strncmp (content_type, "text/", 5) != 0) {
+               soup_message_headers_append (headers,
+                                            "Content-Transfer-Encoding",
+                                            "binary");
+       } else {
+               soup_message_headers_append (headers,
+                                            "Content-Transfer-Encoding",
+                                            "8bit");
+       }
+
+       g_ptr_array_add (multipart->headers, headers);
+       g_ptr_array_add (multipart->bodies, soup_buffer_copy (body));
+}
+
+/**
+ * soup_multipart_to_message:
+ * @multipart: a #SoupMultipart
+ * @dest_headers: the headers of the HTTP message to serialize @multipart to
+ * @dest_body: the body of the HTTP message to serialize @multipart to
+ *
+ * Serializes @multipart to @dest_headers and @dest_body.
+ **/
+void
+soup_multipart_to_message (SoupMultipart *multipart,
+                          SoupMessageHeaders *dest_headers,
+                          SoupMessageBody *dest_body)
+{
+       SoupMessageHeaders *part_headers;
+       SoupBuffer *part_body;
+       SoupMessageHeadersIter iter;
+       const char *name, *value;
+       GString *str;
+       char *content_type;
+       int i;
+
+       content_type = g_strdup_printf ("%s; boundary=\"%s\"",
+                                       multipart->mime_type,
+                                       multipart->boundary);
+       soup_message_headers_replace (dest_headers, "Content-Type",
+                                     content_type);
+       g_free (content_type);
+
+       for (i = 0; i < multipart->bodies->len; i++) {
+               part_headers = multipart->headers->pdata[i];
+               part_body = multipart->bodies->pdata[i];
+
+               str = g_string_new ("\r\n--");
+               g_string_append (str, multipart->boundary);
+               g_string_append (str, "\r\n");
+               soup_message_headers_iter_init (&iter, part_headers);
+               while (soup_message_headers_iter_next (&iter, &name, &value))
+                       g_string_append_printf (str, "%s: %s\r\n", name, value);
+               g_string_append (str, "\r\n");
+               soup_message_body_append (dest_body, SOUP_MEMORY_TAKE,
+                                         str->str, str->len);
+               g_string_free (str, FALSE);
+
+               soup_message_body_append_buffer (dest_body, part_body);
+       }
+
+       str = g_string_new ("\r\n--");
+       g_string_append (str, multipart->boundary);
+       g_string_append (str, "--\r\n");
+       soup_message_body_append (dest_body, SOUP_MEMORY_TAKE,
+                                 str->str, str->len);
+       g_string_free (str, FALSE);
+
+       /* (The "\r\n" after the close-delimiter seems wrong according
+        * to my reading of RFCs 2046 and 2616, but that's what
+        * everyone else does.)
+        */
+}
+
+/**
+ * soup_multipart_free:
+ * @multipart: a #SoupMultipart
+ *
+ * Frees @multipart
+ **/
+void
+soup_multipart_free (SoupMultipart *multipart)
+{
+       int i;
+
+       g_free (multipart->mime_type);
+       g_free (multipart->boundary);
+       for (i = 0; i < multipart->headers->len; i++)
+               soup_message_headers_free (multipart->headers->pdata[i]);
+       g_ptr_array_free (multipart->headers, TRUE);
+       for (i = 0; i < multipart->bodies->len; i++)
+               soup_buffer_free (multipart->bodies->pdata[i]);
+       g_ptr_array_free (multipart->bodies, TRUE);
+
+       g_slice_free (SoupMultipart, multipart);
+}
+
+static SoupMultipart *
+soup_multipart_copy (SoupMultipart *multipart)
+{
+       SoupMultipart *copy;
+       int i;
+
+       copy = soup_multipart_new_internal (g_strdup (multipart->mime_type),
+                                           g_strdup (multipart->boundary));
+       for (i = 0; i < multipart->bodies->len; i++) {
+               soup_multipart_append_part (copy,
+                                           multipart->headers->pdata[i],
+                                           multipart->bodies->pdata[i]);
+       }
+       return copy;
+}
+
+GType
+soup_multipart_get_type (void)
+{
+       static volatile gsize type_volatile = 0;
+
+       if (g_once_init_enter (&type_volatile)) {
+               GType type = g_boxed_type_register_static (
+                       g_intern_static_string ("SoupMultipart"),
+                       (GBoxedCopyFunc) soup_multipart_copy,
+                       (GBoxedFreeFunc) soup_multipart_free);
+               g_once_init_leave (&type_volatile, type);
+       }
+       return type_volatile;
+}
diff --git a/libsoup/soup-multipart.h b/libsoup/soup-multipart.h
new file mode 100644 (file)
index 0000000..8313eca
--- /dev/null
@@ -0,0 +1,47 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2008 Red Hat, Inc.
+ */
+
+#ifndef SOUP_MULTIPART_H
+#define SOUP_MULTIPART_H 1
+
+#include <libsoup/soup-types.h>
+#include <libsoup/soup-message-body.h>
+#include <libsoup/soup-message-headers.h>
+
+typedef struct SoupMultipart SoupMultipart;
+
+GType soup_multipart_get_type (void);
+#define SOUP_TYPE_MULTIPART (soup_multipart_get_type ())
+
+SoupMultipart *soup_multipart_new              (const char          *mime_type);
+SoupMultipart *soup_multipart_new_from_message (SoupMessageHeaders  *headers,
+                                               SoupMessageBody     *body);
+
+int      soup_multipart_get_length         (SoupMultipart       *multipart);
+gboolean soup_multipart_get_part           (SoupMultipart       *multipart,
+                                           int                  part,
+                                           SoupMessageHeaders **headers,
+                                           SoupBuffer         **body);
+
+void     soup_multipart_append_part        (SoupMultipart       *multipart,
+                                           SoupMessageHeaders  *headers,
+                                           SoupBuffer          *body);
+
+void     soup_multipart_append_form_string (SoupMultipart       *multipart,
+                                           const char          *control_name,
+                                           const char          *data);
+void     soup_multipart_append_form_file   (SoupMultipart       *multipart,
+                                           const char          *control_name,
+                                           const char          *filename,
+                                           const char          *content_type,
+                                           SoupBuffer          *body);
+
+void     soup_multipart_to_message         (SoupMultipart       *multipart,
+                                           SoupMessageHeaders  *dest_headers,
+                                           SoupMessageBody     *dest_body);
+
+void     soup_multipart_free               (SoupMultipart       *multipart);
+
+#endif /* SOUP_MULTIPART_H */
index f096f39..afb4892 100644 (file)
@@ -25,6 +25,7 @@ extern "C" {
 #include <libsoup/soup-message.h>
 #include <libsoup/soup-method.h>
 #include <libsoup/soup-misc.h>
+#include <libsoup/soup-multipart.h>
 #include <libsoup/soup-server.h>
 #include <libsoup/soup-session-async.h>
 #include <libsoup/soup-session-feature.h>
index c90f457..ff06482 100644 (file)
@@ -16,6 +16,7 @@ noinst_PROGRAMS =     \
        continue-test   \
        date            \
        dns             \
+       forms-test      \
        get             \
        getbug          \
        header-parsing  \
@@ -38,6 +39,7 @@ context_test_SOURCES = context-test.c $(TEST_SRCS)
 continue_test_SOURCES = continue-test.c $(TEST_SRCS)
 date_SOURCES = date.c $(TEST_SRCS)
 dns_SOURCES = dns.c
+forms_test_SOURCES = forms-test.c $(TEST_SRCS)
 get_SOURCES = get.c
 getbug_SOURCES = getbug.c
 header_parsing_SOURCES = header-parsing.c $(TEST_SRCS)
@@ -45,8 +47,8 @@ misc_test_SOURCES = misc-test.c $(TEST_SRCS)
 ntlm_test_SOURCES = ntlm-test.c $(TEST_SRCS)
 proxy_test_SOURCES = proxy-test.c $(TEST_SRCS)
 pull_api_SOURCES = pull-api.c $(TEST_SRCS)
+range_test_SOURCES = range-test.c $(TEST_SRCS)
 redirect_test_SOURCES = redirect-test.c $(TEST_SRCS)
-query_test_SOURCES = query-test.c $(TEST_SRCS)
 server_auth_test_SOURCES = server-auth-test.c $(TEST_SRCS)
 simple_httpd_SOURCES = simple-httpd.c
 simple_proxy_SOURCES = simple-proxy.c
@@ -56,10 +58,10 @@ xmlrpc_test_SOURCES = xmlrpc-test.c $(TEST_SRCS)
 xmlrpc_server_test_SOURCES = xmlrpc-server-test.c $(TEST_SRCS)
 
 if HAVE_APACHE
-APACHE_TESTS = auth-test proxy-test pull-api
+APACHE_TESTS = auth-test proxy-test pull-api range-test
 endif
 if HAVE_CURL
-CURL_TESTS = query-test server-auth-test
+CURL_TESTS = forms-test server-auth-test
 endif
 if HAVE_SSL
 SSL_TESTS = ssl-test
diff --git a/tests/forms-test.c b/tests/forms-test.c
new file mode 100644 (file)
index 0000000..e1184bf
--- /dev/null
@@ -0,0 +1,432 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
+/*
+ * Copyright (C) 2007, 2008 Red Hat, Inc.
+ */
+
+#include "config.h"
+
+#include <ctype.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <libsoup/soup.h>
+
+#include "test-utils.h"
+
+static struct {
+       char *title, *name;
+       char *result;
+} tests[] = {
+       /* Both fields must be filled in */
+       { NULL, "Name", "" },
+       { "Mr.", NULL, "" },
+
+       /* Filled-in but empty is OK */
+       { "", "", "Hello,  " },
+       { "", "Name", "Hello,  Name" },
+       { "Mr.", "", "Hello, MR. " },
+
+       /* Simple */
+       { "Mr.", "Name", "Hello, MR. Name" },
+
+       /* Encoding of spaces */
+       { "Mr.", "Full Name", "Hello, MR. Full Name" },
+       { "Mr. and Mrs.", "Full Name", "Hello, MR. AND MRS. Full Name" },
+
+       /* Encoding of "+" */
+       { "Mr.+Mrs.", "Full Name", "Hello, MR.+MRS. Full Name" },
+
+       /* Encoding of non-ASCII. */
+       { "Se\xC3\xB1or", "Nombre", "Hello, SE\xC3\xB1OR Nombre" },
+
+       /* Encoding of '%' */
+       { "Mr.", "Foo %2f Bar", "Hello, MR. Foo %2f Bar" },
+};
+
+static void
+do_hello_test (int n, gboolean extra, const char *uri)
+{
+       GPtrArray *args;
+       char *title_arg = NULL, *name_arg = NULL;
+       char *str_stdout = NULL;
+
+       debug_printf (1, "%2d. '%s' '%s'%s: ", n * 2 + (extra ? 2 : 1),
+                     tests[n].title ? tests[n].title : "(null)",
+                     tests[n].name  ? tests[n].name  : "(null)",
+                     extra ? " + extra" : "");
+
+       args = g_ptr_array_new ();
+       g_ptr_array_add (args, "curl");
+       g_ptr_array_add (args, "-G");
+       if (tests[n].title) {
+               title_arg = soup_form_encode ("title", tests[n].title, NULL);
+               g_ptr_array_add (args, "-d");
+               g_ptr_array_add (args, title_arg);
+       }
+       if (tests[n].name) {
+               name_arg = soup_form_encode ("name", tests[n].name, NULL);
+               g_ptr_array_add (args, "-d");
+               g_ptr_array_add (args, name_arg);
+       }
+       if (extra) {
+               g_ptr_array_add (args, "-d");
+               g_ptr_array_add (args, "extra=something");
+       }
+       g_ptr_array_add (args, (char *)uri);
+       g_ptr_array_add (args, NULL);
+
+       if (g_spawn_sync (NULL, (char **)args->pdata, NULL,
+                         G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL,
+                         NULL, NULL,
+                         &str_stdout, NULL, NULL, NULL)) {
+               if (str_stdout && !strcmp (str_stdout, tests[n].result))
+                       debug_printf (1, "OK!\n");
+               else {
+                       debug_printf (1, "WRONG!\n");
+                       debug_printf (1, "  expected '%s', got '%s'\n",
+                                     tests[n].result,
+                                     str_stdout ? str_stdout : "(error)");
+                       errors++;
+               }
+               g_free (str_stdout);
+       } else {
+               debug_printf (1, "ERROR!\n");
+               errors++;
+       }
+       g_ptr_array_free (args, TRUE);
+       g_free (title_arg);
+       g_free (name_arg);
+}
+
+static void
+do_hello_tests (const char *uri)
+{
+       int n;
+
+       debug_printf (1, "Hello tests (GET, application/x-www-form-urlencoded)\n");
+       for (n = 0; n < G_N_ELEMENTS (tests); n++) {
+               do_hello_test (n, FALSE, uri);
+               do_hello_test (n, TRUE, uri);
+       }
+}
+
+static void
+do_md5_test_curl (const char *uri, const char *file, const char *md5)
+{
+       GPtrArray *args;
+       char *file_arg, *str_stdout;
+
+       debug_printf (1, "  via curl: ");
+
+       args = g_ptr_array_new ();
+       g_ptr_array_add (args, "curl");
+       g_ptr_array_add (args, "-L");
+       g_ptr_array_add (args, "-F");
+       file_arg = g_strdup_printf ("file=@%s", file);
+       g_ptr_array_add (args, file_arg);
+       g_ptr_array_add (args, "-F");
+       g_ptr_array_add (args, "fmt=txt");
+       g_ptr_array_add (args, (char *)uri);
+       g_ptr_array_add (args, NULL);
+
+       if (g_spawn_sync (NULL, (char **)args->pdata, NULL,
+                         G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL,
+                         NULL, NULL,
+                         &str_stdout, NULL, NULL, NULL)) {
+               if (str_stdout && !strcmp (str_stdout, md5))
+                       debug_printf (1, "OK!\n");
+               else {
+                       debug_printf (1, "WRONG!\n");
+                       debug_printf (1, "  expected '%s', got '%s'\n",
+                                     md5, str_stdout ? str_stdout : "(error)");
+                       errors++;
+               }
+               g_free (str_stdout);
+       } else {
+               debug_printf (1, "ERROR!\n");
+               errors++;
+       }
+       g_ptr_array_free (args, TRUE);
+       g_free (file_arg);
+}
+
+static void
+do_md5_test_libsoup (const char *uri, const char *contents, const char *md5)
+{
+       SoupMultipart *multipart;
+       SoupBuffer *buffer;
+       SoupMessage *msg;
+       SoupSession *session;
+
+       debug_printf (1, "  via libsoup: ");
+
+       multipart = soup_multipart_new (SOUP_FORM_MIME_TYPE_MULTIPART);
+       buffer = soup_buffer_new (SOUP_MEMORY_COPY, contents, strlen (contents));
+       soup_multipart_append_form_file (multipart, "file",
+                                        "index.txt", "text/plain",
+                                        buffer);
+       soup_buffer_free (buffer);
+       soup_multipart_append_form_string (multipart, "fmt", "text");
+
+       msg = soup_form_request_new_from_multipart (uri, multipart);
+       soup_multipart_free (multipart);
+
+       session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
+       soup_session_send_message (session, msg);
+
+       if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
+               debug_printf (1, "ERROR: Unexpected status %d %s\n",
+                             msg->status_code, msg->reason_phrase);
+               errors++;
+       } else if (strcmp (msg->response_body->data, md5) != 0) {
+               debug_printf (1, "ERROR: Incorrect response: expected '%s' got '%s'\n",
+                             md5, msg->response_body->data);
+               errors++;
+       } else
+               debug_printf (1, "OK!\n");
+
+       g_object_unref (msg);
+       soup_test_session_abort_unref (session);
+}
+
+static void
+do_md5_tests (const char *uri)
+{
+       char *contents, *md5;
+       gsize length;
+       GError *error = NULL;
+
+       debug_printf (1, "\nMD5 tests (POST, multipart/form-data)\n");
+
+       if (!g_file_get_contents (SRCDIR "/index.txt", &contents, &length, &error)) {
+               debug_printf (1, "  ERROR: Could not read " SRCDIR "/index.txt: %s\n", error->message);
+               g_error_free (error);
+               errors++;
+               return;
+       }
+
+       md5 = g_compute_checksum_for_string (G_CHECKSUM_MD5, contents, length);
+
+       do_md5_test_curl (uri, SRCDIR "/index.txt", md5);
+       do_md5_test_libsoup (uri, contents, md5);
+
+       g_free (contents);
+       g_free (md5);
+}
+
+static void
+hello_callback (SoupServer *server, SoupMessage *msg,
+               const char *path, GHashTable *query,
+               SoupClientContext *context, gpointer data)
+{
+       char *title, *name, *fmt;
+       const char *content_type;
+       GString *buf;
+
+       if (msg->method != SOUP_METHOD_GET && msg->method != SOUP_METHOD_HEAD) {
+               soup_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED);
+               return;
+       }
+
+       if (query) {
+               title = g_hash_table_lookup (query, "title");
+               name = g_hash_table_lookup (query, "name");
+               fmt = g_hash_table_lookup (query, "fmt");
+       } else
+               title = name = fmt = NULL;
+
+       buf = g_string_new (NULL);
+       if (!query || (fmt && !strcmp (fmt, "html"))) {
+               content_type = "text/html";
+               g_string_append (buf, "<html><head><title>forms-test: hello</title></head><body>\r\n");
+               if (title && name) {
+                       /* mumble mumble html-escape... */
+                       g_string_append_printf (buf, "<p>Hello, <b><em>%s</em> %s</b></p>\r\n",
+                                               title, name);
+               }
+               g_string_append (buf, "<form action='/hello' method='get'>"
+                                "<p>Title: <input name='title'></p>"
+                                "<p>Name: <input name='name'></p>"
+                                "<p><input type=hidden name='fmt' value='html'></p>"
+                                "<p><input type=submit></p>"
+                                "</form>\r\n");
+               g_string_append (buf, "</body></html>\r\n");
+       } else {
+               content_type = "text/plain";
+               if (title && name) {
+                       char *uptitle = g_ascii_strup (title, -1);
+                       g_string_append_printf (buf, "Hello, %s %s",
+                                               uptitle, name);
+                       g_free (uptitle);
+               }
+       }
+
+       soup_message_set_response (msg, content_type,
+                                  SOUP_MEMORY_TAKE,
+                                  buf->str, buf->len);
+       g_string_free (buf, FALSE);
+       soup_message_set_status (msg, SOUP_STATUS_OK);
+}
+
+static void
+md5_get_callback (SoupServer *server, SoupMessage *msg,
+                 const char *path, GHashTable *query,
+                 SoupClientContext *context, gpointer data)
+{
+       const char *file = NULL, *md5sum = NULL, *fmt;
+       const char *content_type;
+       GString *buf;
+
+       if (query) {
+               file = g_hash_table_lookup (query, "file");
+               md5sum = g_hash_table_lookup (query, "md5sum");
+               fmt = g_hash_table_lookup (query, "fmt");
+       } else
+               fmt = "html";
+
+       buf = g_string_new (NULL);
+       if (!strcmp (fmt, "html")) {
+               content_type = "text/html";
+               g_string_append (buf, "<html><head><title>forms-test: md5</title></head><body>\r\n");
+               if (file && md5sum) {
+                       /* mumble mumble html-escape... */
+                       g_string_append_printf (buf, "<p>File: %s<br>MD5: <b>%s</b></p>\r\n",
+                                               file, md5sum);
+               }
+               g_string_append (buf, "<form action='/md5' method='post' enctype='multipart/form-data'>"
+                                "<p>File: <input type='file' name='file'></p>"
+                                "<p><input type=hidden name='fmt' value='html'></p>"
+                                "<p><input type=submit></p>"
+                                "</form>\r\n");
+               g_string_append (buf, "</body></html>\r\n");
+       } else {
+               content_type = "text/plain";
+               if (md5sum)
+                       g_string_append_printf (buf, "%s", md5sum);
+       }
+
+       soup_message_set_response (msg, content_type,
+                                  SOUP_MEMORY_TAKE,
+                                  buf->str, buf->len);
+       g_string_free (buf, FALSE);
+       soup_message_set_status (msg, SOUP_STATUS_OK);
+}
+
+static void
+md5_post_callback (SoupServer *server, SoupMessage *msg,
+                  const char *path, GHashTable *query,
+                  SoupClientContext *context, gpointer data)
+{
+       const char *content_type;
+       GHashTable *params;
+       const char *fmt;
+       char *filename, *md5sum, *redirect_uri;
+       SoupBuffer *file;
+       SoupURI *uri;
+
+       content_type = soup_message_headers_get_content_type (msg->request_headers, NULL);
+       if (!content_type || strcmp (content_type, "multipart/form-data") != 0) {
+               soup_message_set_status (msg, SOUP_STATUS_BAD_REQUEST);
+               return;
+       }
+
+       params = soup_form_decode_multipart (msg, "file",
+                                            &filename, NULL, &file);
+       if (!params) {
+               soup_message_set_status (msg, SOUP_STATUS_BAD_REQUEST);
+               return;
+       }
+       fmt = g_hash_table_lookup (params, "fmt");
+
+       md5sum = g_compute_checksum_for_data (G_CHECKSUM_MD5,
+                                             (gpointer)file->data,
+                                             file->length);
+       soup_buffer_free (file);
+
+       uri = soup_uri_copy (soup_message_get_uri (msg));
+       soup_uri_set_query_from_fields (uri,
+                                       "file", filename ? filename : "",
+                                       "md5sum", md5sum,
+                                       "fmt", fmt ? fmt : "html",
+                                       NULL);
+       redirect_uri = soup_uri_to_string (uri, FALSE);
+
+       soup_message_set_status (msg, SOUP_STATUS_SEE_OTHER);
+       soup_message_headers_replace (msg->response_headers, "Location",
+                                     redirect_uri);
+
+       g_free (redirect_uri);
+       soup_uri_free (uri);
+       g_free (md5sum);
+       g_free (filename);
+       g_hash_table_destroy (params);
+}
+
+static void
+md5_callback (SoupServer *server, SoupMessage *msg,
+             const char *path, GHashTable *query,
+             SoupClientContext *context, gpointer data)
+{
+       if (msg->method == SOUP_METHOD_GET || msg->method == SOUP_METHOD_HEAD)
+               md5_get_callback (server, msg, path, query, context, data);
+       else if (msg->method == SOUP_METHOD_POST)
+               md5_post_callback (server, msg, path, query, context, data);
+       else
+               soup_message_set_status (msg, SOUP_STATUS_METHOD_NOT_ALLOWED);
+}
+
+static gboolean run_tests = TRUE;
+
+static GOptionEntry no_test_entry[] = {
+        { "no-tests", 'n', G_OPTION_FLAG_NO_ARG | G_OPTION_FLAG_REVERSE,
+          G_OPTION_ARG_NONE, &run_tests,
+          "Don't run tests, just run the test server", NULL },
+        { NULL }
+};
+
+int
+main (int argc, char **argv)
+{
+       GMainLoop *loop;
+       SoupServer *server;
+       guint port;
+       char *uri_str;
+
+       test_init (argc, argv, no_test_entry);
+
+       server = soup_test_server_new (TRUE);
+       soup_server_add_handler (server, "/hello",
+                                hello_callback, NULL, NULL);
+       soup_server_add_handler (server, "/md5",
+                                md5_callback, NULL, NULL);
+       port =  soup_server_get_port (server);
+
+       loop = g_main_loop_new (NULL, TRUE);
+
+       if (run_tests) {
+               uri_str = g_strdup_printf ("http://localhost:%u/hello", port);
+               do_hello_tests (uri_str);
+               g_free (uri_str);
+
+               uri_str = g_strdup_printf ("http://localhost:%u/md5", port);
+               do_md5_tests (uri_str);
+               g_free (uri_str);
+       } else {
+               printf ("Listening on port %d\n", port);
+               g_main_loop_run (loop);
+       }
+
+       g_main_loop_unref (loop);
+
+       if (run_tests)
+               test_cleanup ();
+       return errors != 0;
+}
index 00f48ca..3c7425a 100644 (file)
@@ -833,7 +833,7 @@ do_rfc2231_tests (void)
 
        debug_printf (1, "rfc2231 tests\n");
 
-       hdrs = soup_message_headers_new (SOUP_MESSAGE_HEADERS_RESPONSE);
+       hdrs = soup_message_headers_new (SOUP_MESSAGE_HEADERS_MULTIPART);
        params = g_hash_table_new (g_str_hash, g_str_equal);
        g_hash_table_insert (params, "filename", RFC2231_TEST_FILENAME);
        soup_message_headers_set_content_disposition (hdrs, "attachment", params);
diff --git a/tests/query-test.c b/tests/query-test.c
deleted file mode 100644 (file)
index 40e21ef..0000000
+++ /dev/null
@@ -1,217 +0,0 @@
-/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
-/*
- * Copyright (C) 2007, 2008 Red Hat, Inc.
- */
-
-#include "config.h"
-
-#include <ctype.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <errno.h>
-#include <signal.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/stat.h>
-#include <unistd.h>
-
-#include <glib.h>
-#include <libsoup/soup-form.h>
-#include <libsoup/soup-message.h>
-#include <libsoup/soup-server.h>
-#include <libsoup/soup-session-sync.h>
-
-#include "test-utils.h"
-
-static struct {
-       const char *title, *name;
-       const char *result;
-} tests[] = {
-       /* Both fields must be filled in */
-       { NULL, "Name", "" },
-       { "Mr.", NULL, "" },
-
-       /* Filled-in but empty is OK */
-       { "", "", "Hello,  " },
-       { "", "Name", "Hello,  Name" },
-       { "Mr.", "", "Hello, MR. " },
-
-       /* Simple */
-       { "Mr.", "Name", "Hello, MR. Name" },
-
-       /* Encoding of spaces */
-       { "Mr.", "Full Name", "Hello, MR. Full Name" },
-       { "Mr. and Mrs.", "Full Name", "Hello, MR. AND MRS. Full Name" },
-
-       /* Encoding of "+" */
-       { "Mr.+Mrs.", "Full Name", "Hello, MR.+MRS. Full Name" },
-
-       /* Encoding of non-ASCII. */
-       { "Se\xC3\xB1or", "Nombre", "Hello, SE\xC3\xB1OR Nombre" },
-
-       /* Encoding of '%' */
-       { "Mr.", "Foo %2f Bar", "Hello, MR. Foo %2f Bar" },
-};
-
-static void
-do_test (int n, gboolean extra, const char *uri)
-{
-       GPtrArray *args;
-       char *title_arg = NULL, *name_arg = NULL;
-       char *str_stdout = NULL;
-
-       debug_printf (1, "%2d. '%s' '%s'%s: ", n * 2 + (extra ? 2 : 1),
-                     tests[n].title ? tests[n].title : "(null)",
-                     tests[n].name  ? tests[n].name  : "(null)",
-                     extra ? " + extra" : "");
-
-       args = g_ptr_array_new ();
-       g_ptr_array_add (args, "curl");
-       g_ptr_array_add (args, "-G");
-       if (tests[n].title) {
-               title_arg = soup_form_encode ("title", tests[n].title, NULL);
-               g_ptr_array_add (args, "-d");
-               g_ptr_array_add (args, title_arg);
-       }
-       if (tests[n].name) {
-               name_arg = soup_form_encode ("name", tests[n].name, NULL);
-               g_ptr_array_add (args, "-d");
-               g_ptr_array_add (args, name_arg);
-       }
-       if (extra) {
-               g_ptr_array_add (args, "-d");
-               g_ptr_array_add (args, "extra=something");
-       }
-       g_ptr_array_add (args, (char *)uri);
-       g_ptr_array_add (args, NULL);
-
-       if (g_spawn_sync (NULL, (char **)args->pdata, NULL,
-                         G_SPAWN_SEARCH_PATH | G_SPAWN_STDERR_TO_DEV_NULL,
-                         NULL, NULL,
-                         &str_stdout, NULL, NULL, NULL)) {
-               if (str_stdout && !strcmp (str_stdout, tests[n].result))
-                       debug_printf (1, "OK!\n");
-               else {
-                       debug_printf (1, "WRONG!\n");
-                       debug_printf (1, "  expected '%s', got '%s'\n",
-                                     tests[n].result,
-                                     str_stdout ? str_stdout : "(error)");
-                       errors++;
-               }
-               g_free (str_stdout);
-       } else {
-               debug_printf (1, "ERROR!\n");
-               errors++;
-       }
-       g_ptr_array_free (args, TRUE);
-       g_free (title_arg);
-       g_free (name_arg);
-}
-
-static void
-do_query_tests (const char *uri)
-{
-       int n;
-
-       for (n = 0; n < G_N_ELEMENTS (tests); n++) {
-               do_test (n, FALSE, uri);
-               do_test (n, TRUE, uri);
-       }
-}
-
-static void
-server_callback (SoupServer *server, SoupMessage *msg,
-                const char *path, GHashTable *query,
-                SoupClientContext *context, gpointer data)
-{
-       char *title, *name, *fmt;
-       const char *content_type;
-       GString *buf;
-
-       if (msg->method != SOUP_METHOD_GET && msg->method != SOUP_METHOD_HEAD) {
-               soup_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED);
-               return;
-       }
-
-       if (query) {
-               title = g_hash_table_lookup (query, "title");
-               name = g_hash_table_lookup (query, "name");
-               fmt = g_hash_table_lookup (query, "fmt");
-       } else
-               title = name = fmt = NULL;
-
-       buf = g_string_new (NULL);
-       if (!query || (fmt && !strcmp (fmt, "html"))) {
-               content_type = "text/html";
-               g_string_append (buf, "<html><head><title>query-test</title></head><body>\r\n");
-               if (title && name) {
-                       /* mumble mumble html-escape... */
-                       g_string_append_printf (buf, "<p>Hello, <b><em>%s</em> %s</b></p>\r\n",
-                                               title, name);
-               }
-               g_string_append (buf, "<form action='/' method='get'>"
-                                "<p>Title: <input name='title'></p>"
-                                "<p>Name: <input name='name'></p>"
-                                "<p><input type=hidden name='fmt' value='html'></p>"
-                                "<p><input type=submit></p>"
-                                "</form>\r\n");
-               g_string_append (buf, "</body></html>\r\n");
-       } else {
-               content_type = "text/plain";
-               if (title && name) {
-                       char *uptitle = g_ascii_strup (title, -1);
-                       g_string_append_printf (buf, "Hello, %s %s",
-                                               uptitle, name);
-                       g_free (uptitle);
-               }
-       }
-
-       soup_message_set_response (msg, content_type,
-                                  SOUP_MEMORY_TAKE,
-                                  buf->str, buf->len);
-       g_string_free (buf, FALSE);
-       soup_message_set_status (msg, SOUP_STATUS_OK);
-}
-
-static gboolean run_tests = TRUE;
-
-static GOptionEntry no_test_entry[] = {
-        { "no-tests", 'n', G_OPTION_FLAG_NO_ARG | G_OPTION_FLAG_REVERSE,
-          G_OPTION_ARG_NONE, &run_tests,
-          "Don't run tests, just run the test server", NULL },
-        { NULL }
-};
-
-int
-main (int argc, char **argv)
-{
-       GMainLoop *loop;
-       SoupServer *server;
-       guint port;
-       char *uri_str;
-
-       test_init (argc, argv, no_test_entry);
-
-       server = soup_test_server_new (TRUE);
-       soup_server_add_handler (server, NULL,
-                                server_callback, NULL, NULL);
-       port =  soup_server_get_port (server);
-
-       loop = g_main_loop_new (NULL, TRUE);
-
-       if (run_tests) {
-               uri_str = g_strdup_printf ("http://localhost:%u", port);
-               do_query_tests (uri_str);
-               g_free (uri_str);
-       } else {
-               printf ("Listening on port %d\n", port);
-               g_main_loop_run (loop);
-       }
-
-       g_main_loop_unref (loop);
-
-       if (run_tests)
-               test_cleanup ();
-       return errors != 0;
-}
diff --git a/tests/range-test.c b/tests/range-test.c
new file mode 100644 (file)
index 0000000..c79693e
--- /dev/null
@@ -0,0 +1,294 @@
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "libsoup/soup.h"
+
+#include "test-utils.h"
+
+SoupBuffer *full_response;
+int total_length;
+char *test_response;
+
+static void
+get_full_response (void)
+{
+       char *contents;
+       gsize length;
+       GError *error = NULL;
+
+       if (!g_file_get_contents (SRCDIR "/index.txt", &contents, &length, &error)) {
+               fprintf (stderr, "Could not read index.txt: %s\n",
+                        error->message);
+               exit (1);
+       }
+
+       full_response = soup_buffer_new (SOUP_MEMORY_TAKE, contents, length);
+       debug_printf (1, "Total response length is %d\n\n", (int)length);
+}
+
+static void
+check_part (SoupMessageHeaders *headers, const char *body, gsize body_len,
+           gboolean check_start_end, int expected_start, int expected_end)
+{
+       goffset start, end, total_length;
+
+       debug_printf (1, "    Content-Range: %s\n",
+                     soup_message_headers_get (headers, "Content-Range"));
+
+       if (!soup_message_headers_get_content_range (headers, &start, &end, &total_length)) {
+               debug_printf (1, "    Could not find/parse Content-Range\n");
+               errors++;
+               return;
+       }
+
+       if (total_length != full_response->length && total_length != -1) {
+               debug_printf (1, "    Unexpected total length %" G_GINT64_FORMAT " in response\n",
+                             total_length);
+               errors++;
+               return;
+       }
+
+       if (check_start_end) {
+               if ((expected_start >= 0 && start != expected_start) ||
+                   (expected_start < 0 && start != full_response->length + expected_start)) {
+                       debug_printf (1, "    Unexpected range start %" G_GINT64_FORMAT " in response\n",
+                                     start);
+                       errors++;
+                       return;
+               }
+
+               if ((expected_end >= 0 && end != expected_end) ||
+                   (expected_end < 0 && end != full_response->length - 1)) {
+                       debug_printf (1, "    Unexpected range end %" G_GINT64_FORMAT " in response\n",
+                                     end);
+                       errors++;
+                       return;
+               }
+       }
+
+       if (end - start + 1 != body_len) {
+               debug_printf (1, "    Range length (%d) does not match body length (%d)\n",
+                             (int)(end - start) + 1,
+                             (int)body_len);
+               errors++;
+               return;
+       }
+
+       memcpy (test_response + start, body, body_len);
+}
+
+static void
+request_single_range (SoupSession *session, char *uri,
+                     int start, int end)
+{
+       SoupMessage *msg;
+
+       msg = soup_message_new ("GET", uri);
+       soup_message_headers_set_range (msg->request_headers, start, end);
+
+       debug_printf (1, "    Range: %s\n",
+                     soup_message_headers_get (msg->request_headers, "Range"));
+
+       soup_session_send_message (session, msg);
+
+       if (msg->status_code != SOUP_STATUS_PARTIAL_CONTENT) {
+               debug_printf (1, "    Unexpected status %d %s\n",
+                             msg->status_code, msg->reason_phrase);
+               g_object_unref (msg);
+               errors++;
+               return;
+       }
+
+       check_part (msg->response_headers, msg->response_body->data,
+                   msg->response_body->length, TRUE, start, end);
+       g_object_unref (msg);
+}
+
+static void
+request_multi_range (SoupSession *session, SoupMessage *msg)
+{
+       SoupMultipart *multipart;
+       const char *content_type;
+       int i, length;
+
+       debug_printf (1, "    Range: %s\n",
+                     soup_message_headers_get (msg->request_headers, "Range"));
+
+       soup_session_send_message (session, msg);
+
+       if (msg->status_code != SOUP_STATUS_PARTIAL_CONTENT) {
+               debug_printf (1, "    Unexpected status %d %s\n",
+                             msg->status_code, msg->reason_phrase);
+               g_object_unref (msg);
+               errors++;
+               return;
+       }
+
+       content_type = soup_message_headers_get_content_type (msg->response_headers, NULL);
+       if (!content_type || strcmp (content_type, "multipart/byteranges") != 0) {
+               debug_printf (1, "    Response Content-Type (%s) was not multipart/byteranges\n",
+                             content_type);
+               g_object_unref (msg);
+               errors++;
+               return;
+       }
+
+       multipart = soup_multipart_new_from_message (msg->response_headers,
+                                                    msg->response_body);
+       if (!multipart) {
+               debug_printf (1, "    Could not parse multipart\n");
+               g_object_unref (msg);
+               errors++;
+               return;
+       }
+
+       length = soup_multipart_get_length (multipart);
+       for (i = 0; i < length; i++) {
+               SoupMessageHeaders *headers;
+               SoupBuffer *body;
+
+               debug_printf (1, "  Part %d\n", i + 1);
+               soup_multipart_get_part (multipart, i, &headers, &body);
+               check_part (headers, body->data, body->length, FALSE, 0, 0);
+       }
+
+       soup_multipart_free (multipart);
+       g_object_unref (msg);
+}
+
+static void
+request_double_range (SoupSession *session, char *uri,
+                     int first_start, int first_end,
+                     int second_start, int second_end)
+{
+       SoupMessage *msg;
+       SoupRange ranges[2];
+
+       msg = soup_message_new ("GET", uri);
+       ranges[0].start = first_start;
+       ranges[0].end = first_end;
+       ranges[1].start = second_start;
+       ranges[1].end = second_end;
+       soup_message_headers_set_ranges (msg->request_headers, ranges, 2);
+
+       request_multi_range (session, msg);
+}
+
+static void
+request_triple_range (SoupSession *session, char *uri,
+                     int first_start, int first_end,
+                     int second_start, int second_end,
+                     int third_start, int third_end)
+{
+       SoupMessage *msg;
+       SoupRange ranges[3];
+
+       msg = soup_message_new ("GET", uri);
+       ranges[0].start = first_start;
+       ranges[0].end = first_end;
+       ranges[1].start = second_start;
+       ranges[1].end = second_end;
+       ranges[2].start = third_start;
+       ranges[2].end = third_end;
+       soup_message_headers_set_ranges (msg->request_headers, ranges, 3);
+
+       request_multi_range (session, msg);
+}
+
+static void
+do_range_test (SoupSession *session, char *uri)
+{
+       int sevenths = full_response->length / 7;
+
+       memset (test_response, 0, full_response->length);
+
+       debug_printf (1, "Requesting %d-%d\n", 0 * sevenths, 1 * sevenths);
+       request_single_range (session, uri,
+                             0 * sevenths, 1 * sevenths);
+
+       /* These two are redundant in terms of data coverage (except
+        * maybe for a single byte because of rounding), but they may
+        * still catch Range-header-generating bugs.
+        */
+       debug_printf (1, "Requesting %d-\n", 6 * sevenths);
+       request_single_range (session, uri,
+                             6 * sevenths, -1);
+       debug_printf (1, "Requesting -%d\n", 1 * sevenths);
+       request_single_range (session, uri,
+                             -1 * sevenths, -1);
+
+       debug_printf (1, "Requesting %d-%d,%d-%d\n",
+                     2 * sevenths, 3 * sevenths,
+                     5 * sevenths, 6 * sevenths);
+       request_double_range (session, uri,
+                             2 * sevenths, 3 * sevenths,
+                             5 * sevenths, 6 * sevenths);
+
+       debug_printf (1, "Requesting %d-%d,%d-%d,%d-%d\n",
+                     3 * sevenths, 4 * sevenths,
+                     1 * sevenths, 2 * sevenths,
+                     4 * sevenths, 5 * sevenths);
+       request_triple_range (session, uri,
+                             3 * sevenths, 4 * sevenths,
+                             1 * sevenths, 2 * sevenths,
+                             4 * sevenths, 5 * sevenths);
+
+       if (memcmp (full_response->data, test_response, full_response->length) != 0) {
+               debug_printf (1, "\nfull_response and test_response don't match\n");
+               errors++;
+       }
+}
+
+static void
+server_handler (SoupServer        *server,
+               SoupMessage       *msg, 
+               const char        *path,
+               GHashTable        *query,
+               SoupClientContext *client,
+               gpointer           user_data)
+{
+       soup_message_set_status (msg, SOUP_STATUS_OK);
+       soup_message_body_append_buffer (msg->response_body,
+                                        full_response);
+}
+
+int
+main (int argc, char **argv)
+{
+       SoupSession *session;
+       SoupServer *server;
+       char *base_uri;
+
+       test_init (argc, argv, NULL);
+       apache_init ();
+
+       get_full_response ();
+       test_response = g_malloc0 (full_response->length);
+
+       session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
+
+       debug_printf (1, "1. Testing against apache\n");
+       do_range_test (session, "http://localhost:47524/");
+
+       debug_printf (1, "\n2. Testing against SoupServer\n");
+       server = soup_test_server_new (FALSE);
+       soup_server_add_handler (server, NULL, server_handler, NULL, NULL);
+       base_uri = g_strdup_printf ("http://localhost:%u/",
+                                   soup_server_get_port (server));
+       do_range_test (session, base_uri);
+
+       soup_test_session_abort_unref (session);
+
+       soup_buffer_free (full_response);
+       g_free (test_response);
+
+       test_cleanup ();
+       return errors != 0;
+}