Skip to content

Commit 691b32a

Browse files
committed
Take a OpenSSL::SSL:Context in HTTP::Client's ssl option
1 parent df2aca2 commit 691b32a

File tree

2 files changed

+118
-51
lines changed

2 files changed

+118
-51
lines changed

spec/std/http/client/client_spec.cr

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,26 +48,47 @@ module HTTP
4848

4949
describe "from URI" do
5050
it "has sane defaults" do
51-
cl = Client.new(URI.parse("http://demo.com"))
52-
cl.ssl?.should be_false
51+
cl = Client.new(URI.parse("http://example.com"))
52+
cl.ssl?.should be_nil
5353
cl.port.should eq(80)
5454
end
5555

56-
it "detects ssl" do
57-
cl = Client.new(URI.parse("https://demo.com"))
58-
cl.ssl?.should be_true
59-
cl.port.should eq(443)
60-
end
56+
ifdef !without_openssl
57+
it "detects ssl" do
58+
cl = Client.new(URI.parse("https://example.com"))
59+
cl.ssl?.should be_truthy
60+
cl.port.should eq(443)
61+
end
62+
63+
it "keeps context" do
64+
ctx = OpenSSL::SSL::Context.new
65+
cl = Client.new(URI.parse("https://example.com"), ctx)
66+
cl.ssl.should be(ctx)
67+
end
6168

62-
it "allows for specified ports" do
63-
cl = Client.new(URI.parse("https://demo.com:9999"))
64-
cl.ssl?.should be_true
65-
cl.port.should eq(9999)
69+
it "doesn't take context for HTTP" do
70+
ctx = OpenSSL::SSL::Context.new
71+
expect_raises(ArgumentError, "SSL context given") do
72+
Client.new(URI.parse("http://example.com"), ctx)
73+
end
74+
end
75+
76+
it "allows for specified ports" do
77+
cl = Client.new(URI.parse("https://example.com:9999"))
78+
cl.ssl?.should be_truthy
79+
cl.port.should eq(9999)
80+
end
81+
else
82+
it "raises when trying to activate SSL" do
83+
expect_raises do
84+
Client.new "example.org", 443, ssl: true
85+
end
86+
end
6687
end
6788

6889
it "raises error if not http schema" do
6990
expect_raises(ArgumentError, "Unsupported scheme: ssh") do
70-
Client.new(URI.parse("ssh://demo.com"))
91+
Client.new(URI.parse("ssh://example.com"))
7192
end
7293
end
7394

src/http/client/client.cr

Lines changed: 85 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,19 @@ class HTTP::Client
6969
# ```
7070
getter port : Int32
7171

72-
# Returns whether this client is using SSL.
72+
# If this client uses SSL, returns its `OpenSSL::SSL::Context`, raises otherwise.
73+
#
74+
# Changes made after the initial request will have no effect.
7375
#
7476
# ```
7577
# client = HTTP::Client.new "www.example.com", ssl: true
76-
# client.ssl? # => true
78+
# client.ssl # => #<OpenSSL::SSL::Context ...>
7779
# ```
78-
getter? ssl : Bool
80+
ifdef without_openssl
81+
getter! ssl : Nil
82+
else
83+
getter! ssl : OpenSSL::SSL::Context?
84+
end
7985

8086
# Whether automatic compression/decompression is enabled.
8187
property? compress : Bool
@@ -93,16 +99,32 @@ class HTTP::Client
9399
# Creates a new HTTP client with the given *host*, *port* and *ssl*
94100
# configurations. If no port is given, the default one will
95101
# be used depending on the *ssl* arguments: 80 for if *ssl* is `false`,
96-
# 443 if *ssl* is `true`.
97-
def initialize(@host, port = nil, @ssl = false)
98-
ifdef without_openssl
99-
if @ssl
102+
# 443 if *ssl* is truthy. If *ssl* is `true` a new `OpenSSL::SSL::Context` will
103+
# be used, else the given one. In any case the active context can be accessed through `ssl`.
104+
ifdef without_openssl
105+
def initialize(@host, port = nil, ssl : Bool = false)
106+
@ssl = nil
107+
if ssl
100108
raise "HTTP::Client ssl is disabled because `-D without_openssl` was passed at compile time"
101109
end
102-
end
103110

