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