pezza_action_push_web 0.1.2 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb9c4ebdcbd17365efb10ed10e722ddc65e1313c46904a2fefcab41e2a77d7b3
4
- data.tar.gz: b470819fb3149a2a9b48d98ccdfce99aafa86bf7f7f153eb0019c33b15a2e9ac
3
+ metadata.gz: 2b38a01863916f285ea98b99ea79483767c4633bc1bf51a54c232703260e947c
4
+ data.tar.gz: e0cad328df4292a41a013554ff90176b8fe34baaaf77477a03e26620ae7e0075
5
5
  SHA512:
6
- metadata.gz: 3b550df8977bdd96661dd9cb96993697d409b0f6555a22e235429971b2e7cbd68a5800205526217b622a101774f1482b65fc60ef4a6ecd99467d8bc28cfb353b
7
- data.tar.gz: 644ee1e71e8d4065e87fe9334078725b801ff3a9c2a7ea6f81de719e3e53ec139c4642c4d19852fbed0b798eb051854901a79f3f561189d2419971dd9b5eadef
6
+ metadata.gz: 1f8cc2f332367bfef4d89b06864379447337e09ae825354aa712de31c863d3b2b5ea90451cab640aca6756b4808bb013f9afe8572d258c3288b097b38d5e8418
7
+ data.tar.gz: 151514426c7a3218ada6b191a8fe917364d7a571577ec9d0b1174cb0d73e2b0cc3a91182dfabfa23934823879ab7d1a0dcdd409a1eb3b074b9ed85f1c7c108c5
data/README.md CHANGED
@@ -93,8 +93,8 @@ shared:
93
93
  # Change the subject (default: mailto:sender@example.com).
94
94
  # expiration: mailto:support@my-domain.com
95
95
 
