From ff9a238f5c067b36756268000e907f9d8408f7c3 Mon Sep 17 00:00:00 2001 From: Jocelyn Fiat Date: Tue, 15 Sep 2015 16:57:01 +0200 Subject: [PATCH] Added https support with Net implementation. Added notion of default HTTP_CLIENT, to be able to build portable code among http client implementation. --- .../network/http_client/http_client-safe.ecf | 42 +- .../src/default/libcurl/default_http_client.e | 15 + .../libcurl_or_net/default_http_client.e | 43 ++ .../src/default/net/default_http_client.e | 15 + .../src/default/null/default_http_client.e | 15 + library/network/http_client/src/http_client.e | 2 +- .../http_client/src/http_client_response.e | 18 + .../spec/{socket => net}/net_http_client.e | 12 - .../{socket => net}/net_http_client_request.e | 386 ++++++++++-------- .../{socket => net}/net_http_client_session.e | 10 +- .../spec/net/no_ssl/net_http_client_info.e | 24 ++ .../net/no_ssl/ssl_network_stream_socket.e | 22 + .../src/spec/net/ssl/net_http_client_info.e | 24 ++ .../network/http_client/tests/test-safe.ecf | 17 +- library/network/http_client/tests/test.e | 28 +- .../http_client/tests/test_http_client_i.e | 27 +- .../tests/test_libcurl_http_client.e | 4 +- .../http_client/tests/test_net_http_client.e | 4 +- 18 files changed, 514 insertions(+), 194 deletions(-) create mode 100644 library/network/http_client/src/default/libcurl/default_http_client.e create mode 100644 library/network/http_client/src/default/libcurl_or_net/default_http_client.e create mode 100644 library/network/http_client/src/default/net/default_http_client.e create mode 100644 library/network/http_client/src/default/null/default_http_client.e rename library/network/http_client/src/spec/{socket => net}/net_http_client.e (85%) rename library/network/http_client/src/spec/{socket => net}/net_http_client_request.e (58%) rename library/network/http_client/src/spec/{socket => net}/net_http_client_session.e (96%) create mode 100644 library/network/http_client/src/spec/net/no_ssl/net_http_client_info.e create mode 100644 library/network/http_client/src/spec/net/no_ssl/ssl_network_stream_socket.e create mode 100644 library/network/http_client/src/spec/net/ssl/net_http_client_info.e diff --git a/library/network/http_client/http_client-safe.ecf b/library/network/http_client/http_client-safe.ecf index 4768dc03..d09ef84b 100644 --- a/library/network/http_client/http_client-safe.ecf +++ b/library/network/http_client/http_client-safe.ecf @@ -24,20 +24,60 @@ + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/library/network/http_client/src/default/libcurl/default_http_client.e b/library/network/http_client/src/default/libcurl/default_http_client.e new file mode 100644 index 00000000..288dc20d --- /dev/null +++ b/library/network/http_client/src/default/libcurl/default_http_client.e @@ -0,0 +1,15 @@ +note + description: "[ + Default HTTP_CLIENT based on LIBCURL_HTTP_CLIENT. + ]" + author: "$Author$" + date: "$Date$" + revision: "$Revision$" + +class + DEFAULT_HTTP_CLIENT + +inherit + LIBCURL_HTTP_CLIENT + +end diff --git a/library/network/http_client/src/default/libcurl_or_net/default_http_client.e b/library/network/http_client/src/default/libcurl_or_net/default_http_client.e new file mode 100644 index 00000000..9f50c575 --- /dev/null +++ b/library/network/http_client/src/default/libcurl_or_net/default_http_client.e @@ -0,0 +1,43 @@ +note + description: "[ + Default HTTP_CLIENT based on LIBCURL_HTTP_CLIENT. + ]" + author: "$Author$" + date: "$Date$" + revision: "$Revision$" + +class + DEFAULT_HTTP_CLIENT + +inherit + HTTP_CLIENT + +feature -- Access + + new_session (a_base_url: READABLE_STRING_8): HTTP_CLIENT_SESSION + -- Create a new session using `a_base_url'. + local + libcurl: LIBCURL_HTTP_CLIENT + net: NET_HTTP_CLIENT + do + --| For now, try libcurl first, and then net + --| the reason is the net implementation is still in progress. + create libcurl + Result := libcurl.new_session (a_base_url) + if not Result.is_available then + create net + Result := net.new_session (a_base_url) + end + end + +note + copyright: "2011-2015, 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 diff --git a/library/network/http_client/src/default/net/default_http_client.e b/library/network/http_client/src/default/net/default_http_client.e new file mode 100644 index 00000000..1a9b936d --- /dev/null +++ b/library/network/http_client/src/default/net/default_http_client.e @@ -0,0 +1,15 @@ +note + description: "[ + Default HTTP_CLIENT based on NET_HTTP_CLIENT. + ]" + author: "$Author$" + date: "$Date$" + revision: "$Revision$" + +class + DEFAULT_HTTP_CLIENT + +inherit + NET_HTTP_CLIENT + +end diff --git a/library/network/http_client/src/default/null/default_http_client.e b/library/network/http_client/src/default/null/default_http_client.e new file mode 100644 index 00000000..70b4a992 --- /dev/null +++ b/library/network/http_client/src/default/null/default_http_client.e @@ -0,0 +1,15 @@ +note + description: "[ + Default HTTP_CLIENT based on NULL_HTTP_CLIENT. + ]" + author: "$Author$" + date: "$Date$" + revision: "$Revision$" + +class + DEFAULT_HTTP_CLIENT + +inherit + NULL_HTTP_CLIENT + +end diff --git a/library/network/http_client/src/http_client.e b/library/network/http_client/src/http_client.e index bd6c2d89..2b3ba358 100644 --- a/library/network/http_client/src/http_client.e +++ b/library/network/http_client/src/http_client.e @@ -9,7 +9,7 @@ note deferred class HTTP_CLIENT -feature -- Status +feature -- Access new_session (a_base_url: READABLE_STRING_8): HTTP_CLIENT_SESSION -- Create a new session using `a_base_url'. diff --git a/library/network/http_client/src/http_client_response.e b/library/network/http_client/src/http_client_response.e index 1db2e774..92e64fe0 100644 --- a/library/network/http_client/src/http_client_response.e +++ b/library/network/http_client/src/http_client_response.e @@ -95,6 +95,24 @@ feature -- Access Result := s end + multiple_header (a_name: READABLE_STRING_8): detachable LIST [READABLE_STRING_8] + -- Header multiple entries related to `a_name' + local + k: READABLE_STRING_8 + do + across + headers as hds + loop + k := hds.item.name + if k.same_string (a_name) then + if Result = Void then + create {ARRAYED_LIST [READABLE_STRING_8]} Result.make (1) + end + Result.force (hds.item.value) + end + end + end + headers: LIST [TUPLE [name: READABLE_STRING_8; value: READABLE_STRING_8]] -- Computed table of http headers of the response. --| We use a LIST since one might have multiple message-header fields with the same field-name diff --git a/library/network/http_client/src/spec/socket/net_http_client.e b/library/network/http_client/src/spec/net/net_http_client.e similarity index 85% rename from library/network/http_client/src/spec/socket/net_http_client.e rename to library/network/http_client/src/spec/net/net_http_client.e index 863451f5..753d3b3a 100644 --- a/library/network/http_client/src/spec/socket/net_http_client.e +++ b/library/network/http_client/src/spec/net/net_http_client.e @@ -14,18 +14,6 @@ class inherit HTTP_CLIENT -create - default_create, - make - -feature {NONE} -- Initialization - - make - -- Initialize `Current'. - do - default_create - end - feature -- Status new_session (a_base_url: READABLE_STRING_8): NET_HTTP_CLIENT_SESSION diff --git a/library/network/http_client/src/spec/socket/net_http_client_request.e b/library/network/http_client/src/spec/net/net_http_client_request.e similarity index 58% rename from library/network/http_client/src/spec/socket/net_http_client_request.e rename to library/network/http_client/src/spec/net/net_http_client_request.e index 4b29180e..41c91294 100644 --- a/library/network/http_client/src/spec/socket/net_http_client_request.e +++ b/library/network/http_client/src/spec/net/net_http_client_request.e @@ -28,6 +28,16 @@ feature {NONE} -- Internal session: NET_HTTP_CLIENT_SESSION net_http_client_version: STRING = "0.1" + + new_socket (a_host: READABLE_STRING_8; a_port: INTEGER; a_is_https: BOOLEAN; ctx: detachable HTTP_CLIENT_REQUEST_CONTEXT): NETWORK_STREAM_SOCKET + do + if a_is_https then + create {SSL_NETWORK_STREAM_SOCKET} Result.make_client_by_port (a_port, a_host) + else + create Result.make_client_by_port (a_port, a_host) + end + end + feature -- Access response: HTTP_CLIENT_RESPONSE @@ -40,12 +50,13 @@ feature -- Access l_cookie: detachable READABLE_STRING_8 l_request_uri: STRING l_url: HTTP_URL - socket: NETWORK_STREAM_SOCKET + l_socket: NETWORK_STREAM_SOCKET s: STRING l_message: STRING l_content_length: INTEGER l_location: detachable READABLE_STRING_8 l_port: INTEGER + l_is_https: BOOLEAN l_authorization: HTTP_AUTHORIZATION l_platform: STRING l_upload_data: detachable READABLE_STRING_8 @@ -69,13 +80,13 @@ feature -- Access end create Result.make (url) - create l_form_string.make_empty -- Get URL data + l_is_https := url.starts_with_general ("https://") create l_uri.make_from_string (url) l_port := l_uri.port if l_port = 0 then - if url.starts_with_general ("https://") then + if l_is_https then l_port := 443 else l_port := 80 @@ -88,144 +99,147 @@ feature -- Access l_host := l_url.host end - -- add headers for authorization - if not headers.has ("Authorization") then - if - attached username as u_name and - attached password as u_pass - then - create l_authorization.make_basic_auth (u_name, u_pass) - if attached l_authorization.http_authorization as auth then - headers.extend (auth, "Authorization") - end - check headers.has_key ("Authorization") end - end - end - - create l_request_uri.make_from_string (l_uri.path) - if attached l_uri.query as l_query then - l_request_uri.append_character ('?') - l_request_uri.append (l_query) - end - - -- add computed header User-Agent if not yet set. - if not headers.has ("User-Agent") then - if {PLATFORM}.is_unix then - l_platform := "Unix" - elseif {PLATFORM}.is_windows then - l_platform := "Windows" - elseif {PLATFORM}.is_mac then - l_platform := "Mac" - elseif {PLATFORM}.is_vms then - l_platform := "VMS" - elseif {PLATFORM}.is_vxworks then - l_platform := "VxWorks" - else - l_platform := "Unknown" - end - headers.extend ("eiffelhttpclient/" + net_http_client_version + " (" + l_platform + ")", "User-Agent") - end - - -- handle sending data - if ctx /= Void then - if ctx.has_upload_filename then - l_upload_filename := ctx.upload_filename - end - - if ctx.has_upload_data then - l_upload_data := ctx.upload_data - 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 - headers.extend ("application/x-www-form-urlencoded", "Content-Type") - l_upload_data := ctx.form_parameters_to_url_encoded_string - headers.force (l_upload_data.count.out, "Content-Length") - - else - -- create form - l_boundary := new_mime_boundary - headers.extend ("multipart/form-data; boundary=" + l_boundary, "Content-Type") - if l_form_data /= Void then - headers.extend ("*/*", "Accept") - from - l_form_data.start - until - l_form_data.after - loop - l_form_string.append (l_boundary) - l_form_string.append (http_end_of_header_line) - l_form_string.append ("Content-Disposition: form-data; name=") - l_form_string.append ("%"" + l_form_data.key_for_iteration + "%"") - l_form_string.append (http_end_of_header_line) - l_form_string.append (http_end_of_header_line) - l_form_string.append (l_form_data.item_for_iteration) - l_form_string.append (http_end_of_header_line) - l_form_data.forth - end - - if l_upload_filename /= Void then - -- get file extension, otherwise set default - l_mime_type := "application/octet-stream" - create l_mime_type_mapping.make_default - l_fn_extension := l_upload_filename.tail (l_upload_filename.count - l_upload_filename.last_index_of ('.', l_upload_filename.count)) - if attached l_mime_type_mapping.mime_type (l_fn_extension) as mime then - l_mime_type := mime - end - - l_form_string.append (l_boundary) - l_form_string.append (http_end_of_header_line) - l_form_string.append ("Content-Disposition: form-data; name=%"" + l_upload_filename.as_string_32 + "%"") - l_form_string.append ("; filename=%"" + l_upload_filename + "%"") - l_form_string.append (http_end_of_header_line) - l_form_string.append ("Content-Type: ") - l_form_string.append (l_mime_type) - l_form_string.append (http_end_of_header_line) - l_form_string.append (http_end_of_header_line) - - create l_upload_file.make_with_name (l_upload_filename) - if l_upload_file.exists and then l_upload_file.is_access_readable then - append_file_content_to (l_upload_file, l_upload_file.count, l_form_string) - -- Reset l_upload_file to Void, since the related content is already processed. - l_upload_file := Void - end - l_form_string.append (http_end_of_header_line) - end - l_form_string.append (l_boundary + "--") --| end - - l_upload_data := l_form_string - headers.extend (l_upload_data.count.out, "Content-Length") - end - end - elseif - request_method.is_case_insensitive_equal ("POST") - or else request_method.is_case_insensitive_equal ("PUT") - then - if l_upload_data /= Void then - check ctx.has_upload_data end - headers.extend ("application/x-www-form-urlencoded", "Content-Type") - headers.extend (l_upload_data.count.out, "Content-Length") - elseif l_upload_filename /= Void then - check ctx.has_upload_filename end - create l_upload_file.make_with_name (l_upload_filename) - if l_upload_file.exists and then l_upload_file.readable then - headers.extend (l_upload_file.count.out, "Content-Length") - end - check l_upload_file /= Void end - end - - end - end - -- Connect - create socket.make_client_by_port (l_port, l_host) - socket.set_connect_timeout (connect_timeout) - socket.set_timeout (timeout) - socket.connect - if socket.is_connected then + l_socket := new_socket (l_host, l_port, l_is_https, ctx) + l_socket.set_connect_timeout (connect_timeout) + l_socket.set_timeout (timeout) + l_socket.connect + if l_socket.is_connected then + + create l_form_string.make_empty + + -- add headers for authorization + if not headers.has ("Authorization") then + if + attached username as u_name and + attached password as u_pass + then + create l_authorization.make_basic_auth (u_name, u_pass) + if attached l_authorization.http_authorization as auth then + headers.extend (auth, "Authorization") + end + check headers.has_key ("Authorization") end + end + end + + create l_request_uri.make_from_string (l_uri.path) + if attached l_uri.query as l_query then + l_request_uri.append_character ('?') + l_request_uri.append (l_query) + end + + -- add computed header User-Agent if not yet set. + if not headers.has ("User-Agent") then + if {PLATFORM}.is_unix then + l_platform := "Unix" + elseif {PLATFORM}.is_windows then + l_platform := "Windows" + elseif {PLATFORM}.is_mac then + l_platform := "Mac" + elseif {PLATFORM}.is_vms then + l_platform := "VMS" + elseif {PLATFORM}.is_vxworks then + l_platform := "VxWorks" + else + l_platform := "Unknown" + end + headers.extend ("eiffelhttpclient/" + net_http_client_version + " (" + l_platform + ")", "User-Agent") + end + + -- handle sending data + if ctx /= Void then + if ctx.has_upload_filename then + l_upload_filename := ctx.upload_filename + end + + if ctx.has_upload_data then + l_upload_data := ctx.upload_data + 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 + headers.extend ("application/x-www-form-urlencoded", "Content-Type") + l_upload_data := ctx.form_parameters_to_url_encoded_string + headers.force (l_upload_data.count.out, "Content-Length") + + else + -- create form + l_boundary := new_mime_boundary + headers.extend ("multipart/form-data; boundary=" + l_boundary, "Content-Type") + if l_form_data /= Void then + headers.extend ("*/*", "Accept") + from + l_form_data.start + until + l_form_data.after + loop + l_form_string.append (l_boundary) + l_form_string.append (http_end_of_header_line) + l_form_string.append ("Content-Disposition: form-data; name=") + l_form_string.append ("%"" + l_form_data.key_for_iteration + "%"") + l_form_string.append (http_end_of_header_line) + l_form_string.append (http_end_of_header_line) + l_form_string.append (l_form_data.item_for_iteration) + l_form_string.append (http_end_of_header_line) + l_form_data.forth + end + + if l_upload_filename /= Void then + -- get file extension, otherwise set default + l_mime_type := "application/octet-stream" + create l_mime_type_mapping.make_default + l_fn_extension := l_upload_filename.tail (l_upload_filename.count - l_upload_filename.last_index_of ('.', l_upload_filename.count)) + if attached l_mime_type_mapping.mime_type (l_fn_extension) as mime then + l_mime_type := mime + end + + l_form_string.append (l_boundary) + l_form_string.append (http_end_of_header_line) + l_form_string.append ("Content-Disposition: form-data; name=%"" + l_upload_filename.as_string_32 + "%"") + l_form_string.append ("; filename=%"" + l_upload_filename + "%"") + l_form_string.append (http_end_of_header_line) + l_form_string.append ("Content-Type: ") + l_form_string.append (l_mime_type) + l_form_string.append (http_end_of_header_line) + l_form_string.append (http_end_of_header_line) + + create l_upload_file.make_with_name (l_upload_filename) + if l_upload_file.exists and then l_upload_file.is_access_readable then + append_file_content_to (l_upload_file, l_upload_file.count, l_form_string) + -- Reset l_upload_file to Void, since the related content is already processed. + l_upload_file := Void + end + l_form_string.append (http_end_of_header_line) + end + l_form_string.append (l_boundary + "--") --| end + + l_upload_data := l_form_string + headers.extend (l_upload_data.count.out, "Content-Length") + end + end + elseif + request_method.is_case_insensitive_equal ("POST") + or else request_method.is_case_insensitive_equal ("PUT") + then + if l_upload_data /= Void then + check ctx.has_upload_data end + headers.extend ("application/x-www-form-urlencoded", "Content-Type") + headers.extend (l_upload_data.count.out, "Content-Length") + elseif l_upload_filename /= Void then + check ctx.has_upload_filename end + create l_upload_file.make_with_name (l_upload_filename) + if l_upload_file.exists and then l_upload_file.readable then + headers.extend (l_upload_file.count.out, "Content-Length") + end + check l_upload_file /= Void end + end + + end + end + -- FIXME: check usage of headers and specific header variable. --| only one Cookie: is allowed, so merge multiple into one; --| if Host is in header, use that one. @@ -295,20 +309,32 @@ feature -- Access --| Note that any remaining file to upload will be done directly via the socket --| to optimize memory usage - --| Send request - if socket.ready_for_writing then - socket.put_string (s) + + --|-----------------------------|-- + --| Request preparation is done |-- + --|-----------------------------|-- + + if l_socket.ready_for_writing then + --| Socket is ready for writing, so let's send the request. + + --|-------------------------|-- + --| Send request |-- + --|-------------------------|-- + + l_socket.put_string (s) --| Send remaining payload data, if needed. if l_upload_file /= Void then -- i.e: not yet processed - append_file_content_to_socket (l_upload_file, l_upload_file.count, socket) + append_file_content_to_socket (l_upload_file, l_upload_file.count, l_socket) end - --| Get response. - --| Get header message - if socket.ready_for_reading then + --|-------------------------|-- + --| Get response. |-- + --| Get header message |-- + --|-------------------------|-- + if l_socket.ready_for_reading then create l_message.make_empty - append_socket_header_content_to (socket, l_message) + append_socket_header_content_to (l_socket, l_message) l_prev_header := Result.raw_header Result.set_raw_header (l_message.string) l_content_length := -1 @@ -316,13 +342,13 @@ feature -- Access l_content_length := s_len.to_integer end l_location := Result.header ("Location") - if attached Result.header ("Set-Cookies") as s_cookies then + if attached Result.header ("Set-Cookie") as s_cookies then session.set_cookie (s_cookies) end l_message.append (http_end_of_header_line) -- Get content if any. - append_socket_content_to (Result, socket, l_content_length, l_message) + append_socket_content_to (Result, l_socket, l_content_length, l_message) -- Restore previous header Result.set_raw_header (l_prev_header) -- Set message @@ -455,33 +481,47 @@ feature {NONE} -- Helpers end end - append_socket_content_to (a_response: HTTP_CLIENT_RESPONSE; a_socket: NETWORK_STREAM_SOCKET; a_len: INTEGER; a_buffer: STRING) - -- Get content from `a_socket' and append it to `a_buffer'. + append_socket_content_to (a_response: HTTP_CLIENT_RESPONSE; a_socket: NETWORK_STREAM_SOCKET; a_len: INTEGER; a_output: STRING) + -- Get content from `a_socket' and append it to `a_output'. -- If `a_len' is negative, try to get as much as possible, -- this is probably HTTP/1.0 without any Content-Length. local s: STRING_8 + r: INTEGER -- remaining count n,pos, l_chunk_size, l_count: INTEGER hexa2int: HEXADECIMAL_STRING_TO_INTEGER_CONVERTER do if a_socket.readable then if a_len >= 0 then + debug ("socket_content") + io.error.put_string ("Content-Length="+ a_len.out +"%N") + end from - n := a_len + r := a_len until - n = 0 or else not a_socket.readable or else a_response.error_occurred + r = 0 or else not a_socket.readable or else a_response.error_occurred loop if a_socket.ready_for_reading then - a_socket.read_stream_thread_aware (n) + a_socket.read_stream_thread_aware (r) l_count := l_count + a_socket.bytes_read - n := n - a_socket.bytes_read - a_buffer.append (a_socket.last_string) + debug ("socket_content") + io.error.put_string (" - byte read=" + a_socket.bytes_read.out + "%N") + io.error.put_string (" - current count=" + l_count.out + "%N") + end + r := r - a_socket.bytes_read + a_output.append (a_socket.last_string) else + debug ("socket_content") + io.error.put_string (" -! TIMEOUT%N") + end a_response.set_error_message ("Could not read chunked data, timeout") end end - check full_content_read: l_count = a_len end + check full_content_read: not a_response.error_occurred implies l_count = a_len end elseif attached a_response.header ("Transfer-Encoding") as l_enc and then l_enc.is_case_insensitive_equal ("chunked") then + debug ("socket_content") + io.error.put_string ("Chunked encoding%N") + end from create hexa2int.make n := 1 @@ -491,6 +531,9 @@ feature {NONE} -- Helpers a_socket.read_line_thread_aware -- Read chunk info s := a_socket.last_string s.right_adjust + debug ("socket_content") + io.error.put_string (" - chunk info='" + s + "'%N") + end pos := s.index_of (';', 1) if pos > 0 then s.keep_head (pos - 1) @@ -505,17 +548,28 @@ feature {NONE} -- Helpers n := 0 end end + debug ("socket_content") + io.error.put_string (" - chunk size=" + n.out + "%N") + end if n > 0 then from + r := n until - n = 0 or else not a_socket.readable or else a_response.error_occurred + r = 0 or else not a_socket.readable or else a_response.error_occurred loop if a_socket.ready_for_reading then - a_socket.read_stream_thread_aware (n) + a_socket.read_stream_thread_aware (r) l_count := l_count + a_socket.bytes_read - n := n - a_socket.bytes_read - a_buffer.append (a_socket.last_string) + debug ("socket_content") + io.error.put_string (" - byte read=" + a_socket.bytes_read.out + "%N") + io.error.put_string (" - current count=" + l_count.out + "%N") + end + r := r - a_socket.bytes_read + a_output.append (a_socket.last_string) else + debug ("socket_content") + io.error.put_string (" -! TIMEOUT%N") + end a_response.set_error_message ("Could not read chunked data, timeout") end end @@ -525,7 +579,13 @@ feature {NONE} -- Helpers check a_socket.last_character = '%R' end a_socket.read_character check a_socket.last_character = '%N' end + debug ("socket_content") + io.error.put_string (" - Found CRNL %N") + end else + debug ("socket_content") + io.error.put_string (" -! TIMEOUT%N") + end a_response.set_error_message ("Could not read chunked data, timeout") end end @@ -546,7 +606,7 @@ feature {NONE} -- Helpers s := a_socket.last_string n := a_socket.bytes_read l_count := l_count + n - a_buffer.append (s) + a_output.append (s) else a_response.set_error_message ("Could not read data, timeout") end diff --git a/library/network/http_client/src/spec/socket/net_http_client_session.e b/library/network/http_client/src/spec/net/net_http_client_session.e similarity index 96% rename from library/network/http_client/src/spec/socket/net_http_client_session.e rename to library/network/http_client/src/spec/net/net_http_client_session.e index 2bec47b1..2c375574 100644 --- a/library/network/http_client/src/spec/socket/net_http_client_session.e +++ b/library/network/http_client/src/spec/net/net_http_client_session.e @@ -11,6 +11,8 @@ class inherit HTTP_CLIENT_SESSION + NET_HTTP_CLIENT_INFO + create make @@ -22,8 +24,14 @@ feature {NONE} -- Initialization feature -- Status report - is_available: BOOLEAN = True + is_available: BOOLEAN -- Is interface usable? + do + Result := True + if base_url.starts_with_general ("https://") then + Result := has_https_support + end + end feature -- Custom diff --git a/library/network/http_client/src/spec/net/no_ssl/net_http_client_info.e b/library/network/http_client/src/spec/net/no_ssl/net_http_client_info.e new file mode 100644 index 00000000..47517b32 --- /dev/null +++ b/library/network/http_client/src/spec/net/no_ssl/net_http_client_info.e @@ -0,0 +1,24 @@ +note + description: "Additional information related to NET HTTP Client.." + date: "$Date$" + revision: "$Revision$" + +class + NET_HTTP_CLIENT_INFO + +feature -- Access + + has_https_support: BOOLEAN = False + -- Is HTTPS supported? + +note + copyright: "2011-2015, 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 diff --git a/library/network/http_client/src/spec/net/no_ssl/ssl_network_stream_socket.e b/library/network/http_client/src/spec/net/no_ssl/ssl_network_stream_socket.e new file mode 100644 index 00000000..5a3264c7 --- /dev/null +++ b/library/network/http_client/src/spec/net/no_ssl/ssl_network_stream_socket.e @@ -0,0 +1,22 @@ +note + description: "[ + A fake SSL network stream socket... when SSL is disabled at compilation time. + Its behavior is similar to NETWORK_STREAM_SOCKET. + ]" + date: "$Date$" + revision: "$Revision$" + +class + SSL_NETWORK_STREAM_SOCKET + +inherit + NETWORK_STREAM_SOCKET + +create + make, make_empty, make_client_by_port, make_client_by_address_and_port, make_server_by_port, make_loopback_server_by_port + +create {SSL_NETWORK_STREAM_SOCKET} + make_from_descriptor_and_address, create_from_descriptor + + +end diff --git a/library/network/http_client/src/spec/net/ssl/net_http_client_info.e b/library/network/http_client/src/spec/net/ssl/net_http_client_info.e new file mode 100644 index 00000000..0648f04d --- /dev/null +++ b/library/network/http_client/src/spec/net/ssl/net_http_client_info.e @@ -0,0 +1,24 @@ +note + description: "Additional information related to NET HTTP Client.." + date: "$Date$" + revision: "$Revision$" + +class + NET_HTTP_CLIENT_INFO + +feature -- Access + + has_https_support: BOOLEAN = True + -- Is HTTPS supported? + +note + copyright: "2011-2015, 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 diff --git a/library/network/http_client/tests/test-safe.ecf b/library/network/http_client/tests/test-safe.ecf index 8d9005e2..75c48706 100644 --- a/library/network/http_client/tests/test-safe.ecf +++ b/library/network/http_client/tests/test-safe.ecf @@ -10,6 +10,8 @@ + + - + + + .*libcurl_.*.e$ + + + + + + .*net_.*.e$ + + + + + diff --git a/library/network/http_client/tests/test.e b/library/network/http_client/tests/test.e index 6829c245..f5020cb7 100644 --- a/library/network/http_client/tests/test.e +++ b/library/network/http_client/tests/test.e @@ -13,16 +13,40 @@ feature -- Init if attached null.new_session ("http://example.com/") as l_sess then check not l_sess.is_available end end + test_get_with_authentication test_http_client end + test_get_with_authentication + local + cl: DEFAULT_HTTP_CLIENT + sess: HTTP_CLIENT_SESSION + ctx: HTTP_CLIENT_REQUEST_CONTEXT + do + -- GET REQUEST WITH AUTHENTICATION, see http://browserspy.dk/password.php + -- check header WWW-Authenticate is received (authentication successful) + create cl + sess := cl.new_session ("http://browserspy.dk") + sess.set_credentials ("test", "test") + create ctx.make_with_credentials_required + if attached sess.get ("/password-ok.php", ctx) as res then + if attached {READABLE_STRING_8} res.body as l_body then + assert ("Fetch all body, including closing html tag", l_body.has_substring ("")) + else + assert ("has body", False) + end + end + end + test_http_client -- New test routine local - sess: LIBCURL_HTTP_CLIENT_SESSION + cl: DEFAULT_HTTP_CLIENT + sess: HTTP_CLIENT_SESSION h: STRING_8 do - create sess.make ("http://www.google.com") + create cl + sess := cl.new_session ("http://www.google.com") if attached sess.get ("/search?q=eiffel", Void) as res then assert ("Get returned without error", not res.error_occurred) create h.make_empty diff --git a/library/network/http_client/tests/test_http_client_i.e b/library/network/http_client/tests/test_http_client_i.e index e49f6823..66f42c39 100644 --- a/library/network/http_client/tests/test_http_client_i.e +++ b/library/network/http_client/tests/test_http_client_i.e @@ -47,21 +47,30 @@ feature -- Test routines end end - test_http_client_requestbin + test_http_client_ssl + -- New test routine local sess: like new_session h: STRING_8 do - sess := new_session ("http://requestb.in") - create h.make_empty - if attached sess.get ("/1a0q2h61", Void).headers as hds then - across - hds as c - loop - h.append (c.item.name + ": " + c.item.value + "%R%N") + sess := new_session ("https://www.eiffel.org") + if attached sess.get ("/welcome", Void) as res then + assert ("Get returned without error", not res.error_occurred) + create h.make_empty + if attached res.headers as hds then + across + hds as c + loop + h.append (c.item.name + ": " + c.item.value + "%R%N") + end end + if attached res.body as l_body then + assert ("body not empty", not l_body.is_empty) + else + assert ("missing body", False) + end + assert ("same headers", h.same_string (res.raw_header)) end - print (h) end test_headers diff --git a/library/network/http_client/tests/test_libcurl_http_client.e b/library/network/http_client/tests/test_libcurl_http_client.e index fe83bed6..2cb0db17 100644 --- a/library/network/http_client/tests/test_libcurl_http_client.e +++ b/library/network/http_client/tests/test_libcurl_http_client.e @@ -27,9 +27,9 @@ feature -- Tests test_http_client end - test_libcurl_http_client_requestbin + test_libcurl_http_client_ssl do - test_http_client_requestbin + test_http_client_ssl end test_libcurl_headers diff --git a/library/network/http_client/tests/test_net_http_client.e b/library/network/http_client/tests/test_net_http_client.e index 9727b505..d906d334 100644 --- a/library/network/http_client/tests/test_net_http_client.e +++ b/library/network/http_client/tests/test_net_http_client.e @@ -27,9 +27,9 @@ feature -- Tests test_http_client end - test_net_http_client_requestbin + test_net_http_client_ssl do - test_http_client_requestbin + test_http_client_ssl end test_net_headers