Skip to content

Commit

Permalink
Merge branch 'main' of github.com:philnash/pwned
Browse files Browse the repository at this point in the history
  • Loading branch information
philnash committed Feb 23, 2022
2 parents 362f306 + dcfbb25 commit b08dbb3
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 18 deletions.
8 changes: 7 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ruby: [2.5, 2.6, 2.7, 3.0, head]
ruby: [2.6, 2.7, 3.0, 3.1, head]
rails: [4.2.11.3, 5.0.7.2, 5.1.7, 5.2.4.4, 6.0.3.4, 6.1.0]
exclude:
# Ruby 3.0 and Rails 5 do not get along together.
Expand All @@ -18,6 +18,12 @@ jobs:
rails: 5.1.7
- ruby: 3.0
rails: 5.2.4.4
- ruby: 3.1
rails: 5.0.7.2
- ruby: 3.1
rails: 5.1.7
- ruby: 3.1
rails: 5.2.4.4
- ruby: head
rails: 5.0.7.2
- ruby: head
Expand Down
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,20 @@ Pwned.pwned_count("password")

#### Custom request options

You can set http request options to be used with `Net::HTTP.start` when making the request to the API. These options are documented in the [`Net::HTTP.start` documentation](https://ruby-doc.org/stdlib-3.0.0/libdoc/net/http/rdoc/Net/HTTP.html#method-c-start). For example:
You can set HTTP request options to be used with `Net::HTTP.start` when making the request to the API. These options are documented in the [`Net::HTTP.start` documentation](https://ruby-doc.org/stdlib-3.0.0/libdoc/net/http/rdoc/Net/HTTP.html#method-c-start).

You can pass the options to the constructor:

```ruby
password = Pwned::Password.new("password", read_timeout: 10)
```

You can also specify global defaults:

```ruby
Pwned.default_request_options = { read_timeout: 10 }
```

##### HTTP Headers

The `:headers` option defines defines HTTP headers. These headers must be string keys.
Expand Down Expand Up @@ -220,7 +228,7 @@ end

#### Custom Request Options

You can configure network requests made from the validator using `:request_options` (see [Net::HTTP.start](http://ruby-doc.org/stdlib-2.6.3/libdoc/net/http/rdoc/Net/HTTP.html#method-c-start) for the list of available options).
You can configure network requests made from the validator using `:request_options` (see [Net::HTTP.start](http://ruby-doc.org/stdlib-2.6.3/libdoc/net/http/rdoc/Net/HTTP.html#method-c-start) for the list of available options).

```ruby
validates :password, not_pwned: {
Expand All @@ -231,6 +239,8 @@ You can configure network requests made from the validator using `:request_optio
}
```

These options override the globally defined default options (see above).

In addition to these options, you can also set the following:

##### HTTP Headers
Expand Down Expand Up @@ -278,15 +288,15 @@ If you don't want to set a proxy and you don't want a proxy to be inferred from

### Using Asynchronously

You may have a use case for hashing the password in advance, and then making the call to the Pwned Passwords API later (for example if you want to enqueue a job without storing the plaintext password). To do this, you can hash the password with the `Pwned.hash_password` method and then initialize the `Pwned::HashPassword` class with the hash, like this:
You may have a use case for hashing the password in advance, and then making the call to the Pwned Passwords API later (for example if you want to enqueue a job without storing the plaintext password). To do this, you can hash the password with the `Pwned.hash_password` method and then initialize the `Pwned::HashedPassword` class with the hash, like this:

```ruby
hashed_password = Pwned.hash_password(password)
# some time later
Pwned::HashPassword.new(hashed_password, request_options).pwned?
Pwned::HashedPassword.new(hashed_password, request_options).pwned?
```

The `Pwned::HashPassword` constructor takes all the same options as the regular `Pwned::Password` contructor.
The `Pwned::HashedPassword` constructor takes all the same options as the regular `Pwned::Password` contructor.

### Devise

Expand Down
23 changes: 23 additions & 0 deletions lib/pwned.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,29 @@
# results for a password.

module Pwned
@default_request_options = {}

##
# The default request options passed to +Net::HTTP.start+ when calling the API.
#
# @return [Hash]
# @see Pwned::Password#initialize
def self.default_request_options
@default_request_options
end

##
# Sets the default request options passed to +Net::HTTP.start+ when calling
# the API.
#
# The default options may be overridden in +Pwned::Password#new+.
#
# @param [Hash] request_options
# @see Pwned::Password#initialize
def self.default_request_options=(request_options)
@default_request_options = request_options
end

##
# Returns +true+ when the password has been pwned.
#
Expand Down
13 changes: 13 additions & 0 deletions lib/pwned/deep_merge.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module DeepMerge
refine Hash do
def deep_merge(other)
self.merge(other) do |key, this_val, other_val|
if this_val.is_a?(Hash) && other_val.is_a?(Hash)
this_val.deep_merge(other_val)
else
other_val
end
end
end
end
end
13 changes: 8 additions & 5 deletions lib/pwned/hashed_password.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

require "pwned/password_base"
require "pwned/deep_merge"


module Pwned
##
Expand All @@ -9,6 +11,7 @@ module Pwned
# @see https://haveibeenpwned.com/API/v2#PwnedPasswords
class HashedPassword
include PasswordBase
using DeepMerge
##
# Creates a new hashed password object.
#
Expand All @@ -19,7 +22,7 @@ class HashedPassword
#
# @param hashed_password [String] The hash of the password you want to check against the API.
# @param [Hash] request_options Options that can be passed to +Net::HTTP.start+ when
# calling the API
# calling the API. This overrides any keys specified in +Pwned.default_request_options+.
# @option request_options [Symbol] :headers ({ "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })
# HTTP headers to include in the request
# @option request_options [Symbol] :ignore_env_proxy (false) The library
Expand All @@ -30,11 +33,11 @@ class HashedPassword
def initialize(hashed_password, request_options={})
raise TypeError, "hashed_password must be of type String" unless hashed_password.is_a? String
@hashed_password = hashed_password.upcase
@request_options = Hash(request_options).dup
@request_headers = Hash(request_options.delete(:headers))
@request_options = Pwned.default_request_options.deep_merge(request_options)
@request_headers = Hash(@request_options.delete(:headers))
@request_headers = DEFAULT_REQUEST_HEADERS.merge(@request_headers)
@request_proxy = URI(request_options.delete(:proxy)) if request_options.key?(:proxy)
@ignore_env_proxy = request_options.delete(:ignore_env_proxy) || false
@request_proxy = URI(@request_options.delete(:proxy)) if @request_options.key?(:proxy)
@ignore_env_proxy = @request_options.delete(:ignore_env_proxy) || false
end
end
end
12 changes: 7 additions & 5 deletions lib/pwned/password.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require "pwned/password_base"
require "pwned/deep_merge"

module Pwned
##
Expand All @@ -9,6 +10,7 @@ module Pwned
# @see https://haveibeenpwned.com/API/v2#PwnedPasswords
class Password
include PasswordBase
using DeepMerge
##
# @return [String] the password that is being checked.
# @since 1.0.0
Expand All @@ -24,7 +26,7 @@ class Password
#
# @param password [String] The password you want to check against the API.
# @param [Hash] request_options Options that can be passed to +Net::HTTP.start+ when
# calling the API
# calling the API. This overrides any keys specified in +Pwned.default_request_options+.
# @option request_options [Symbol] :headers ({ "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })
# HTTP headers to include in the request
# @option request_options [Symbol] :ignore_env_proxy (false) The library
Expand All @@ -36,11 +38,11 @@ def initialize(password, request_options={})
raise TypeError, "password must be of type String" unless password.is_a? String
@password = password
@hashed_password = Pwned.hash_password(password)
@request_options = Hash(request_options).dup
@request_headers = Hash(request_options.delete(:headers))
@request_options = Pwned.default_request_options.deep_merge(request_options)
@request_headers = Hash(@request_options.delete(:headers))
@request_headers = DEFAULT_REQUEST_HEADERS.merge(@request_headers)
@request_proxy = URI(request_options.delete(:proxy)) if request_options.key?(:proxy)
@ignore_env_proxy = request_options.delete(:ignore_env_proxy) || false
@request_proxy = URI(@request_options.delete(:proxy)) if @request_options.key?(:proxy)
@ignore_env_proxy = @request_options.delete(:ignore_env_proxy) || false
end
end
end
25 changes: 24 additions & 1 deletion spec/pwned/not_pwned_validator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ def create_model(password)
end

it "allows the user agent to be set" do
# Default option should be overridden
Pwned.default_request_options = { headers: { "User-Agent" => "Default user agent" } }

Model.validates :password, not_pwned: {
request_options: { headers: { "User-Agent" => "Super fun user agent" } }
}
Expand All @@ -44,7 +47,10 @@ def create_model(password)
to have_been_made.once
end

it "allows the proxy to be set" do
it "allows the proxy to be set via options" do
# Default option should be overridden
Pwned.default_request_options = { proxy: "https://username:[email protected]:12345" }

Model.validates :password, not_pwned: {
request_options: { proxy: "https://username:[email protected]:12345" }
}
Expand All @@ -61,6 +67,23 @@ def create_model(password)
with(headers: { "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })).
to have_been_made.once
end

it "allows the proxy to be set via default options" do
Pwned.default_request_options = { proxy: "https://username:[email protected]:12345" }
Model.validates :password, not_pwned: true
model = create_model("password")

# Webmock doesn't support proxy assertions (https://github.com/bblimke/webmock/issues/753)
# so we check that Net::HTTP receives the correct arguments.
expect(Net::HTTP).to receive(:start).
with("api.pwnedpasswords.com", 443, "default.com", 12345, "username", "password", anything).
and_call_original

expect(model).to_not be_valid
expect(a_request(:get, "https://api.pwnedpasswords.com/range/5BAA6").
with(headers: { "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" })).
to have_been_made.once
end
end

describe "when not pwned", pwned_range: "37D5B" do
Expand Down
45 changes: 44 additions & 1 deletion spec/pwned/password_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ def verify_not_found_error(error)
to have_been_made.once
end

it "allows the user agent to be set" do
it "allows the user agent to be set in constructor" do
Pwned.default_request_options = { headers: { "User-Agent" => "Default user agent" } }
password = Pwned::Password.new("password", headers: { "User-Agent" => "Super fun user agent" })
password.pwned?

Expand All @@ -149,6 +150,26 @@ def verify_not_found_error(error)
to have_been_made.once
end

it "allows the user agent to be set with default settings" do
Pwned.default_request_options = { headers: { "User-Agent" => "Default user agent" } }
password = Pwned::Password.new("password")
password.pwned?

expect(a_request(:get, "https://api.pwnedpasswords.com/range/5BAA6").
with(headers: { "User-Agent" => "Default user agent" })).
to have_been_made.once
end

it "allows headers to be set by default or in the constructor and merges them" do
Pwned.default_request_options = { headers: { "User-Agent" => "Default user agent" } }
password = Pwned::Password.new("password", headers: { "X-Test-Header" => "this-is-a-test" })
password.pwned?

expect(a_request(:get, "https://api.pwnedpasswords.com/range/5BAA6").
with(headers: { "User-Agent" => "Default user agent", "X-Test-Header" => "this-is-a-test" })).
to have_been_made.once
end

let(:subject) { Pwned::Password.new("password", request_options).pwned? }

let(:request_options) { {} }
Expand Down Expand Up @@ -274,6 +295,28 @@ def verify_not_found_error(error)
include_examples "doesn't use proxy from environment"
end
end

context "proxy given in default request options" do
before { Pwned.default_request_options = { proxy: "https://username:[email protected]:12345" } }

it "uses proxy from the default require options" do
expect(Net::HTTP).to receive(:start).and_wrap_original do |original_method, *args, &block|
http = original_method.call(*args)
expect(http.proxy_from_env?).to eq(false)
expect(http.proxy_address).to eq("default.com")
expect(http.proxy_user).to eq("username")
expect(http.proxy_pass).to eq("password")
expect(http.proxy_port).to eq(12_345)
original_method.call(*args, &block)
end

subject

expect(a_request(:get, "https://api.pwnedpasswords.com/range/5BAA6")
.with(headers: { "User-Agent" => "Ruby Pwned::Password #{Pwned::VERSION}" }))
.to have_been_made.once
end
end
end

describe "streaming", pwned_range: "A0F41" do
Expand Down
4 changes: 4 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@
config.expect_with :rspec do |c|
c.syntax = :expect
end

config.after do |c|
Pwned.default_request_options = {}
end
end

0 comments on commit b08dbb3

Please sign in to comment.