Added https support with Net implementation.

Added notion of default HTTP_CLIENT, to be able to build portable code among http client implementation.
This commit is contained in:
2015-09-15 16:57:01 +02:00
parent eec3cbdba1
commit ff9a238f5c
18 changed files with 514 additions and 194 deletions

View File

@@ -24,20 +24,60 @@
<custom name="net_http_client_disabled" excluded_value="true"/>
</condition>
</library>
<library name="net_ssl" location="$ISE_LIBRARY\unstable\library\network\socket\netssl\net_ssl-safe.ecf">
<condition>
<custom name="net_http_client_disabled" excluded_value="true"/>
<custom name="netssl_http_client_enabled" value="true"/>
</condition>
</library>
<library name="uri" location="$ISE_LIBRARY\library\text\uri\uri-safe.ecf"/>
<cluster name="src" location=".\src\">
<cluster name="spec_null" location="$|spec/null" recursive="true"/>
<cluster name="spec_net" location="$|spec/socket" recursive="true">
<cluster name="spec_net" location="$|spec/net">
<condition>
<custom name="net_http_client_disabled" excluded_value="true"/>
</condition>
<cluster name="net_ssl_disabled" location="$|no_ssl">
<condition>
<custom name="netssl_http_client_enabled" excluded_value="true"/>
</condition>
</cluster>
<cluster name="net_ssl_enabled" location="$|ssl">
<condition>
<custom name="netssl_http_client_enabled" value="true"/>
</condition>
</cluster>
</cluster>
<cluster name="spec_libcurl" location="$|spec/libcurl" recursive="true">
<condition>
<custom name="libcurl_http_client_disabled" excluded_value="true"/>
</condition>
</cluster>
<cluster name="default_null" location="$|default/null">
<condition>
<custom name="net_http_client_disabled" value="true"/>
<custom name="libcurl_http_client_disabled" value="true"/>
</condition>
</cluster>
<cluster name="default_net" location="$|default/net">
<condition>
<custom name="net_http_client_disabled" excluded_value="true"/>
<custom name="libcurl_http_client_disabled" value="true"/>
</condition>
</cluster>
<cluster name="default_libcurl" location="$|default/libcurl">
<condition>
<custom name="net_http_client_disabled" value="true"/>
<custom name="libcurl_http_client_disabled" excluded_value="true"/>
</condition>
</cluster>
<cluster name="default_libcurl_or_net" location="$|default/libcurl_or_net">
<condition>
<custom name="net_http_client_disabled" excluded_value="true"/>
<custom name="libcurl_http_client_disabled" excluded_value="true"/>
</condition>
</cluster>
</cluster>
</target>
</system>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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'.

View File

@@ -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

View File

@@ -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

View File

@@ -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,6 +99,15 @@ feature -- Access
l_host := l_url.host
end
-- Connect
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
@@ -220,12 +240,6 @@ feature -- Access
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
-- 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -10,6 +10,8 @@
<option warning="true" full_class_checking="true" is_attached_by_default="true" void_safety="all">
<assertions precondition="true" postcondition="true" check="true" invariant="true" loop="true" supplier_precondition="true"/>
</option>
<variable name="netssl_http_client_enabled" value="false"/>
<variable name="libcurl_http_client_disabled" value="false"/>
<library name="base" location="$ISE_LIBRARY\library\base\base-safe.ecf"/>
<library name="http_client" location="..\http_client-safe.ecf" readonly="false" use_application_options="true">
<option>
@@ -17,6 +19,19 @@
</option>
</library>
<library name="testing" location="$ISE_LIBRARY\library\testing\testing-safe.ecf"/>
<tests name="tests" location=".\"/>
<tests name="tests" location=".\">
<file_rule>
<exclude>.*libcurl_.*.e$</exclude>
<condition>
<custom name="libcurl_http_client_disabled" value="true"/>
</condition>
</file_rule>
<file_rule>
<exclude>.*net_.*.e$</exclude>
<condition>
<custom name="net_http_client_disabled" value="true"/>
</condition>
</file_rule>
</tests>
</target>
</system>

View File

@@ -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 ("</html>"))
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

View File

@@ -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")
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 sess.get ("/1a0q2h61", Void).headers as hds then
if attached res.headers as hds then
across
hds as c
loop
h.append (c.item.name + ": " + c.item.value + "%R%N")
end
end
print (h)
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
end
test_headers

View File

@@ -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

View File

@@ -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