Improved the libcurl implementation of http_client by adding HTTP_CLIENT_SESSION.is_debug: BOOLEAN

if True, this display verbose debug information in console
Implemented uploading of file for PUT and POST requests
Refactored LIBCURL_HTTP_CLIENT_REQUEST to free used pointer, and also ease extension of the class if needed.
Updated cURL library with addition of {CURL_EXTERNALS}.slist_free_all (..)
This commit is contained in:
Jocelyn Fiat
2012-05-03 16:21:42 +02:00
parent 31cf64f4ad
commit eb04ac5405
9 changed files with 310 additions and 125 deletions

View File

@@ -13,12 +13,12 @@
<library name="net" location="$ISE_LIBRARY\library\net\net-safe.ecf"/>
<library name="curl" location="$ISE_LIBRARY\library\cURL\cURL-safe.ecf">
<condition>
<version type="compiler" min="7.0.8.7340"/>
<version type="compiler" min="7.1.8.8674"/>
</condition>
</library>
<library name="curl_local" location="..\..\..\contrib\ise_library\cURL-safe.ecf">
<condition>
<version type="compiler" max="7.0.8.7339"/>
<version type="compiler" max="7.1.8.8673"/>
</condition>
</library>
<library name="encoder" location="..\..\text\encoder\encoder-safe.ecf"/>

View File

@@ -13,12 +13,12 @@
<library name="net" location="$ISE_LIBRARY\library\net\net.ecf"/>
<library name="curl" location="$ISE_LIBRARY\library\cURL\cURL.ecf">
<condition>
<version type="compiler" min="7.0.8.7340"/>
<version type="compiler" min="7.1.8.8674"/>
</condition>
</library>
<library name="curl_local" location="..\..\..\contrib\ise_library\cURL.ecf">
<condition>
<version type="compiler" max="7.0.8.7339"/>
<version type="compiler" max="7.1.8.8673"/>
</condition>
</library>
<library name="encoder" location="../../text/encoder/encoder.ecf"/>

View File

@@ -31,6 +31,14 @@ feature {NONE} -- Initialization
context: detachable HTTP_CLIENT_REQUEST_CONTEXT
feature -- Status report
is_debug: BOOLEAN
-- Debug mode enabled?
do
Result := session.is_debug
end
feature -- Access
request_method: READABLE_STRING_8

View File

@@ -122,12 +122,16 @@ feature -- Basic operation
deferred
end
feature -- Status report
is_debug: BOOLEAN
-- Produce debug output
feature -- Settings
timeout: INTEGER
-- HTTP transaction timeout in seconds. Defaults to 5 seconds.
connect_timeout: INTEGER
-- HTTP connection timeout in seconds. Defaults to 1 second.
@@ -173,7 +177,15 @@ feature -- Authentication
credentials: detachable READABLE_STRING_32
feature -- Change
feature -- Status setting
set_is_debug (b: BOOLEAN)
do
is_debug := b
end
feature -- Element change
set_base_url (u: like base_url)
do

View File

