http 0.7.4 → 0.8.0.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/.rubocop.yml +5 -2
- data/CHANGES.md +24 -7
- data/CONTRIBUTING.md +25 -0
- data/Gemfile +24 -22
- data/Guardfile +2 -2
- data/README.md +34 -4
- data/Rakefile +7 -7
- data/examples/parallel_requests_with_celluloid.rb +2 -2
- data/http.gemspec +12 -12
- data/lib/http.rb +11 -10
- data/lib/http/cache.rb +146 -0
- data/lib/http/cache/headers.rb +100 -0
- data/lib/http/cache/null_cache.rb +13 -0
- data/lib/http/chainable.rb +14 -3
- data/lib/http/client.rb +64 -80
- data/lib/http/connection.rb +139 -0
- data/lib/http/content_type.rb +2 -2
- data/lib/http/errors.rb +7 -1
- data/lib/http/headers.rb +21 -8
- data/lib/http/headers/mixin.rb +1 -1
- data/lib/http/mime_type.rb +2 -2
- data/lib/http/mime_type/adapter.rb +2 -2
- data/lib/http/mime_type/json.rb +4 -4
- data/lib/http/options.rb +65 -74
- data/lib/http/redirector.rb +3 -3
- data/lib/http/request.rb +20 -13
- data/lib/http/request/caching.rb +95 -0
- data/lib/http/request/writer.rb +5 -5
- data/lib/http/response.rb +15 -9
- data/lib/http/response/body.rb +21 -8
- data/lib/http/response/caching.rb +142 -0
- data/lib/http/response/io_body.rb +63 -0
- data/lib/http/response/parser.rb +1 -1
- data/lib/http/response/status.rb +4 -12
- data/lib/http/response/status/reasons.rb +53 -53
- data/lib/http/response/string_body.rb +53 -0
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/cache/headers_spec.rb +77 -0
- data/spec/lib/http/cache_spec.rb +182 -0
- data/spec/lib/http/client_spec.rb +123 -95
- data/spec/lib/http/content_type_spec.rb +25 -25
- data/spec/lib/http/headers/mixin_spec.rb +8 -8
- data/spec/lib/http/headers_spec.rb +213 -173
- data/spec/lib/http/options/body_spec.rb +5 -5
- data/spec/lib/http/options/form_spec.rb +3 -3
- data/spec/lib/http/options/headers_spec.rb +7 -7
- data/spec/lib/http/options/json_spec.rb +3 -3
- data/spec/lib/http/options/merge_spec.rb +26 -22
- data/spec/lib/http/options/new_spec.rb +10 -10
- data/spec/lib/http/options/proxy_spec.rb +8 -8
- data/spec/lib/http/options_spec.rb +2 -2
- data/spec/lib/http/redirector_spec.rb +32 -32
- data/spec/lib/http/request/caching_spec.rb +133 -0
- data/spec/lib/http/request/writer_spec.rb +26 -26
- data/spec/lib/http/request_spec.rb +63 -58
- data/spec/lib/http/response/body_spec.rb +13 -13
- data/spec/lib/http/response/caching_spec.rb +201 -0
- data/spec/lib/http/response/io_body_spec.rb +35 -0
- data/spec/lib/http/response/status_spec.rb +25 -25
- data/spec/lib/http/response/string_body_spec.rb +35 -0
- data/spec/lib/http/response_spec.rb +64 -45
- data/spec/lib/http_spec.rb +103 -76
- data/spec/spec_helper.rb +10 -12
- data/spec/support/connection_reuse_shared.rb +100 -0
- data/spec/support/create_certs.rb +12 -12
- data/spec/support/dummy_server.rb +11 -11
- data/spec/support/dummy_server/servlet.rb +43 -31
- data/spec/support/proxy_server.rb +31 -25
- metadata +57 -8
- data/spec/support/example_server.rb +0 -30
- data/spec/support/example_server/servlet.rb +0 -102
@@ -0,0 +1,100 @@
|
|
1
|
+
require "delegate"
|
2
|
+
|
3
|
+
require "http/errors"
|
4
|
+
require "http/headers"
|
5
|
+
|
6
|
+
module HTTP
|
7
|
+
class Cache
|
8
|
+
# Convenience methods around cache control headers.
|
9
|
+
class Headers < ::SimpleDelegator
|
10
|
+
def initialize(headers)
|
11
|
+
if headers.is_a? HTTP::Headers
|
12
|
+
super headers
|
13
|
+
else
|
14
|
+
super HTTP::Headers.coerce headers
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Boolean] does this message force revalidation
|
19
|
+
def forces_revalidation?
|
20
|
+
must_revalidate? || max_age == 0
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [Boolean] does the cache control include 'must-revalidate'
|
24
|
+
def must_revalidate?
|
25
|
+
matches?(/\bmust-revalidate\b/i)
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Boolean] does the cache control include 'no-cache'
|
29
|
+
def no_cache?
|
30
|
+
matches?(/\bno-cache\b/i)
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [Boolean] does the cache control include 'no-stor'
|
34
|
+
def no_store?
|
35
|
+
matches?(/\bno-store\b/i)
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Boolean] does the cache control include 'public'
|
39
|
+
def public?
|
40
|
+
matches?(/\bpublic\b/i)
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Boolean] does the cache control include 'private'
|
44
|
+
def private?
|
45
|
+
matches?(/\bprivate\b/i)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Numeric] the max number of seconds this message is
|
49
|
+
# considered fresh.
|
50
|
+
def max_age
|
51
|
+
explicit_max_age || seconds_til_expires || Float::INFINITY
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [Boolean] is the vary header set to '*'
|
55
|
+
def vary_star?
|
56
|
+
get("Vary").any? { |v| "*" == v.strip }
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# @return [Boolean] true when cache-control header matches the pattern
|
62
|
+
def matches?(pattern)
|
63
|
+
get("Cache-Control").any? { |v| v =~ pattern }
|
64
|
+
end
|
65
|
+
|
66
|
+
# @return [Numeric] number of seconds until the time in the
|
67
|
+
# expires header is reached.
|
68
|
+
#
|
69
|
+
# ---
|
70
|
+
# Some servers send a "Expire: -1" header which must be treated as expired
|
71
|
+
def seconds_til_expires
|
72
|
+
get("Expires")
|
73
|
+
.map { |e| http_date_to_ttl(e) }
|
74
|
+
.max
|
75
|
+
end
|
76
|
+
|
77
|
+
def http_date_to_ttl(t_str)
|
78
|
+
ttl = to_time_or_epoch(t_str) - Time.now
|
79
|
+
|
80
|
+
ttl < 0 ? 0 : ttl
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [Time] parses t_str at a time; if that fails returns epoch time
|
84
|
+
def to_time_or_epoch(t_str)
|
85
|
+
Time.httpdate(t_str)
|
86
|
+
rescue ArgumentError
|
87
|
+
Time.at(0)
|
88
|
+
end
|
89
|
+
|
90
|
+
# @return [Numeric] the value of the max-age component of cache control
|
91
|
+
def explicit_max_age
|
92
|
+
get("Cache-Control")
|
93
|
+
.map { |v| (/max-age=(\d+)/i).match(v) }
|
94
|
+
.compact
|
95
|
+
.map { |m| m[1].to_i }
|
96
|
+
.max
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module HTTP
|
2
|
+
class Cache
|
3
|
+
# NoOp cache. Always makes the request. Allows avoiding
|
4
|
+
# conditionals in the request flow.
|
5
|
+
class NullCache
|
6
|
+
# @return [Response] the result of the provided block
|
7
|
+
# @yield [request, options] so that the request can actually be made
|
8
|
+
def perform(request, options)
|
9
|
+
yield(request, options)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/lib/http/chainable.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require
|
1
|
+
require "base64"
|
2
2
|
|
3
3
|
module HTTP
|
4
4
|
module Chainable
|
@@ -72,6 +72,13 @@ module HTTP
|
|
72
72
|
branch(options).request verb, uri
|
73
73
|
end
|
74
74
|
|
75
|
+
# Flag as persistent
|
76
|
+
# @param [String] host
|
77
|
+
# @raise [Request::Error] if Host is invalid
|
78
|
+
def persistent(host)
|
79
|
+
branch default_options.with_persistent host
|
80
|
+
end
|
81
|
+
|
75
82
|
# Make a request through an HTTP proxy
|
76
83
|
# @param [Array] proxy
|
77
84
|
# @raise [Request::Error] if HTTP proxy is invalid
|
@@ -107,6 +114,10 @@ module HTTP
|
|
107
114
|
# @see #follow
|
108
115
|
alias_method :with_follow, :follow
|
109
116
|
|
117
|
+
def with_cache(cache)
|
118
|
+
branch default_options.with_cache(cache)
|
119
|
+
end
|
120
|
+
|
110
121
|
# Make a request with the given headers
|
111
122
|
# @param headers
|
112
123
|
def with_headers(headers)
|
@@ -138,7 +149,7 @@ module HTTP
|
|
138
149
|
user = opts.fetch :user
|
139
150
|
pass = opts.fetch :pass
|
140
151
|
|
141
|
-
auth(
|
152
|
+
auth("Basic " << Base64.strict_encode64("#{user}:#{pass}"))
|
142
153
|
end
|
143
154
|
|
144
155
|
# Get options for HTTP
|
@@ -167,7 +178,7 @@ module HTTP
|
|
167
178
|
end
|
168
179
|
end
|
169
180
|
|
170
|
-
|
181
|
+
private
|
171
182
|
|
172
183
|
# :nodoc:
|
173
184
|
def branch(options)
|
data/lib/http/client.rb
CHANGED
@@ -1,23 +1,22 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
1
|
+
require "cgi"
|
2
|
+
require "uri"
|
3
|
+
require "http/form_data"
|
4
|
+
require "http/options"
|
5
|
+
require "http/redirector"
|
6
6
|
|
7
7
|
module HTTP
|
8
8
|
# Clients make requests and receive responses
|
9
9
|
class Client
|
10
10
|
include Chainable
|
11
11
|
|
12
|
-
|
13
|
-
|
12
|
+
CONNECTION = "Connection".freeze
|
13
|
+
KEEP_ALIVE = "Keep-Alive".freeze
|
14
|
+
CLOSE = "close".freeze
|
14
15
|
|
15
16
|
attr_reader :default_options
|
16
17
|
|
17
18
|
def initialize(default_options = {})
|
18
19
|
@default_options = HTTP::Options.new(default_options)
|
19
|
-
@parser = HTTP::Response::Parser.new
|
20
|
-
@socket = nil
|
21
20
|
end
|
22
21
|
|
23
22
|
# Make an HTTP request
|
@@ -28,6 +27,13 @@ module HTTP
|
|
28
27
|
proxy = opts.proxy
|
29
28
|
body = make_request_body(opts, headers)
|
30
29
|
|
30
|
+
# Tell the server to keep the conn open
|
31
|
+
if default_options.persistent?
|
32
|
+
headers[CONNECTION] = KEEP_ALIVE
|
33
|
+
else
|
34
|
+
headers[CONNECTION] = CLOSE
|
35
|
+
end
|
36
|
+
|
31
37
|
req = HTTP::Request.new(verb, uri, headers, proxy, body)
|
32
38
|
res = perform req, opts
|
33
39
|
|
@@ -42,66 +48,61 @@ module HTTP
|
|
42
48
|
|
43
49
|
# Perform a single (no follow) HTTP request
|
44
50
|
def perform(req, options)
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
finish_response
|
50
|
-
|
51
|
-
uri = req.uri
|
52
|
-
|
53
|
-
# TODO: keep-alive support
|
54
|
-
@socket = options[:socket_class].open(req.socket_host, req.socket_port)
|
55
|
-
@socket = start_tls(@socket, uri.host, options) if uri.is_a?(URI::HTTPS) && !req.using_proxy?
|
51
|
+
options.cache.perform(req, options) do |r, opts|
|
52
|
+
make_request(r, opts)
|
53
|
+
end
|
54
|
+
end
|
56
55
|
|
57
|
-
|
56
|
+
def make_request(req, options)
|
57
|
+
verify_connection!(req.uri)
|
58
58
|
|
59
|
-
|
59
|
+
@connection ||= HTTP::Connection.new(req, options)
|
60
|
+
@connection.send_request(req)
|
61
|
+
@connection.read_headers!
|
60
62
|
|
61
|
-
|
62
|
-
|
63
|
+
res = Response.new(
|
64
|
+
@connection.parser.status_code,
|
65
|
+
@connection.parser.http_version,
|
66
|
+
@connection.parser.headers,
|
67
|
+
Response::Body.new(@connection),
|
68
|
+
req.uri
|
69
|
+
)
|
63
70
|
|
64
|
-
finish_response if
|
71
|
+
@connection.finish_response if req.verb == :head
|
65
72
|
|
66
73
|
res
|
67
|
-
end
|
68
74
|
|
69
|
-
#
|
70
|
-
#
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
begin
|
77
|
-
read_more size
|
78
|
-
finished = @parser.finished?
|
79
|
-
rescue EOFError
|
80
|
-
finished = true
|
75
|
+
# On any exception we reset the conn. This is a safety measure, to ensure
|
76
|
+
# we don't have conns in a bad state resulting in mixed requests/responses
|
77
|
+
rescue
|
78
|
+
if default_options.persistent? && @connection
|
79
|
+
@connection.close
|
80
|
+
@connection = nil
|
81
81
|
end
|
82
82
|
|
83
|
-
|
84
|
-
|
85
|
-
finish_response if finished
|
86
|
-
|
87
|
-
chunk.to_s
|
83
|
+
raise
|
88
84
|
end
|
89
85
|
|
90
|
-
|
86
|
+
private
|
91
87
|
|
92
|
-
#
|
93
|
-
def
|
94
|
-
|
95
|
-
|
96
|
-
socket = options[:ssl_socket_class].new(socket, context)
|
88
|
+
# Verify our request isn't going to be made against another URI
|
89
|
+
def verify_connection!(uri)
|
90
|
+
if default_options.persistent? && base_host(uri) != default_options.persistent
|
91
|
+
fail StateError, "Persistence is enabled for #{default_options.persistent}, but we got #{base_host(uri)}"
|
97
92
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
93
|
+
# We re-create the connection object because we want to let prior requests
|
94
|
+
# lazily load the body as long as possible, and this mimics prior functionality.
|
95
|
+
elsif !default_options.persistent? || (@connection && !@connection.keep_alive?)
|
96
|
+
@connection = nil
|
102
97
|
end
|
98
|
+
end
|
103
99
|
|
104
|
-
|
100
|
+
# Strips out query/path to give us a consistent way of comparing hosts
|
101
|
+
def base_host(uri)
|
102
|
+
base = uri.dup
|
103
|
+
base.query = nil
|
104
|
+
base.path = ""
|
105
|
+
base.to_s
|
105
106
|
end
|
106
107
|
|
107
108
|
# Merges query params if needed
|
@@ -121,12 +122,16 @@ module HTTP
|
|
121
122
|
# @param [#to_s] uri
|
122
123
|
# @return [URI]
|
123
124
|
def normalize_uri(uri)
|
124
|
-
|
125
|
+
if default_options.persistent? && uri !~ /^http|https/
|
126
|
+
uri = URI("#{default_options.persistent}#{uri}")
|
127
|
+
else
|
128
|
+
uri = URI(uri.to_s)
|
129
|
+
end
|
125
130
|
|
126
131
|
# Some proxies (seen on WEBRick) fail if URL has
|
127
132
|
# empty path (e.g. `http://example.com`) while it's RFC-complaint:
|
128
133
|
# http://tools.ietf.org/html/rfc1738#section-3.1
|
129
|
-
uri.path =
|
134
|
+
uri.path = "/" if uri.path.empty?
|
130
135
|
|
131
136
|
uri
|
132
137
|
end
|
@@ -138,34 +143,13 @@ module HTTP
|
|
138
143
|
opts.body
|
139
144
|
when opts.form
|
140
145
|
form = HTTP::FormData.create opts.form
|
141
|
-
headers[
|
142
|
-
headers[
|
146
|
+
headers["Content-Type"] ||= form.content_type
|
147
|
+
headers["Content-Length"] ||= form.content_length
|
143
148
|
form.to_s
|
144
149
|
when opts.json
|
145
|
-
headers[
|
150
|
+
headers["Content-Type"] ||= "application/json"
|
146
151
|
MimeType[:json].encode opts.json
|
147
152
|
end
|
148
153
|
end
|
149
|
-
|
150
|
-
# Reads data from socket up until headers
|
151
|
-
def read_headers!
|
152
|
-
read_more BUFFER_SIZE until @parser.headers
|
153
|
-
rescue IOError, Errno::ECONNRESET, Errno::EPIPE => ex
|
154
|
-
return if ex.is_a?(EOFError) && @parser.headers
|
155
|
-
raise IOError, "problem making HTTP request: #{ex}"
|
156
|
-
end
|
157
|
-
|
158
|
-
# Callback for when we've reached the end of a response
|
159
|
-
def finish_response
|
160
|
-
@socket.close if @socket && !@socket.closed?
|
161
|
-
@parser.reset
|
162
|
-
|
163
|
-
@socket = nil
|
164
|
-
end
|
165
|
-
|
166
|
-
# Feeds some more data into parser
|
167
|
-
def read_more(size)
|
168
|
-
@parser << @socket.readpartial(size) unless @parser.finished?
|
169
|
-
end
|
170
154
|
end
|
171
155
|
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require "http/response/parser"
|
2
|
+
|
3
|
+
module HTTP
|
4
|
+
# A connection to the HTTP server
|
5
|
+
class Connection
|
6
|
+
attr_reader :socket, :parser, :persistent,
|
7
|
+
:pending_request, :pending_response, :sequence_id
|
8
|
+
|
9
|
+
# Attempt to read this much data
|
10
|
+
BUFFER_SIZE = 16_384
|
11
|
+
|
12
|
+
def initialize(req, options)
|
13
|
+
@persistent = options.persistent?
|
14
|
+
|
15
|
+
@parser = Response::Parser.new
|
16
|
+
@sequence_id = 0
|
17
|
+
|
18
|
+
@socket = options[:socket_class].open(req.socket_host, req.socket_port)
|
19
|
+
|
20
|
+
start_tls(req.uri.host, options[:ssl_socket_class], options[:ssl_context]) if req.uri.is_a?(URI::HTTPS) && !req.using_proxy?
|
21
|
+
end
|
22
|
+
|
23
|
+
# Send a request to the server
|
24
|
+
def send_request(req)
|
25
|
+
if pending_request
|
26
|
+
fail StateError, "Tried to send a request while one is pending already. This cannot be called from multiple threads!"
|
27
|
+
elsif pending_request
|
28
|
+
fail StateError, "Tried to send a request while a response is pending. Make sure you've fully read the body from the request."
|
29
|
+
end
|
30
|
+
|
31
|
+
@pending_request = true
|
32
|
+
@sequence_id += 1
|
33
|
+
|
34
|
+
req.stream socket
|
35
|
+
|
36
|
+
@pending_response = true
|
37
|
+
@pending_request = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
# Read a chunk of the body
|
41
|
+
#
|
42
|
+
# @return [String] data chunk
|
43
|
+
# @return [Nil] when no more data left
|
44
|
+
def readpartial(size = BUFFER_SIZE)
|
45
|
+
return unless pending_response
|
46
|
+
|
47
|
+
begin
|
48
|
+
read_more size
|
49
|
+
finished = parser.finished?
|
50
|
+
rescue EOFError
|
51
|
+
finished = true
|
52
|
+
end
|
53
|
+
|
54
|
+
chunk = parser.chunk
|
55
|
+
|
56
|
+
finish_response if finished
|
57
|
+
|
58
|
+
chunk.to_s
|
59
|
+
end
|
60
|
+
|
61
|
+
# Reads data from socket up until headers
|
62
|
+
def read_headers!
|
63
|
+
read_more BUFFER_SIZE until parser.headers
|
64
|
+
set_keep_alive
|
65
|
+
|
66
|
+
rescue IOError, Errno::ECONNRESET, Errno::EPIPE => ex
|
67
|
+
return if ex.is_a?(EOFError) && parser.headers
|
68
|
+
raise IOError, "problem making HTTP request: #{ex}"
|
69
|
+
end
|
70
|
+
|
71
|
+
# Callback for when we've reached the end of a response
|
72
|
+
def finish_response
|
73
|
+
close unless keep_alive?
|
74
|
+
|
75
|
+
parser.reset
|
76
|
+
|
77
|
+
@pending_response = nil
|
78
|
+
end
|
79
|
+
|
80
|
+
# Close the connection
|
81
|
+
def close
|
82
|
+
socket.close unless socket.closed?
|
83
|
+
|
84
|
+
@pending_response = nil
|
85
|
+
@pending_request = nil
|
86
|
+
end
|
87
|
+
|
88
|
+
# Whether we're keeping the conn alive
|
89
|
+
def keep_alive?
|
90
|
+
!!@keep_alive && !socket.closed?
|
91
|
+
end
|
92
|
+
|
93
|
+
# Store whether the connection should be kept alive.
|
94
|
+
# Once we reset the parser, we lose all of this state.
|
95
|
+
def set_keep_alive
|
96
|
+
return @keep_alive = false unless persistent
|
97
|
+
|
98
|
+
# HTTP/1.0 requires opt in for Keep Alive
|
99
|
+
if parser.http_version == "1.0"
|
100
|
+
@keep_alive = parser.headers["Connection"] == HTTP::Client::KEEP_ALIVE
|
101
|
+
|
102
|
+
# HTTP/1.1 is opt-out
|
103
|
+
elsif parser.http_version == "1.1"
|
104
|
+
@keep_alive = parser.headers["Connection"] != HTTP::Client::CLOSE
|
105
|
+
|
106
|
+
# Anything else we assume doesn't supportit
|
107
|
+
else
|
108
|
+
@keep_alive = false
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
private :set_keep_alive
|
113
|
+
|
114
|
+
# Feeds some more data into parser
|
115
|
+
def read_more(size)
|
116
|
+
parser << socket.readpartial(size) unless parser.finished?
|
117
|
+
end
|
118
|
+
|
119
|
+
private :read_more
|
120
|
+
|
121
|
+
# Starts the SSL connection
|
122
|
+
def start_tls(host, ssl_socket_class, ssl_context)
|
123
|
+
# TODO: abstract away SSLContexts so we can use other TLS libraries
|
124
|
+
ssl_context ||= OpenSSL::SSL::SSLContext.new
|
125
|
+
@socket = ssl_socket_class.new(socket, ssl_context)
|
126
|
+
socket.sync_close = true
|
127
|
+
|
128
|
+
socket.connect
|
129
|
+
|
130
|
+
if ssl_context.verify_mode == OpenSSL::SSL::VERIFY_PEER
|
131
|
+
socket.post_connection_check(host)
|
132
|
+
end
|
133
|
+
|
134
|
+
socket
|
135
|
+
end
|
136
|
+
|
137
|
+
private :start_tls
|
138
|
+
end
|
139
|
+
end
|