/
request.rb
178 lines (143 loc) · 4.7 KB
/
request.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
require 'jwt'
require 'base64'
module Webpush
# It is temporary URL until supported by the GCM server.
GCM_URL = 'https://android.googleapis.com/gcm/send'.freeze
TEMP_GCM_URL = 'https://gcm-http.googleapis.com/gcm'.freeze
class Request
def initialize(message: '', subscription:, vapid:, **options)
endpoint = subscription.fetch(:endpoint)
@endpoint = endpoint.gsub(GCM_URL, TEMP_GCM_URL)
@payload = build_payload(message, subscription)
@vapid_options = vapid
@options = default_options.merge(options)
end
def perform
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.ssl_timeout = @options[:ssl_timeout] unless @options[:ssl_timeout].nil?
http.open_timeout = @options[:open_timeout] unless @options[:open_timeout].nil?
http.read_timeout = @options[:read_timeout] unless @options[:read_timeout].nil?
req = Net::HTTP::Post.new(uri.request_uri, headers)
req.body = body
resp = http.request(req)
if resp.is_a?(Net::HTTPGone) # 410
raise ExpiredSubscription.new(resp, uri.host)
elsif resp.is_a?(Net::HTTPNotFound) # 404
raise InvalidSubscription.new(resp, uri.host)
elsif resp.is_a?(Net::HTTPUnauthorized) || resp.is_a?(Net::HTTPForbidden) || # 401, 403
resp.is_a?(Net::HTTPBadRequest) && resp.message == 'UnauthorizedRegistration' # 400, Google FCM
raise Unauthorized.new(resp, uri.host)
elsif resp.is_a?(Net::HTTPRequestEntityTooLarge) # 413
raise PayloadTooLarge.new(resp, uri.host)
elsif resp.is_a?(Net::HTTPTooManyRequests) # 429, try again later!
raise TooManyRequests.new(resp, uri.host)
elsif resp.is_a?(Net::HTTPServerError) # 5xx
raise PushServiceError.new(resp, uri.host)
elsif !resp.is_a?(Net::HTTPSuccess) # unknown/unhandled response error
raise ResponseError.new(resp, uri.host)
end
resp
end
def headers
headers = {}
headers['Content-Type'] = 'application/octet-stream'
headers['Ttl'] = ttl
headers['Urgency'] = urgency
if @payload.key?(:server_public_key)
headers['Content-Encoding'] = 'aesgcm'
headers['Encryption'] = "salt=#{salt_param}"
headers['Crypto-Key'] = "dh=#{dh_param}"
end
if api_key?
headers['Authorization'] = "key=#{api_key}"
elsif vapid?
vapid_headers = build_vapid_headers
headers['Authorization'] = vapid_headers['Authorization']
headers['Crypto-Key'] = [headers['Crypto-Key'], vapid_headers['Crypto-Key']].compact.join(';')
end
headers
end
def build_vapid_headers
vapid_key = vapid_pem ? VapidKey.from_pem(vapid_pem) : VapidKey.from_keys(vapid_public_key, vapid_private_key)
jwt = JWT.encode(jwt_payload, vapid_key.curve, 'ES256', jwt_header_fields)
p256ecdsa = vapid_key.public_key_for_push_header
{
'Authorization' => 'WebPush ' + jwt,
'Crypto-Key' => 'p256ecdsa=' + p256ecdsa
}
end
def body
@payload.fetch(:ciphertext, '')
end
private
def uri
@uri ||= URI.parse(@endpoint)
end
def ttl
@options.fetch(:ttl).to_s
end
def urgency
@options.fetch(:urgency).to_s
end
def dh_param
trim_encode64(@payload.fetch(:server_public_key))
end
def salt_param
trim_encode64(@payload.fetch(:salt))
end
def jwt_payload
{
aud: audience,
exp: Time.now.to_i + expiration,
sub: subject
}
end
def jwt_header_fields
{ 'typ' => 'JWT' }
end
def audience
uri.scheme + '://' + uri.host
end
def expiration
@vapid_options.fetch(:expiration, 24 * 60 * 60)
end
def subject
@vapid_options.fetch(:subject, 'sender@example.com')
end
def vapid_public_key
@vapid_options.fetch(:public_key, nil)
end
def vapid_private_key
@vapid_options.fetch(:private_key, nil)
end
def vapid_pem
@vapid_options.fetch(:pem, nil)
end
def default_options
{
ttl: 60 * 60 * 24 * 7 * 4, # 4 weeks
urgency: 'normal'
}
end
def build_payload(message, subscription)
return {} if message.nil? || message.empty?
encrypt_payload(message, subscription.fetch(:keys))
end
def encrypt_payload(message, p256dh:, auth:)
Encryption.encrypt(message, p256dh, auth)
end
def api_key
@options.fetch(:api_key, nil)
end
def api_key?
!(api_key.nil? || api_key.empty?) && @endpoint =~ %r{\Ahttps://(android|gcm-http)\.googleapis\.com}
end
def vapid?
@vapid_options.any?
end
def trim_encode64(bin)
Webpush.encode64(bin).delete('=')
end
end
end