@@ -48,18 +48,29 @@ feature -- Execution
l_result: INTEGER
l_curl_string: CURL_STRING
l_url: READABLE_STRING_8
p: POINTER
l_form, l_last: CURL_FORM
curl: CURL_EXTERNALS
curl_easy: CURL_EASY_EXTERNALS
l_form: detachable CURL_FORM
l_last: CURL_FORM
l_upload_file: detachable RAW_FILE
l_uploade_file_read_function: detachable LIBCURL_UPLOAD_FILE_READ_FUNCTION
curl: detachable CURL_EXTERNALS
curl_easy: detachable CURL_EASY_EXTERNALS
curl_handle: POINTER
ctx: like context
l_proxy: like proxy
p_slist: POINTER
retried: BOOLEAN
do
ctx := context
if not retried then
curl := session.curl
curl_easy := session.curl_easy
curl_handle := curl_easy.init
curl.global_init
ctx := context
--| Configure cURL session
initialize_curl_session (ctx, curl, curl_easy, curl_handle)
--| URL
l_url := url
if ctx /= Void then
if attached ctx.query_parameters as l_query_params then
@@ -74,12 +85,136 @@ feature -- Execution
end
end
--| Configure cURL session
curl_handle := curl_easy.init
--| URL
debug ("service")
io.put_string ("SERVICE: " + l_url)
io.put_new_line
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)
end
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
curl_easy.set_read_function (curl_handle)
curl_easy.set_write_function (curl_handle)
if is_debug then
curl_easy.set_debug_function (curl_handle)
curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_verbose, 1)
end
create l_curl_string.make_empty
curl_easy.setopt_curl_string (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_writedata, l_curl_string)
create Result.make
l_result := curl_easy.perform (curl_handle)
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)
else
Result.set_error_occurred (True)
Result.status := response_status_code (curl_easy, curl_handle)
end
curl.global_cleanup
curl_easy.cleanup (curl_handle)
else
create Result.make
Result.set_error_occurred (True)
end
if l_form /= Void then
l_form.dispose
end
if curl /= Void and then p_slist /= default_pointer then
curl.slist_free_all (p_slist)
end
if l_upload_file /= Void and then not l_upload_file.is_closed then
l_upload_file.close
end
rescue
retried := True
if curl /= Void then
curl.global_cleanup
curl := Void
end
if curl_easy /= Void and curl_handle /= default_pointer then
curl_easy.cleanup (curl_handle)
curl_easy := Void
end
retry
end
initialize_curl_session (ctx: like context; curl: CURL_EXTERNALS; curl_easy: CURL_EASY_EXTERNALS; curl_handle: POINTER)
local
l_proxy: like proxy
do
--| RESPONSE HEADERS
curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_header, 1)
@@ -118,6 +253,7 @@ feature -- Execution
curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_ssl_verifypeer, 0)
end
--| Request method
if request_method.is_case_insensitive_equal ("GET") then
curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httpget, 1)
elseif request_method.is_case_insensitive_equal ("POST") then
@@ -132,98 +268,6 @@ feature -- Execution
curl_easy.setopt_string (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_customrequest, request_method)
--| ignored
end
--| 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
-- curl_easy.set_debug_function (curl_handle)
-- curl_easy.setopt_integer (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_verbose, 1)
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
-- 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)
--| Not Yet Implemented
end
end
end
curl.global_init
if attached headers as l_headers then
across
l_headers as curs
loop
p := curl.slist_append (p, curs.key + ": " + curs.item)
end
end
p := curl.slist_append (p, "Expect:")
curl_easy.setopt_slist (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_httpheader, p)
curl.global_cleanup
curl_easy.set_read_function (curl_handle)
curl_easy.set_write_function (curl_handle)
create l_curl_string.make_empty
curl_easy.setopt_curl_string (curl_handle, {CURL_OPT_CONSTANTS}.curlopt_writedata, l_curl_string)
debug ("service")
io.put_string ("SERVICE: " + l_url)
io.put_new_line
end
create Result.make
l_result := curl_easy.perform (curl_handle)
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)
else
Result.set_error_occurred (True)
Result.status := response_status_code (curl_easy, curl_handle)
end
curl_easy.cleanup (curl_handle)
end
feature {NONE} -- Implementation

View File

@@ -0,0 +1,78 @@
note
description: "[
LIBCURL_UPLOAD_FILE_READ_FUNCTION is used to uploaded file as part of the client request
]"
date: "$Date$"
revision: "$Revision$"
class
LIBCURL_UPLOAD_FILE_READ_FUNCTION
inherit
LIBCURL_DEFAULT_FUNCTION
redefine
read_function
end
create
make_with_file
feature {NONE} -- Initialization
make_with_file (f: FILE)
require
f_is_open: f.is_open_read
do
make
file_to_read := f
end
feature -- Access
file_to_read: detachable FILE
-- File for sending data
feature -- Basic operation
read_function (a_data_pointer: POINTER; a_size, a_nmemb: INTEGER_32; a_object_id: POINTER): INTEGER_32
-- <Precursor>
local
l_pointer: MANAGED_POINTER
l_max_transfer, l_byte_transfered: INTEGER
do
if attached file_to_read as l_file and then not l_file.after then
l_max_transfer := a_size * a_nmemb
if l_max_transfer > l_file.count - l_file.position then
l_max_transfer := l_file.count - l_file.position
end
create l_pointer.share_from_pointer (a_data_pointer, l_max_transfer)
from
until
l_file.after or l_byte_transfered >= l_max_transfer
loop
l_file.read_character
l_pointer.put_character (l_file.last_character, l_byte_transfered)
l_byte_transfered := l_byte_transfered + 1
end
Result := l_max_transfer
else
-- Result is 0 means stop file transfer
Result := 0
end
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)"
source: "[
Eiffel Software
5949 Hollister Ave., Goleta, CA 93117 USA
Telephone 805-685-1006, Fax 805-685-6869
Website http://www.eiffel.com
Customer support http://support.eiffel.com
]"
end

