pwned 2.0.2 → 2.1.0
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/CHANGELOG.md +9 -1
- data/README.md +32 -13
- data/bin/pwned +3 -3
- data/lib/pwned.rb +18 -2
- data/lib/pwned/hashed_password.rb +35 -0
- data/lib/pwned/password.rb +4 -125
- data/lib/pwned/password_base.rb +133 -0
- data/lib/pwned/version.rb +1 -1
- data/pwned.gemspec +1 -1
- metadata +5 -23
- data/docs/NotPwnedValidator.html +0 -494
- data/docs/Pwned.html +0 -521
- data/docs/Pwned/Error.html +0 -149
- data/docs/Pwned/Password.html +0 -936
- data/docs/Pwned/TimeoutError.html +0 -152
- data/docs/PwnedValidator.html +0 -192
- data/docs/_index.html +0 -162
- data/docs/class_list.html +0 -51
- data/docs/css/common.css +0 -1
- data/docs/css/full_list.css +0 -58
- data/docs/css/style.css +0 -496
- data/docs/file.README.html +0 -424
- data/docs/file_list.html +0 -56
- data/docs/frames.html +0 -17
- data/docs/index.html +0 -424
- data/docs/js/app.js +0 -303
- data/docs/js/full_list.js +0 -216
- data/docs/js/jquery.js +0 -4
- data/docs/method_list.html +0 -115
- data/docs/top-level-namespace.html +0 -112
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4661790082f543ba897baf211da660c7a4f654444121f4ff3ba08542c08c412b
|
4
|
+
data.tar.gz: f52a3f3cf36d461e8704a632f97829a5d9f871d9916a55687d4a0b2156b44b75
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c114c3ca6e7667d1760ad2ae5dabcc7bf8d14b91e42788f7e36bba716eecd9bef6e1847e93dd12df4f8afed19460d26a068dc22ffb2270ceef8dc342f81690e0
|
7
|
+
data.tar.gz: c19d20d765cd57e64468c27a3e8f134e53d8f6e9ae22497c2d94a315a584e2e19b1913d47b37e89c9525ce80d85d10d43437a623d211766aa7242dbe1144e906
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,14 @@
|
|
2
2
|
|
3
3
|
## Ongoing [☰](https://github.com/philnash/pwned/compare/v2.0.2...master)
|
4
4
|
|
5
|
+
## 2.1.0 (July 8, 2020) [☰](https://github.com/philnash/pwned/compare/v2.0.2...v2.1.0)
|
6
|
+
|
7
|
+
- Minor updates
|
8
|
+
|
9
|
+
- Adds `Pwned::HashedPassword` class which is initializd with a SHA1 hash to
|
10
|
+
query the API with so that the lookup can be done in the background without
|
11
|
+
storing passwords. Fixes #19, thanks [@paprikati](https://github.com/paprikati).
|
12
|
+
|
5
13
|
## 2.0.2 (May 20, 2020) [☰](https://github.com/philnash/pwned/compare/v2.0.1...v2.0.2)
|
6
14
|
|
7
15
|
- Minor fix
|
@@ -10,7 +18,7 @@
|
|
10
18
|
result in a `nil` which caused trouble with string concatenation. This
|
11
19
|
avoids that scenario. Fixes #18, thanks [@flori](https://github.com/flori).
|
12
20
|
|
13
|
-
## 2.0.1 (January 14,
|
21
|
+
## 2.0.1 (January 14, 2020) [☰](https://github.com/philnash/pwned/compare/v2.0.0...v2.0.1)
|
14
22
|
|
15
23
|
- Minor updates
|
16
24
|
|
data/README.md
CHANGED
@@ -4,22 +4,30 @@ An easy, Ruby way to use the Pwned Passwords API.
|
|
4
4
|
|
5
5
|
[](https://rubygems.org/gems/pwned) [](https://travis-ci.org/philnash/pwned) [](https://codeclimate.com/github/philnash/pwned/maintainability) [](https://inch-ci.org/github/philnash/pwned)
|
6
6
|
|
7
|
-
[API docs](https://
|
7
|
+
[API docs](https://www.rubydoc.info/gems/pwned) | [GitHub repo](https://github.com/philnash/pwned)
|
8
8
|
|
9
9
|
## Table of Contents
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
11
|
+
- [Pwned](#pwned)
|
12
|
+
- [Table of Contents](#table-of-contents)
|
13
|
+
- [About](#about)
|
14
|
+
- [Installation](#installation)
|
15
|
+
- [Usage](#usage)
|
16
|
+
- [Plain Ruby](#plain-ruby)
|
17
|
+
- [Advanced](#advanced)
|
18
|
+
- [ActiveRecord Validator](#activerecord-validator)
|
19
|
+
- [I18n](#i18n)
|
20
|
+
- [Threshold](#threshold)
|
21
|
+
- [Network Error Handling](#network-error-handling)
|
22
|
+
- [Custom Request Options](#custom-request-options)
|
23
|
+
- [Using Asynchronously](#using-asynchronously)
|
24
|
+
- [Devise](#devise)
|
25
|
+
- [Command line](#command-line)
|
26
|
+
- [How Pwned is Pi?](#how-pwned-is-pi)
|
27
|
+
- [Development](#development)
|
28
|
+
- [Contributing](#contributing)
|
29
|
+
- [License](#license)
|
30
|
+
- [Code of Conduct](#code-of-conduct)
|
23
31
|
|
24
32
|
## About
|
25
33
|
|
@@ -180,6 +188,17 @@ In addition to these options, HTTP headers can be specified with the `:headers`
|
|
180
188
|
}
|
181
189
|
```
|
182
190
|
|
191
|
+
### Using Asynchronously
|
192
|
+
|
193
|
+
You may have a use case for hashing the password in advance, and then making the call to the Pwned api later
|
194
|
+
(for example if you want to enqueue a job without storing the plaintext password):
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
hashed_password = Pwned.hash_password(password)
|
198
|
+
# some time later
|
199
|
+
Pwned::HashPassword.new(hashed_password, request_options).pwned?
|
200
|
+
```
|
201
|
+
|
183
202
|
### Devise
|
184
203
|
|
185
204
|
If you are using Devise I recommend you use the [devise-pwned_password extension](https://github.com/michaelbanfield/devise-pwned_password) which is now powered by this gem.
|
data/bin/pwned
CHANGED
data/lib/pwned.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "digest"
|
3
4
|
require "pwned/version"
|
4
5
|
require "pwned/error"
|
5
6
|
require "pwned/password"
|
7
|
+
require "pwned/hashed_password"
|
6
8
|
|
7
9
|
begin
|
8
10
|
# Load Rails and our custom validator
|
@@ -31,7 +33,7 @@ module Pwned
|
|
31
33
|
# @param password [String] The password you want to check against the API.
|
32
34
|
# @param [Hash] request_options Options that can be passed to +Net::HTTP.start+ when
|
33
35
|
# calling the API
|
34
|
-
# @option request_options [Symbol] :headers ({ "User-Agent" =>
|
36
|
+
# @option request_options [Symbol] :headers ({ "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })
|
35
37
|
# HTTP headers to include in the request
|
36
38
|
# @return [Boolean] Whether the password appears in the data breaches or not.
|
37
39
|
# @since 1.1.0
|
@@ -49,7 +51,7 @@ module Pwned
|
|
49
51
|
# @param password [String] The password you want to check against the API.
|
50
52
|
# @param [Hash] request_options Options that can be passed to +Net::HTTP.start+ when
|
51
53
|
# calling the API
|
52
|
-
# @option request_options [Symbol] :headers ({ "User-Agent" =>
|
54
|
+
# @option request_options [Symbol] :headers ({ "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })
|
53
55
|
# HTTP headers to include in the request
|
54
56
|
# @return [Integer] The number of times the password has appeared in the data
|
55
57
|
# breaches.
|
@@ -57,4 +59,18 @@ module Pwned
|
|
57
59
|
def self.pwned_count(password, request_options={})
|
58
60
|
Pwned::Password.new(password, request_options).pwned_count
|
59
61
|
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Returns the full SHA1 hash of the given password in uppercase. This can be safely passed around your code
|
65
|
+
# before making the pwned request (e.g. dropped into a queue table).
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# Pwned.hash_password("password") #=> 5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8
|
69
|
+
#
|
70
|
+
# @param password [String] The password you want to check against the API
|
71
|
+
# @return [String] An uppercase SHA1 hash of the password
|
72
|
+
# @since 2.1.0
|
73
|
+
def self.hash_password(password)
|
74
|
+
Digest::SHA1.hexdigest(password).upcase
|
75
|
+
end
|
60
76
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pwned/password_base"
|
4
|
+
|
5
|
+
module Pwned
|
6
|
+
##
|
7
|
+
# This class represents a hashed password. It does all the work of talking to the
|
8
|
+
# Pwned Passwords API to find out if the password has been pwned.
|
9
|
+
# @see https://haveibeenpwned.com/API/v2#PwnedPasswords
|
10
|
+
class HashedPassword
|
11
|
+
include PasswordBase
|
12
|
+
##
|
13
|
+
# Creates a new hashed password object.
|
14
|
+
#
|
15
|
+
# @example A simple password with the default request options
|
16
|
+
# password = Pwned::HashedPassword.new("ABC123")
|
17
|
+
# @example Setting the user agent and the read timeout of the request
|
18
|
+
# password = Pwned::HashedPassword.new("ABC123", headers: { "User-Agent" => "My user agent" }, read_timout: 10)
|
19
|
+
#
|
20
|
+
# @param hashed_password [String] The hash of the password you want to check against the API.
|
21
|
+
# @param [Hash] request_options Options that can be passed to +Net::HTTP.start+ when
|
22
|
+
# calling the API
|
23
|
+
# @option request_options [Symbol] :headers ({ "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })
|
24
|
+
# HTTP headers to include in the request
|
25
|
+
# @raise [TypeError] if the password is not a string.
|
26
|
+
# @since 2.1.0
|
27
|
+
def initialize(hashed_password, request_options={})
|
28
|
+
raise TypeError, "hashed_password must be of type String" unless hashed_password.is_a? String
|
29
|
+
@hashed_password = hashed_password.upcase
|
30
|
+
@request_options = Hash(request_options).dup
|
31
|
+
@request_headers = Hash(request_options.delete(:headers))
|
32
|
+
@request_headers = DEFAULT_REQUEST_HEADERS.merge(@request_headers)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/pwned/password.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
4
|
-
require 'net/http'
|
3
|
+
require "pwned/password_base"
|
5
4
|
|
6
5
|
module Pwned
|
7
6
|
##
|
@@ -9,27 +8,7 @@ module Pwned
|
|
9
8
|
# Pwned Passwords API to find out if the password has been pwned.
|
10
9
|
# @see https://haveibeenpwned.com/API/v2#PwnedPasswords
|
11
10
|
class Password
|
12
|
-
|
13
|
-
# The base URL for the Pwned Passwords API
|
14
|
-
API_URL = "https://api.pwnedpasswords.com/range/"
|
15
|
-
|
16
|
-
##
|
17
|
-
# The number of characters from the start of the hash of the password that
|
18
|
-
# are used to search for the range of passwords.
|
19
|
-
HASH_PREFIX_LENGTH = 5
|
20
|
-
|
21
|
-
##
|
22
|
-
# The total length of a SHA1 hash
|
23
|
-
SHA1_LENGTH = 40
|
24
|
-
|
25
|
-
##
|
26
|
-
# The default request headers that are used to make HTTP requests to the
|
27
|
-
# API. A user agent is provided as requested in the documentation.
|
28
|
-
# @see https://haveibeenpwned.com/API/v2#UserAgent
|
29
|
-
DEFAULT_REQUEST_HEADERS = {
|
30
|
-
"User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}"
|
31
|
-
}.freeze
|
32
|
-
|
11
|
+
include PasswordBase
|
33
12
|
##
|
34
13
|
# @return [String] the password that is being checked.
|
35
14
|
# @since 1.0.0
|
@@ -46,7 +25,7 @@ module Pwned
|
|
46
25
|
# @param password [String] The password you want to check against the API.
|
47
26
|
# @param [Hash] request_options Options that can be passed to +Net::HTTP.start+ when
|
48
27
|
# calling the API
|
49
|
-
# @option request_options [Symbol] :headers ({ "User-Agent" =>
|
28
|
+
# @option request_options [Symbol] :headers ({ "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })
|
50
29
|
# HTTP headers to include in the request
|
51
30
|
# @return [Boolean] Whether the password appears in the data breaches or not.
|
52
31
|
# @raise [TypeError] if the password is not a string.
|
@@ -54,110 +33,10 @@ module Pwned
|
|
54
33
|
def initialize(password, request_options={})
|
55
34
|
raise TypeError, "password must be of type String" unless password.is_a? String
|
56
35
|
@password = password
|
36
|
+
@hashed_password = Pwned.hash_password(password)
|
57
37
|
@request_options = Hash(request_options).dup
|
58
38
|
@request_headers = Hash(request_options.delete(:headers))
|
59
39
|
@request_headers = DEFAULT_REQUEST_HEADERS.merge(@request_headers)
|
60
40
|
end
|
61
|
-
|
62
|
-
##
|
63
|
-
# Returns the full SHA1 hash of the given password in uppercase.
|
64
|
-
# @return [String] The full SHA1 hash of the given password.
|
65
|
-
# @since 1.0.0
|
66
|
-
def hashed_password
|
67
|
-
@hashed_password ||= Digest::SHA1.hexdigest(password).upcase
|
68
|
-
end
|
69
|
-
|
70
|
-
##
|
71
|
-
# @example
|
72
|
-
# password = Pwned::Password.new("password")
|
73
|
-
# password.pwned? #=> true
|
74
|
-
#
|
75
|
-
# @return [Boolean] +true+ when the password has been pwned.
|
76
|
-
# @raise [Pwned::Error] if there are errors with the HTTP request.
|
77
|
-
# @raise [Pwned::TimeoutError] if the HTTP request times out.
|
78
|
-
# @since 1.0.0
|
79
|
-
def pwned?
|
80
|
-
pwned_count > 0
|
81
|
-
end
|
82
|
-
|
83
|
-
##
|
84
|
-
# @example
|
85
|
-
# password = Pwned::Password.new("password")
|
86
|
-
# password.pwned_count #=> 3303003
|
87
|
-
#
|
88
|
-
# @return [Integer] the number of times the password has been pwned.
|
89
|
-
# @raise [Pwned::Error] if there are errors with the HTTP request.
|
90
|
-
# @raise [Pwned::TimeoutError] if the HTTP request times out.
|
91
|
-
# @since 1.0.0
|
92
|
-
def pwned_count
|
93
|
-
@pwned_count ||= fetch_pwned_count
|
94
|
-
end
|
95
|
-
|
96
|
-
private
|
97
|
-
|
98
|
-
attr_reader :request_options, :request_headers
|
99
|
-
|
100
|
-
def fetch_pwned_count
|
101
|
-
for_each_response_line do |line|
|
102
|
-
next unless line.start_with?(hashed_password_suffix)
|
103
|
-
# Count starts after the suffix, followed by a colon
|
104
|
-
return line[(SHA1_LENGTH-HASH_PREFIX_LENGTH+1)..-1].to_i
|
105
|
-
end
|
106
|
-
|
107
|
-
# The hash was not found, we can assume the password is not pwned [yet]
|
108
|
-
0
|
109
|
-
end
|
110
|
-
|
111
|
-
def for_each_response_line(&block)
|
112
|
-
begin
|
113
|
-
with_http_response "#{API_URL}#{hashed_password_prefix}" do |response|
|
114
|
-
response.value # raise if request was unsuccessful
|
115
|
-
stream_response_lines(response, &block)
|
116
|
-
end
|
117
|
-
rescue Timeout::Error => e
|
118
|
-
raise Pwned::TimeoutError, e.message
|
119
|
-
rescue => e
|
120
|
-
raise Pwned::Error, e.message
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
def hashed_password_prefix
|
125
|
-
hashed_password[0...HASH_PREFIX_LENGTH]
|
126
|
-
end
|
127
|
-
|
128
|
-
def hashed_password_suffix
|
129
|
-
hashed_password[HASH_PREFIX_LENGTH..-1]
|
130
|
-
end
|
131
|
-
|
132
|
-
# Make a HTTP GET request given the url and headers.
|
133
|
-
# Yields a `Net::HTTPResponse`.
|
134
|
-
def with_http_response(url, &block)
|
135
|
-
uri = URI(url)
|
136
|
-
|
137
|
-
request = Net::HTTP::Get.new(uri)
|
138
|
-
request.initialize_http_header(request_headers)
|
139
|
-
request_options[:use_ssl] = true
|
140
|
-
|
141
|
-
Net::HTTP.start(uri.host, uri.port, request_options) do |http|
|
142
|
-
http.request(request, &block)
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
# Stream a Net::HTTPResponse by line, handling lines that cross chunks.
|
147
|
-
def stream_response_lines(response, &block)
|
148
|
-
last_line = ''
|
149
|
-
|
150
|
-
response.read_body do |chunk|
|
151
|
-
chunk_lines = (last_line + chunk).lines
|
152
|
-
# This could end with half a line, so save it for next time. If
|
153
|
-
# chunk_lines is empty, pop returns nil, so this also ensures last_line
|
154
|
-
# is always a string.
|
155
|
-
last_line = chunk_lines.pop || ''
|
156
|
-
chunk_lines.each(&block)
|
157
|
-
end
|
158
|
-
|
159
|
-
yield last_line unless last_line.empty?
|
160
|
-
end
|
161
|
-
|
162
41
|
end
|
163
42
|
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
require "net/http"
|
5
|
+
|
6
|
+
module Pwned
|
7
|
+
##
|
8
|
+
# This class represents a password. It does all the work of talking to the
|
9
|
+
# Pwned Passwords API to find out if the password has been pwned.
|
10
|
+
# @see https://haveibeenpwned.com/API/v2#PwnedPasswords
|
11
|
+
module PasswordBase
|
12
|
+
##
|
13
|
+
# The base URL for the Pwned Passwords API
|
14
|
+
API_URL = "https://api.pwnedpasswords.com/range/"
|
15
|
+
|
16
|
+
##
|
17
|
+
# The number of characters from the start of the hash of the password that
|
18
|
+
# are used to search for the range of passwords.
|
19
|
+
HASH_PREFIX_LENGTH = 5
|
20
|
+
|
21
|
+
##
|
22
|
+
# The total length of a SHA1 hash
|
23
|
+
SHA1_LENGTH = 40
|
24
|
+
|
25
|
+
##
|
26
|
+
# The default request headers that are used to make HTTP requests to the
|
27
|
+
# API. A user agent is provided as requested in the documentation.
|
28
|
+
# @see https://haveibeenpwned.com/API/v2#UserAgent
|
29
|
+
DEFAULT_REQUEST_HEADERS = {
|
30
|
+
"User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}"
|
31
|
+
}.freeze
|
32
|
+
|
33
|
+
##
|
34
|
+
# @example
|
35
|
+
# password = Pwned::Password.new("password")
|
36
|
+
# password.pwned? #=> true
|
37
|
+
# password.pwned? #=> true
|
38
|
+
#
|
39
|
+
# @return [Boolean] +true+ when the password has been pwned.
|
40
|
+
# @raise [Pwned::Error] if there are errors with the HTTP request.
|
41
|
+
# @raise [Pwned::TimeoutError] if the HTTP request times out.
|
42
|
+
# @since 1.0.0
|
43
|
+
def pwned?
|
44
|
+
pwned_count > 0
|
45
|
+
end
|
46
|
+
|
47
|
+
##
|
48
|
+
# @example
|
49
|
+
# password = Pwned::Password.new("password")
|
50
|
+
# password.pwned_count #=> 3303003
|
51
|
+
#
|
52
|
+
# @return [Integer] the number of times the password has been pwned.
|
53
|
+
# @raise [Pwned::Error] if there are errors with the HTTP request.
|
54
|
+
# @raise [Pwned::TimeoutError] if the HTTP request times out.
|
55
|
+
# @since 1.0.0
|
56
|
+
def pwned_count
|
57
|
+
@pwned_count ||= fetch_pwned_count
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Returns the full SHA1 hash of the given password in uppercase.
|
62
|
+
# @return [String] The full SHA1 hash of the given password.
|
63
|
+
# @since 1.0.0
|
64
|
+
attr_reader :hashed_password
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
attr_reader :request_options, :request_headers
|
69
|
+
|
70
|
+
def fetch_pwned_count
|
71
|
+
for_each_response_line do |line|
|
72
|
+
next unless line.start_with?(hashed_password_suffix)
|
73
|
+
# Count starts after the suffix, followed by a colon
|
74
|
+
return line[(SHA1_LENGTH-HASH_PREFIX_LENGTH+1)..-1].to_i
|
75
|
+
end
|
76
|
+
|
77
|
+
# The hash was not found, we can assume the password is not pwned [yet]
|
78
|
+
0
|
79
|
+
end
|
80
|
+
|
81
|
+
def for_each_response_line(&block)
|
82
|
+
begin
|
83
|
+
with_http_response "#{API_URL}#{hashed_password_prefix}" do |response|
|
84
|
+
response.value # raise if request was unsuccessful
|
85
|
+
stream_response_lines(response, &block)
|
86
|
+
end
|
87
|
+
rescue Timeout::Error => e
|
88
|
+
raise Pwned::TimeoutError, e.message
|
89
|
+
rescue => e
|
90
|
+
raise Pwned::Error, e.message
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def hashed_password_prefix
|
95
|
+
@hashed_password[0...HASH_PREFIX_LENGTH]
|
96
|
+
end
|
97
|
+
|
98
|
+
def hashed_password_suffix
|
99
|
+
@hashed_password[HASH_PREFIX_LENGTH..-1]
|
100
|
+
end
|
101
|
+
|
102
|
+
# Make a HTTP GET request given the url and headers.
|
103
|
+
# Yields a `Net::HTTPResponse`.
|
104
|
+
def with_http_response(url, &block)
|
105
|
+
uri = URI(url)
|
106
|
+
|
107
|
+
request = Net::HTTP::Get.new(uri)
|
108
|
+
request.initialize_http_header(request_headers)
|
109
|
+
request_options[:use_ssl] = true
|
110
|
+
|
111
|
+
Net::HTTP.start(uri.host, uri.port, request_options) do |http|
|
112
|
+
http.request(request, &block)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Stream a Net::HTTPResponse by line, handling lines that cross chunks.
|
117
|
+
def stream_response_lines(response, &block)
|
118
|
+
last_line = ""
|
119
|
+
|
120
|
+
response.read_body do |chunk|
|
121
|
+
chunk_lines = (last_line + chunk).lines
|
122
|
+
# This could end with half a line, so save it for next time. If
|
123
|
+
# chunk_lines is empty, pop returns nil, so this also ensures last_line
|
124
|
+
# is always a string.
|
125
|
+
last_line = chunk_lines.pop || ""
|
126
|
+
chunk_lines.each(&block)
|
127
|
+
end
|
128
|
+
|
129
|
+
yield last_line unless last_line.empty?
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
end
|