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 +4 -4
- data/README.md +2 -2
- data/app/controllers/action_push_web/subscriptions_controller.rb +2 -5
- data/app/models/action_push_web/ssrf_protection.rb +52 -0
- data/app/models/action_push_web/subscription.rb +41 -0
- data/db/migrate/20251209235643_add_unique_index.rb +6 -0
- data/db/migrate/20260517003340_add_unique_index_for_null_owner.rb +6 -0
- data/lib/action_push_web/notification.rb +1 -1
- data/lib/action_push_web/pusher.rb +13 -2
- data/lib/action_push_web/subscription_notification.rb +1 -1
- data/lib/action_push_web/version.rb +1 -1
- data/lib/generators/action_push_web/install/templates/config/push.yml.tt +2 -2
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2b38a01863916f285ea98b99ea79483767c4633bc1bf51a54c232703260e947c
|
|
4
|
+
data.tar.gz: e0cad328df4292a41a013554ff90176b8fe34baaaf77477a03e26620ae7e0075
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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:
|
|
97
|
-
# urgency:
|
|
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
|
-
|
|
5
|
-
|
|
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
|
|
@@ -10,7 +10,11 @@ module ActionPushWeb
|
|
|
10
10
|
it.body = payload
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
|
|
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
|
|
@@ -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:
|
|
19
|
-
# urgency:
|
|
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.
|
|
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:
|
|
162
|
+
rubygems_version: 4.0.11
|
|
160
163
|
specification_version: 4
|
|
161
164
|
summary: Send push notifications to web apps
|
|
162
165
|
test_files: []
|