96
- # Change the urgency (default: normal). You also choose to set this at the notification level.
97
- # urgency: high
96
+ # Change the urgency (default: high). You also choose to set this at the notification level.
97
+ # urgency: normal
98
98
  ```
99
99
 
100
100
  This file contains the configuration for the push notification services you want to use.
@@ -1,11 +1,8 @@
1
1
  module ActionPushWeb
2
2
  class SubscriptionsController < ApplicationController
3
3
  def create
4
- if subscription = ApplicationPushSubscription.find_by(push_subscription_params)
5
- subscription.touch
6
- else
7
- ApplicationPushSubscription.create! push_subscription_params.merge(user_agent: request.user_agent)
8
- end
4
+ ApplicationPushSubscription.create_with(user_agent: request.user_agent).
5
+ create_or_find_by!(push_subscription_params)
9
6
 
10
7
  head :ok
11
8
  end
@@ -0,0 +1,52 @@
1
+ module ActionPushWeb
2
+ module SsrfProtection
3
+ extend self
4
+
5
+ DNS_RESOLUTION_TIMEOUT = 2
6
+
7
+ DNS_NAMESERVERS = %w[
8
+ 1.1.1.1
9
+ 8.8.8.8
10
+ ]
11
+
12
+ DISALLOWED_IP_RANGES = [
13
+ IPAddr.new("0.0.0.0/8"), # "This" network (RFC1700)
14
+ IPAddr.new("100.64.0.0/10"), # Carrier-grade NAT (RFC6598)
15
+ IPAddr.new("198.18.0.0/15") # Benchmark testing (RFC2544)
16
+ ].freeze
17
+
18
+ def resolve_public_ip(hostname)
19
+ ip_addresses = resolve_dns(hostname)
20
+ public_ips = ip_addresses.reject { |ip| blocked_address?(ip) }
21
+ public_ips.sort_by { |ipaddr| ipaddr.ipv4? ? 0 : 1 }.first&.to_s
22
+ end
23
+
24
+ def blocked_address?(ip)
25
+ ip = IPAddr.new(ip.to_s) unless ip.is_a?(IPAddr)
26
+
27
+ ip.private? ||
28
+ ip.loopback? ||
29
+ ip.link_local? ||
30
+ ip.ipv4_mapped? ||
31
+ ip.ipv4_compat? ||
32
+ in_disallowed_range?(ip)
33
+ end
34
+
35
+ private
36
+ def resolve_dns(hostname)
37
+ ip_addresses = []
38
+
39
+ Resolv::DNS.open(nameserver: DNS_NAMESERVERS, timeouts: DNS_RESOLUTION_TIMEOUT) do |dns|
40
+ dns.each_address(hostname) do |ip_address|
41
+ ip_addresses << IPAddr.new(ip_address.to_s)
42
+ end
43
+ end
44
+
45
+ ip_addresses
46
+ end
47
+
48
+ def in_disallowed_range?(ip)
49
+ DISALLOWED_IP_RANGES.any? { |range| range.include?(ip) }
50
+ end
51
+ end
52
+ end
@@ -2,12 +2,53 @@ module ActionPushWeb
2
2
  class Subscription < ApplicationRecord
3
3
  include ActiveSupport::Rescuable
4
4
 
5
+ PERMITTED_ENDPOINT_HOSTS = %w[
6
+ jmt17.google.com
7
+ fcm.googleapis.com
8
+ updates.push.services.mozilla.com
9
+ web.push.apple.com
10
+ notify.windows.com
11
+ ].freeze
12
+
5
13
  rescue_from(TokenError) { destroy! }
6
14
 
7
15
  belongs_to :owner, polymorphic: true, optional: true
8
16
 
17
+ validates :endpoint, presence: true
18
+ validate :validate_endpoint_url
19
+
9
20
  def push(notification)
10
21
  ActionPushWeb.push(SubscriptionNotification.new(notification:, subscription: self))
11
22
  end
23
+
24
+ def resolved_endpoint_ip
25
+ return @resolved_endpoint_ip if defined?(@resolved_endpoint_ip)
26
+ @resolved_endpoint_ip = SsrfProtection.resolve_public_ip(endpoint_uri&.host)
27
+ end
28
+
29
+ private
30
+
31
+ def endpoint_uri
32
+ @endpoint_uri ||= URI.parse(endpoint) if endpoint.present?
33
+ rescue URI::InvalidURIError
34
+ nil
35
+ end
36
+
37
+ def validate_endpoint_url
38
+ if endpoint_uri.nil?
39
+ errors.add(:endpoint, "is not a valid URL")
40
+ elsif endpoint_uri.scheme != "https"
41
+ errors.add(:endpoint, "must use HTTPS")
42
+ elsif !permitted_endpoint_host?
43
+ errors.add(:endpoint, "is not a permitted push service")
44
+ elsif resolved_endpoint_ip.nil?
45
+ errors.add(:endpoint, "resolves to a private or invalid IP address")
46
+ end
47
+ end
48
+
49
+ def permitted_endpoint_host?
50
+ host = endpoint_uri&.host&.downcase
51
+ PERMITTED_ENDPOINT_HOSTS.any? { |permitted| host&.end_with?(permitted) }
52
+ end
12
53
  end
13
54
  end
@@ -0,0 +1,6 @@
1
+ class AddUniqueIndex < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_index :action_push_web_subscriptions,
4
+ [ :owner_type, :owner_id, :endpoint ], unique: true
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ class AddUniqueIndexForNullOwner < ActiveRecord::Migration[8.1]
2
+ def change
3
+ add_index :action_push_web_subscriptions, :endpoint, unique: true,
4
+ where: "owner_id IS NULL AND owner_type IS NULL"
5
+ end
6
+ end
@@ -46,7 +46,7 @@ module ActionPushWeb
46
46
  end
47
47
 
48
48
  def urgency
49
- (@urgency.presence || config.fetch(:urgency, :normal)).to_s
49
+ (@urgency.presence || config.fetch(:urgency, :high)).to_s
50
50
  end
51
51
 
52
52
  def as_json
@@ -10,7 +10,11 @@ module ActionPushWeb
10
10
  it.body = payload
11
11
  end
12
12
 
13
- connection.request(uri, request).tap { handle_response(it) }
13
+ if resolved_endpoint_ip
14
+ pinned_connection.request(request)
15
+ else
16
+ connection.request(uri, request)
17
+ end.tap { handle_response(it) }
14
18
  rescue OpenSSL::OpenSSLError
15
19
  raise TokenError.new
16
20
  end
@@ -20,7 +24,7 @@ module ActionPushWeb
20
24
  attr_reader :config, :notification
21
25
 
22
26
  delegate :title, :body, :icon_path, :path, :silent, :badge, :endpoint, :p256dh_key,
23
- :auth_key, to: :notification
27
+ :auth_key, :resolved_endpoint_ip, to: :notification
24
28
 
25
29
  def message
26
30
  JSON.generate title:, options: { body:, icon: icon_path, silent:, badge:, data: { path: } }
@@ -38,6 +42,13 @@ module ActionPushWeb
38
42
  @uri ||= URI.parse(endpoint)
39
43
  end
40
44
 
45
+ def pinned_connection
46
+ Net::HTTP.new(uri.host, uri.port).tap do |http|
47
+ http.ipaddr = resolved_endpoint_ip
48
+ http.use_ssl = true
49
+ end
50
+ end
51
+
41
52
  def headers
42
53
  headers = {}
43
54
  headers["Content-Type"] = "application/octet-stream"
@@ -8,6 +8,6 @@ module ActionPushWeb
8
8
  attr_reader :notification, :subscription
9
9
 
10
10
  delegate_missing_to :notification
11
- delegate :endpoint, :p256dh_key, :auth_key, to: :subscription
11
+ delegate :endpoint, :p256dh_key, :auth_key, :resolved_endpoint_ip, to: :subscription
12
12
  end
13
13
  end
@@ -1,3 +1,3 @@
1
1
  module ActionPushWeb
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -15,8 +15,8 @@ shared:
15
15
  # Change the subject (default: mailto:sender@example.com).
16
16
  # subject: mailto:support@my-domain.com
17
17
 
18
- # Change the urgency (default: normal). You also choose to set this at the notification level.
19
- # urgency: high
18
+ # Change the urgency (default: high). You also choose to set this at the notification level.
19
+ # urgency: normal
20
20
 
21
21
  # Change the icon path (default: nil). You also choose to set this at the notification level.
22
22
  # icon_path: https://example.com/icon.png
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pezza_action_push_web
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Pezza
@@ -112,10 +112,13 @@ files:
112
112
  - app/controllers/action_push_web/subscriptions_controller.rb
113
113
  - app/helpers/action_push_web/application_helper.rb
114
114
  - app/jobs/action_push_web/notification_job.rb
115
+ - app/models/action_push_web/ssrf_protection.rb
115
116
  - app/models/action_push_web/subscription.rb
116
117
  - config/importmap.rb
117
118
  - config/routes.rb
118
119
  - db/migrate/20250907213606_create_action_push_web_subscriptions.rb
120
+ - db/migrate/20251209235643_add_unique_index.rb
121
+ - db/migrate/20260517003340_add_unique_index_for_null_owner.rb
119
122
  - lib/action_push_web.rb
120
123
  - lib/action_push_web/engine.rb
121
124
  - lib/action_push_web/errors.rb
@@ -156,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
156
159
  - !ruby/object:Gem::Version
157
160
  version: '0'
158
161
  requirements: []
159
- rubygems_version: 3.7.1
162
+ rubygems_version: 4.0.11
160
163
  specification_version: 4
161
164
  summary: Send push notifications to web apps
162
165
  test_files: []