104-
@port = (port || (ssl ? 443 : 80)).to_i
105-
@compress = true
111+
@port = (port || (@ssl ? 443 : 80)).to_i
112+
@compress = true
113+
end
114+
else
115+
def initialize(@host, port = nil, ssl : Bool | OpenSSL::SSL::Context = false)
116+
@ssl = case ssl
117+
when true
118+
OpenSSL::SSL::Context.new_for_client
119+
when OpenSSL::SSL::Context
120+
ssl
121+
when false
122+
nil
123+
end
124+
125+
@port = (port || (@ssl ? 443 : 80)).to_i
126+
@compress = true
127+
end
106128
end
107129

108130
# Creates a new HTTP client from a URI. Parses the *host*, *port*,
@@ -120,10 +142,14 @@ class HTTP::Client
120142
# This constructor will *ignore* any path or query segments in the URI
121143
# as those will need to be passed to the client when a request is made.
122144
#
145+
# If *ssl* is given it will be used, if not a new SSL context will be created.
146+
# If *ssl* is given and *uri* is a HTTP URI, `ArgumentError` is raised.
147+
# In any case the active context can be accessed through `ssl`.
148+
#
123149
# This constructor will raise an exception if any scheme but HTTP or HTTPS
124150
# is used.
125-
def self.new(uri : URI)
126-
ssl = ssl_flag(uri)
151+
def self.new(uri : URI, ssl = nil)
152+
ssl = ssl_flag(uri, ssl)
127153
host = validate_host(uri)
128154
new(host, uri.port, ssl)
129155
end
@@ -296,8 +322,8 @@ class HTTP::Client
296322
# response = HTTP::Client.{{method.id}}("/", headers: HTTP::Headers{"User-agent": "AwesomeApp"}, body: "Hello!")
297323
# response.body #=> "..."
298324
# ```
299-
def self.{{method.id}}(url : String | URI, headers : HTTP::Headers? = nil, body : String? = nil) : HTTP::Client::Response
300-
exec {{method.upcase}}, url, headers, body
325+
def self.{{method.id}}(url : String | URI, headers : HTTP::Headers? = nil, body : String? = nil, ssl = nil) : HTTP::Client::Response
326+
exec {{method.upcase}}, url, headers, body, ssl
301327
end
302328

303329
# Executes a {{method.id.upcase}} request and yields the response to the block.
@@ -308,8 +334,8 @@ class HTTP::Client
308334
# response.body_io.gets #=> "..."
309335
# end
310336
# ```
311-
def self.{{method.id}}(url : String | URI, headers : HTTP::Headers? = nil, body : String? = nil)
312-
exec {{method.upcase}}, url, headers, body do |response|
337+
def self.{{method.id}}(url : String | URI, headers : HTTP::Headers? = nil, body : String? = nil, ssl = nil)
338+
exec {{method.upcase}}, url, headers, body, ssl do |response|
313339
yield response
314340
end
315341
end
@@ -351,8 +377,8 @@ class HTTP::Client
351377
# ```
352378
# response = HTTP::Client.post_form "http://www.example.com", "foo=bar"
353379
# ```
354-
def self.post_form(url, form : String | Hash, headers : HTTP::Headers? = nil) : HTTP::Client::Response
355-
exec(url) do |client, path|
380+
def self.post_form(url, form : String | Hash, headers : HTTP::Headers? = nil, ssl = nil) : HTTP::Client::Response
381+
exec(url, ssl) do |client, path|
356382
client.post_form(path, form, headers)
357383
end
358384
end
@@ -455,8 +481,8 @@ class HTTP::Client
455481
# response = HTTP::Client.exec "GET", "http://www.example.com"
456482
# response.body # => "..."
457483
# ```
458-
def self.exec(method, url : String | URI, headers : HTTP::Headers? = nil, body : String? = nil) : HTTP::Client::Response
459-
exec(url) do |client, path|
484+
def self.exec(method, url : String | URI, headers : HTTP::Headers? = nil, body : String? = nil, ssl = nil) : HTTP::Client::Response
485+
exec(url, ssl) do |client, path|
460486
client.exec method, path, headers, body
461487
end
462488
end
@@ -469,8 +495,8 @@ class HTTP::Client
469495
# response.body_io.gets # => "..."
470496
# end
471497
# ```
472-
def self.exec(method, url : String | URI, headers : HTTP::Headers? = nil, body : String? = nil)
473-
exec(url) do |client, path|
498+
def self.exec(method, url : String | URI, headers : HTTP::Headers? = nil, body : String? = nil, ssl = nil)
499+
exec(url, ssl) do |client, path|
474500
client.exec(method, path, headers, body) do |response|
475501
yield response
476502
end
@@ -503,8 +529,8 @@ class HTTP::Client
503529
@socket = socket
504530

