From 4508a766836011125d6ee66b0c65585daeca836d Mon Sep 17 00:00:00 2001 From: Jocelyn Fiat Date: Fri, 4 May 2012 12:23:37 +0200 Subject: [PATCH] Now the http_client will send the form parameters urlencoded if this is possible (instead of multipart form data) Note for now, the library does not support sending file and form parameters at the same time. --- library/client/http_client/src/http_client.e | 7 +- .../http_client/src/http_client_request.e | 40 ++-- .../src/http_client_request_context.e | 85 +++++++- .../http_client/src/http_client_session.e | 3 +- .../src/spec/libcurl/libcurl_http_client.e | 4 +- .../libcurl/libcurl_http_client_request.e | 202 +++++++++++------- .../libcurl/libcurl_http_client_session.e | 9 +- .../server/wsf/tests/src/test_wsf_request.e | 14 +- 8 files changed, 244 insertions(+), 120 deletions(-) diff --git a/library/client/http_client/src/http_client.e b/library/client/http_client/src/http_client.e index 08d8e5ed..70445058 100644 --- a/library/client/http_client/src/http_client.e +++ b/library/client/http_client/src/http_client.e @@ -1,6 +1,8 @@ note - description : "Objects that ..." - author : "$Author$" + description : "[ + Instantiate one of the descendant of HTTP_CLIENT + then use `new_session' to create a session of http requests + ]" date : "$Date$" revision : "$Revision$" @@ -10,6 +12,7 @@ deferred class feature -- Status new_session (a_base_url: READABLE_STRING_8): HTTP_CLIENT_SESSION + -- Create a new session using `a_base_url'. deferred end diff --git a/library/client/http_client/src/http_client_request.e b/library/client/http_client/src/http_client_request.e index 11a485ab..7d8cd09c 100644 --- a/library/client/http_client/src/http_client_request.e +++ b/library/client/http_client/src/http_client_request.e @@ -1,8 +1,10 @@ note - description : "Objects that ..." - author : "$Author$" - date : "$Date$" - revision : "$Revision$" + description: "[ + Object representing a http client request + It is mainly used internally by the HTTP_CLIENT_SESSION + ]" + date: "$Date$" + revision: "$Revision$" deferred class HTTP_CLIENT_REQUEST @@ -152,15 +154,14 @@ feature -- Settings feature {NONE} -- Utilities - append_parameters_to_url (a_url: STRING; a_parameters: detachable ARRAY [detachable TUPLE [name: READABLE_STRING_8; value: READABLE_STRING_8]]) + append_parameters_to_url (a_parameters: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32]; a_url: STRING) -- Append parameters `a_parameters' to `a_url' require a_url_attached: a_url /= Void local - i: INTEGER l_first_param: BOOLEAN do - if a_parameters /= Void and then a_parameters.count > 0 then + if a_parameters.count > 0 then if a_url.index_of ('?', 1) > 0 then l_first_param := False elseif a_url.index_of ('&', 1) > 0 then @@ -168,23 +169,22 @@ feature {NONE} -- Utilities else l_first_param := True end + from - i := a_parameters.lower + a_parameters.start until - i > a_parameters.upper + a_parameters.after loop - if attached a_parameters[i] as a_param then - if l_first_param then - a_url.append_character ('?') - else - a_url.append_character ('&') - end - a_url.append_string (a_param.name) - a_url.append_character ('=') - a_url.append_string (a_param.value) - l_first_param := False + if l_first_param then + a_url.append_character ('?') + else + a_url.append_character ('&') end - i := i + 1 + a_url.append (urlencode (a_parameters.key_for_iteration)) + a_url.append_character ('=') + a_url.append (urlencode (a_parameters.item_for_iteration)) + l_first_param := False + a_parameters.forth end end end diff --git a/library/client/http_client/src/http_client_request_context.e b/library/client/http_client/src/http_client_request_context.e index 5057f81e..51ccf90a 100644 --- a/library/client/http_client/src/http_client_request_context.e +++ b/library/client/http_client/src/http_client_request_context.e @@ -1,6 +1,24 @@ note - description: "Summary description for {HTTP_CLIENT_REQUEST_CONTEXT}." - author: "" + description: "[ + Context for HTTP client request + This is used to hold + - headers + - query_parameters + - form parameters + - upload_data or upload_filename + And in addition it has + - credentials_required + - proxy + + Note that any value set in this context class overrides conflicting value eventually + set in associated HTTP_CLIENT_SESSION. + + Warning: for now [2012-May], you can have only one of the following data + - form_parameters + - or upload_data + - or upload_filename + If you set more than one, the priority is then upload_data, then upload_filename, then form_parameters + ]" date: "$Date$" revision: "$Revision$" @@ -29,20 +47,31 @@ feature {NONE} -- Initialization feature -- Settings credentials_required: BOOLEAN + -- If True, the request will precise the HTTP_AUTHORIZATION. feature -- Access headers: HASH_TABLE [READABLE_STRING_8, READABLE_STRING_8] + -- Specific headers to use in addition to the one set in the related HTTP_CLIENT_SESSION + --| note: the value from Current context override the one from the session in case of conflict - query_parameters: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_8] + query_parameters: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32] + -- Query parameters to be appended to the url + --| note: if the url already contains a query_string, the `query_parameters' will be appended to the url - form_parameters: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_8] + form_parameters: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32] + -- Form parameters upload_data: detachable READABLE_STRING_8 + -- Upload data + --| Note: make sure to precise the Content-Type header upload_filename: detachable READABLE_STRING_8 + -- Upload data read from `upload_filename' + --| Note: make sure to precise the Content-Type header proxy: detachable TUPLE [host: READABLE_STRING_8; port: INTEGER] + -- Optional proxy, see {HTTP_CLIENT_SESSION}.proxy feature -- Status report @@ -63,12 +92,17 @@ feature -- Status report feature -- Element change - add_query_parameter (k: READABLE_STRING_8; v: READABLE_STRING_32) + add_header (k: READABLE_STRING_8; v: READABLE_STRING_8) + do + headers.force (v, k) + end + + add_query_parameter (k: READABLE_STRING_32; v: READABLE_STRING_32) do query_parameters.force (v, k) end - add_form_parameter (k: READABLE_STRING_8; v: READABLE_STRING_32) + add_form_parameter (k: READABLE_STRING_32; v: READABLE_STRING_32) do form_parameters.force (v, k) end @@ -101,6 +135,45 @@ feature -- Element change end end +feature -- Conversion helpers + + query_parameters_to_url_encoded_string: STRING_8 + -- `query_parameters' as url-encoded string. + do + Result := parameters_to_url_encoded_string (query_parameters) + end + + form_parameters_to_url_encoded_string: STRING_8 + -- `form_parameters' as url-encoded string. + do + Result := parameters_to_url_encoded_string (form_parameters) + end + +feature {NONE} -- Implementation + + parameters_to_url_encoded_string (ht: HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32]): STRING_8 + do + create Result.make (64) + from + ht.start + until + ht.after + loop + if not Result.is_empty then + Result.append_character ('&') + end + Result.append (url_encoder.encoded_string (ht.key_for_iteration)) + Result.append_character ('=') + Result.append (url_encoder.encoded_string (ht.item_for_iteration)) + ht.forth + end + end + + url_encoder: URL_ENCODER + once + create Result + end + note copyright: "2011-2012, Jocelyn Fiat, Javier Velilla, Eiffel Software and others" license: "Eiffel Forum License v2 (see http://www.eiffel.com/licensing/forum.txt)" diff --git a/library/client/http_client/src/http_client_session.e b/library/client/http_client/src/http_client_session.e index e1b8bdf2..59c3eb30 100644 --- a/library/client/http_client/src/http_client_session.e +++ b/library/client/http_client/src/http_client_session.e @@ -1,13 +1,12 @@ note description : "[ - HTTP_CLIENT_SESSION represent a session + HTTP_CLIENT_SESSION represents a session and is used to call get, post, .... request with predefined settings such as base_url specific common headers timeout and so on ... ]" - author: "$Author$" date: "$Date$" revision: "$Revision$" diff --git a/library/client/http_client/src/spec/libcurl/libcurl_http_client.e b/library/client/http_client/src/spec/libcurl/libcurl_http_client.e index bf4e3fcd..6ff332a2 100644 --- a/library/client/http_client/src/spec/libcurl/libcurl_http_client.e +++ b/library/client/http_client/src/spec/libcurl/libcurl_http_client.e @@ -1,5 +1,7 @@ note - description : "Objects that ..." + description : "[ + Specific implementation of HTTP_CLIENT based on Eiffel cURL library + ]" author : "$Author$" date : "$Date$" revision : "$Revision$" diff --git a/library/client/http_client/src/spec/libcurl/libcurl_http_client_request.e b/library/client/http_client/src/spec/libcurl/libcurl_http_client_request.e index ce81683e..3639649d 100644 --- a/library/client/http_client/src/spec/libcurl/libcurl_http_client_request.e +++ b/library/client/http_client/src/spec/libcurl/libcurl_http_client_request.e @@ -1,8 +1,9 @@ note - description : "Objects that ..." - author : "$Author$" - date : "$Date$" - revision : "$Revision$" + description: "[ + Specific implementation of HTTP_CLIENT_REQUEST based on Eiffel cURL library + ]" + date: "$Date$" + revision: "$Revision$" class LIBCURL_HTTP_CLIENT_REQUEST @@ -58,6 +59,10 @@ feature -- Execution ctx: like context p_slist: POINTER retried: BOOLEAN + l_form_data: detachable HASH_TABLE [READABLE_STRING_32, READABLE_STRING_32] + l_upload_data: detachable READABLE_STRING_8 + l_upload_filename: detachable READABLE_STRING_8 + l_headers: like headers do if not retried then curl := session.curl @@ -73,16 +78,7 @@ feature -- Execution --| URL l_url := url if ctx /= Void then - if attached ctx.query_parameters as l_query_params then - from - l_query_params.start - until - l_query_params.after - loop - append_parameters_to_url (l_url, <<[l_query_params.key_for_iteration, urlencode (l_query_params.item_for_iteration)]>>) - l_query_params.forth - end - end + append_parameters_to_url (ctx.query_parameters, l_url) end debug ("service") @@ -91,78 +87,117 @@ feature -- Execution end curl_easy.setopt_string (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_url, l_url) - --| Header - if attached headers as l_headers then - across - l_headers as curs - loop - p_slist := curl.slist_append (p_slist, curs.key + ": " + curs.item) + l_headers := headers + + -- Context + if ctx /= Void then + --| Credential + if ctx.credentials_required then + if attached credentials as l_credentials then + inspect auth_type_id + when {HTTP_CLIENT_CONSTANTS}.Auth_type_none then + curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httpauth, {CURL_OPT_CONSTANTS}.curlauth_none) + when {HTTP_CLIENT_CONSTANTS}.Auth_type_basic then + curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httpauth, {CURL_OPT_CONSTANTS}.curlauth_basic) + when {HTTP_CLIENT_CONSTANTS}.Auth_type_digest then + curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httpauth, {CURL_OPT_CONSTANTS}.curlauth_digest) + when {HTTP_CLIENT_CONSTANTS}.Auth_type_any then + curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httpauth, {CURL_OPT_CONSTANTS}.curlauth_any) + when {HTTP_CLIENT_CONSTANTS}.Auth_type_anysafe then + curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httpauth, {CURL_OPT_CONSTANTS}.curlauth_anysafe) + else + end + + curl_easy.setopt_string (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_userpwd, l_credentials) + else + --| Credentials not provided ... + end end + + if ctx.has_upload_data then + l_upload_data := ctx.upload_data + end + if ctx.has_upload_filename then + l_upload_filename := ctx.upload_filename + end + if ctx.has_form_data then + l_form_data := ctx.form_parameters + check non_empty_form_data: not l_form_data.is_empty end + if l_upload_data = Void and l_upload_filename = Void then + -- Send as form-urlencoded + if + l_headers.has_key ("Content-Type") and then + attached l_headers.found_item as l_ct + then + if l_ct.starts_with ("application/x-www-form-urlencoded") then + -- Content-Type is already application/x-www-form-urlencoded + l_upload_data := ctx.form_parameters_to_url_encoded_string + else + -- Existing Content-Type and not application/x-www-form-urlencoded + end + else + l_upload_data := ctx.form_parameters_to_url_encoded_string + end + else + create l_form.make + create l_last.make + from + l_form_data.start + until + l_form_data.after + loop + curl.formadd_string_string (l_form, l_last, + {CURL_FORM_CONSTANTS}.curlform_copyname, l_form_data.key_for_iteration, + {CURL_FORM_CONSTANTS}.curlform_copycontents, l_form_data.item_for_iteration, + {CURL_FORM_CONSTANTS}.curlform_end + ) + l_form_data.forth + end + l_last.release_item + curl_easy.setopt_form (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httppost, l_form) + end + end + + if l_upload_data /= Void then + check + post_or_put_request_method: request_method.is_case_insensitive_equal ("POST") + or request_method.is_case_insensitive_equal ("PUT") + end + + curl_easy.setopt_string (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_postfields, l_upload_data) + curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_postfieldsize, l_upload_data.count) + elseif l_upload_filename /= Void then + check + post_or_put_request_method: request_method.is_case_insensitive_equal ("POST") + or request_method.is_case_insensitive_equal ("PUT") + end + + create l_upload_file.make (l_upload_filename) + if l_upload_file.exists and then l_upload_file.is_readable then + curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_upload, 1) + + curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_infilesize, l_upload_file.count) + -- specify callback read function for upload file + create l_uploade_file_read_function.make_with_file (l_upload_file) + l_upload_file.open_read + curl_easy.set_curl_function (l_uploade_file_read_function) + curl_easy.set_read_function (curl_handle) + end + else + check no_upload_data: l_upload_data = Void and l_upload_filename = Void end + end + end -- ctx /= Void + + --| Header + across + l_headers as curs + loop + p_slist := curl.slist_append (p_slist, curs.key + ": " + curs.item) end p_slist := curl.slist_append (p_slist, "Expect:") curl_easy.setopt_slist (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httpheader, p_slist) - --| Credential - if ctx /= Void and then ctx.credentials_required then - if attached credentials as l_credentials then - inspect auth_type_id - when {HTTP_CLIENT_CONSTANTS}.Auth_type_none then - curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httpauth, {CURL_OPT_CONSTANTS}.curlauth_none) - when {HTTP_CLIENT_CONSTANTS}.Auth_type_basic then - curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httpauth, {CURL_OPT_CONSTANTS}.curlauth_basic) - when {HTTP_CLIENT_CONSTANTS}.Auth_type_digest then - curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httpauth, {CURL_OPT_CONSTANTS}.curlauth_digest) - when {HTTP_CLIENT_CONSTANTS}.Auth_type_any then - curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httpauth, {CURL_OPT_CONSTANTS}.curlauth_any) - when {HTTP_CLIENT_CONSTANTS}.Auth_type_anysafe then - curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httpauth, {CURL_OPT_CONSTANTS}.curlauth_anysafe) - else - end - - curl_easy.setopt_string (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_userpwd, l_credentials) - else - --| Credentials not prov ided ... - end - end - - if ctx /= Void and then ctx.has_form_data then - if attached ctx.form_parameters as l_forms and then not l_forms.is_empty then - create l_form.make - create l_last.make - from - l_forms.start - until - l_forms.after - loop - curl.formadd_string_string (l_form, l_last, {CURL_FORM_CONSTANTS}.curlform_copyname, l_forms.key_for_iteration, {CURL_FORM_CONSTANTS}.CURLFORM_COPYCONTENTS, l_forms.item_for_iteration, {CURL_FORM_CONSTANTS}.CURLFORM_END) - l_forms.forth - end - l_last.release_item - curl_easy.setopt_form (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httppost, l_form) - end - end - if ctx /= Void then - if request_method.is_case_insensitive_equal ("POST") or request_method.is_case_insensitive_equal ("PUT") then - if ctx.has_upload_data and then attached ctx.upload_data as l_upload_data then - curl_easy.setopt_string (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_postfields, l_upload_data) - curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_postfieldsize, l_upload_data.count) - end - if ctx.has_upload_filename and then attached ctx.upload_filename as l_upload_filename then - create l_upload_file.make (l_upload_filename) - if l_upload_file.exists and then l_upload_file.is_readable then - curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_upload, 1) - - curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_infilesize, l_upload_file.count) - -- specify callback read function for upload file - create l_uploade_file_read_function.make_with_file (l_upload_file) - l_upload_file.open_read - curl_easy.set_curl_function (l_uploade_file_read_function) - curl_easy.set_read_function (curl_handle) - end - end - end - end - + --| Execution curl_easy.set_read_function (curl_handle) curl_easy.set_write_function (curl_handle) if is_debug then @@ -175,6 +210,7 @@ feature -- Execution create Result.make l_result := curl_easy.perform (curl_handle) + --| Result if l_result = {CURL_CODES}.curle_ok then Result.status := response_status_code (curl_easy, curl_handle) set_header_and_body_to (l_curl_string.string, Result) @@ -183,12 +219,16 @@ feature -- Execution Result.status := response_status_code (curl_easy, curl_handle) end + --| Cleaning + curl.global_cleanup curl_easy.cleanup (curl_handle) else create Result.make Result.set_error_occurred (True) end + + --| Remaining cleaning if l_form /= Void then l_form.dispose end diff --git a/library/client/http_client/src/spec/libcurl/libcurl_http_client_session.e b/library/client/http_client/src/spec/libcurl/libcurl_http_client_session.e index 2cdbf721..bc09c5c9 100644 --- a/library/client/http_client/src/spec/libcurl/libcurl_http_client_session.e +++ b/library/client/http_client/src/spec/libcurl/libcurl_http_client_session.e @@ -1,8 +1,9 @@ note - description : "Objects that ..." - author : "$Author$" - date : "$Date$" - revision : "$Revision$" + description: "[ + Specific implementation of HTTP_CLIENT_SESSION based on Eiffel cURL library + ]" + date: "$Date$" + revision: "$Revision$" class LIBCURL_HTTP_CLIENT_SESSION diff --git a/library/server/wsf/tests/src/test_wsf_request.e b/library/server/wsf/tests/src/test_wsf_request.e index fe73d718..438a6ab8 100644 --- a/library/server/wsf/tests/src/test_wsf_request.e +++ b/library/server/wsf/tests/src/test_wsf_request.e @@ -275,6 +275,7 @@ feature -- Test routines fn: FILE_NAME f: RAW_FILE s: STRING + ctx: HTTP_CLIENT_REQUEST_CONTEXT do get_http_session if attached http_session as sess then @@ -284,6 +285,10 @@ feature -- Test routines f.put_string (s) f.close test_post_request_with_filename ("post/file/01", Void, fn.string, "post-file-01%N" + s) + + create ctx.make + ctx.add_form_parameter ("foo", "bar") + test_post_request_with_filename ("post/file/01", ctx, fn.string, "post-file-01%N" + s) else assert ("not_implemented", False) end @@ -298,10 +303,11 @@ feature -- Test routines if attached http_session as sess then create ctx.make ctx.add_form_parameter ("id", "123") - test_post_request ("post/01", ctx, "post-01 : id=123") - test_post_request ("post/01/?foo=bar", ctx, "post-01(foo=bar) : id=123") - test_post_request ("post/01/?foo=bar&abc=def", ctx, "post-01(foo=bar&abc=def) : id=123") - test_post_request ("post/01/?lst=a&lst=b", ctx, "post-01(lst=[a,b]) : id=123") + ctx.add_form_parameter ("Eiffel", "Web") + test_post_request ("post/01", ctx, "post-01 : " + ctx.form_parameters_to_url_encoded_string) + test_post_request ("post/01/?foo=bar", ctx, "post-01(foo=bar) : " + ctx.form_parameters_to_url_encoded_string) + test_post_request ("post/01/?foo=bar&abc=def", ctx, "post-01(foo=bar&abc=def) : " + ctx.form_parameters_to_url_encoded_string) + test_post_request ("post/01/?lst=a&lst=b", ctx, "post-01(lst=[a,b]) : " + ctx.form_parameters_to_url_encoded_string) else assert ("not_implemented", False) end