View File

@@ -77,6 +77,7 @@ feature {NONE} -- Events
execute (req: WSF_REQUEST; res: WSF_RESPONSE)
local
q: detachable STRING_32
n: NATURAL_64
page: WSF_PAGE_RESPONSE
do
debug
@@ -145,6 +146,15 @@ feature {NONE} -- Events
if not q.is_empty then
page.put_string (" : " + q )
end
elseif l_uri.starts_with (test_url ("post/file/01")) then
page.put_header (200, <<["Content-Type", "text/plain"]>>)
page.put_string ("post-file-01")
n := req.content_length_value
if n > 0 then
req.input.read_string (n.to_integer_32)
q := req.input.last_string
page.put_string ("%N" + q)
end
else
page.put_header (200, <<["Content-Type", "text/plain"]>>)
page.put_string ("Hello")
@@ -191,6 +201,7 @@ feature {NONE} -- Events
if attached {HTTP_CLIENT_SESSION} h.new_session ("localhost:" + port_number.out + "/" + b) as sess then
http_session := sess
sess.set_timeout (-1)
sess.set_is_debug (True)
sess.set_connect_timeout (-1)
-- sess.set_proxy ("127.0.0.1", 8888) --| inspect traffic with http://www.fiddler2.com/
end
@@ -220,6 +231,18 @@ feature {NONE} -- Events
end
end
test_post_request_with_filename (a_url: READABLE_STRING_8; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT; a_fn: STRING; a_expected_body: READABLE_STRING_8)
do
get_http_session
if attached http_session as sess then
if attached sess.post_file (a_url, adapted_context (ctx), a_fn) as res and then not res.error_occurred and then attached res.body as l_body then
assert ("Good answer got=%""+l_body+"%" expected=%""+a_expected_body+"%"", l_body.same_string (a_expected_body))
else
assert ("Request %""+a_url+"%" failed", False)
end
end
end
adapted_context (ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT): HTTP_CLIENT_REQUEST_CONTEXT
do
if ctx /= Void then
@@ -246,6 +269,26 @@ feature -- Test routines
end
end
test_post_request_uploaded_file
-- New test routine
local
fn: FILE_NAME
f: RAW_FILE
s: STRING
do
get_http_session
if attached http_session as sess then
create fn.make_temporary_name
create f.make_create_read_write (fn.string)
s := "This is an uploaded file%NTesting purpose%N"
f.put_string (s)
f.close
test_post_request_with_filename ("post/file/01", Void, fn.string, "post-file-01%N" + s)
else
assert ("not_implemented", False)
end
end
test_post_request_01
-- New test routine
local

View File

@@ -7,7 +7,7 @@
<exclude>/EIFGENs$</exclude>
<exclude>/.svn$</exclude>
</file_rule>
<option warning="true" full_class_checking="true" is_attached_by_default="true" void_safety="all" syntax="provisional">
<option debug="true" warning="true" full_class_checking="true" is_attached_by_default="true" void_safety="all" syntax="provisional">
</option>
<setting name="concurrency" value="thread"/>
<library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf"/>
@@ -15,10 +15,10 @@
<library name="connector_null" location="..\..\ewsgi\connectors\null\null-safe.ecf" readonly="false"/>
<library name="dft_nino" location="..\default\nino-safe.ecf"/>
<library name="ewsgi" location="..\..\ewsgi\ewsgi-safe.ecf" readonly="false"/>
<library name="http" location="..\..\..\protocol\http\http-safe.ecf" readonly="false"/>
<library name="http_client" location="..\..\..\client\http_client\http_client-safe.ecf" readonly="false"/>
<library name="testing" location="$ISE_LIBRARY\library\testing\testing-safe.ecf"/>
<library name="thread" location="$ISE_LIBRARY\library\thread\thread-safe.ecf"/>
<library name="http" location="..\..\..\protocol\http\http-safe.ecf" readonly="false"/>
<library name="wsf" location="..\wsf-safe.ecf" readonly="false"/>
<tests name="src" location=".\src\" recursive="true"/>
</target>