505531
ifdef !without_openssl
506-
if @ssl
507-
ssl_socket = OpenSSL::SSL::Socket.new(socket, sync_close: true, hostname: @host)
532+
if ssl = @ssl
533+
ssl_socket = OpenSSL::SSL::Socket.new(socket, context: ssl, sync_close: true, hostname: @host)
508534
@socket = socket = ssl_socket
509535
end
510536
end
@@ -520,30 +546,50 @@ class HTTP::Client
520546
end
521547
end
522548

523-
private def self.exec(string : String)
549+
private def self.exec(string : String, ssl = nil)
524550
uri = URI.parse(string)
525551

526552
unless uri.scheme && uri.host
527553
# Assume http if no scheme and host are specified
528554
uri = URI.parse("http://#{string}")
529555
end
530556

531-
exec(uri) do |client, path|
557+
exec(uri, ssl) do |client, path|
532558
yield client, path
533559
end
534560
end
535561

536-
protected def self.ssl_flag(uri)
537-
scheme = uri.scheme
538-
case scheme
539-
when nil
540-
raise ArgumentError.new("missing scheme: #{uri}")
541-
when "http"
542-
false
543-
when "https"
544-
true
545-
else
546-
raise ArgumentError.new "Unsupported scheme: #{scheme}"
562+
ifdef without_openssl
563+
protected def self.ssl_flag(uri, context : Nil)
564+
scheme = uri.scheme
565+
case scheme
566+
when nil
567+
raise ArgumentError.new("missing scheme: #{uri}")
568+
when "http"
569+
false
570+
when "https"
571+
true
572+
else
573+
raise ArgumentError.new "Unsupported scheme: #{scheme}"
574+
end
575+
end
576+
else
577+
protected def self.ssl_flag(uri, context : OpenSSL::SSL::Context?)
578+
scheme = uri.scheme
579+
case {scheme, context}
580+
when {nil, _}
581+
raise ArgumentError.new("missing scheme: #{uri}")
582+
when {"http", nil}
583+
false
584+
when {"http", OpenSSL::SSL::Context}
585+
raise ArgumentError.new("SSL context given for HTTP URI")
586+
when {"https", nil}
587+
true
588+
when {"https", OpenSSL::SSL::Context}
589+
context
590+
else
591+
raise ArgumentError.new "Unsupported scheme: #{scheme}"
592+
end
547593
end
548594
end
549595

@@ -554,8 +600,8 @@ class HTTP::Client
554600
raise ArgumentError.new %(Request URI must have host (URI is: #{uri}))
555601
end
556602

557-
private def self.exec(uri : URI)
558-
ssl = ssl_flag(uri)
603+
private def self.exec(uri : URI, ssl = nil)
604+
ssl = ssl_flag(uri, ssl)
559605
host = validate_host(uri)
560606

561607
port = uri.port

0 commit comments

Comments
 (0)