ruby-openid2 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +136 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +54 -0
- data/LICENSE.txt +210 -0
- data/README.md +81 -0
- data/SECURITY.md +15 -0
- data/lib/hmac/hmac.rb +110 -0
- data/lib/hmac/sha1.rb +11 -0
- data/lib/hmac/sha2.rb +25 -0
- data/lib/openid/association.rb +246 -0
- data/lib/openid/consumer/associationmanager.rb +354 -0
- data/lib/openid/consumer/checkid_request.rb +179 -0
- data/lib/openid/consumer/discovery.rb +516 -0
- data/lib/openid/consumer/discovery_manager.rb +144 -0
- data/lib/openid/consumer/html_parse.rb +142 -0
- data/lib/openid/consumer/idres.rb +513 -0
- data/lib/openid/consumer/responses.rb +147 -0
- data/lib/openid/consumer/session.rb +36 -0
- data/lib/openid/consumer.rb +406 -0
- data/lib/openid/cryptutil.rb +112 -0
- data/lib/openid/dh.rb +84 -0
- data/lib/openid/extension.rb +38 -0
- data/lib/openid/extensions/ax.rb +552 -0
- data/lib/openid/extensions/oauth.rb +88 -0
- data/lib/openid/extensions/pape.rb +170 -0
- data/lib/openid/extensions/sreg.rb +268 -0
- data/lib/openid/extensions/ui.rb +49 -0
- data/lib/openid/fetchers.rb +277 -0
- data/lib/openid/kvform.rb +113 -0
- data/lib/openid/kvpost.rb +62 -0
- data/lib/openid/message.rb +555 -0
- data/lib/openid/protocolerror.rb +7 -0
- data/lib/openid/server.rb +1571 -0
- data/lib/openid/store/filesystem.rb +260 -0
- data/lib/openid/store/interface.rb +73 -0
- data/lib/openid/store/memcache.rb +109 -0
- data/lib/openid/store/memory.rb +79 -0
- data/lib/openid/store/nonce.rb +72 -0
- data/lib/openid/trustroot.rb +597 -0
- data/lib/openid/urinorm.rb +72 -0
- data/lib/openid/util.rb +119 -0
- data/lib/openid/version.rb +5 -0
- data/lib/openid/yadis/accept.rb +141 -0
- data/lib/openid/yadis/constants.rb +16 -0
- data/lib/openid/yadis/discovery.rb +151 -0
- data/lib/openid/yadis/filters.rb +192 -0
- data/lib/openid/yadis/htmltokenizer.rb +290 -0
- data/lib/openid/yadis/parsehtml.rb +50 -0
- data/lib/openid/yadis/services.rb +44 -0
- data/lib/openid/yadis/xrds.rb +160 -0
- data/lib/openid/yadis/xri.rb +86 -0
- data/lib/openid/yadis/xrires.rb +87 -0
- data/lib/openid.rb +27 -0
- data/lib/ruby-openid.rb +1 -0
- data.tar.gz.sig +0 -0
- metadata +331 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,597 @@
|
|
1
|
+
# stdlib
|
2
|
+
require "uri"
|
3
|
+
|
4
|
+
# This library
|
5
|
+
require_relative "urinorm"
|
6
|
+
|
7
|
+
module OpenID
|
8
|
+
class RealmVerificationRedirected < Exception
|
9
|
+
# Attempting to verify this realm resulted in a redirect.
|
10
|
+
def initialize(relying_party_url, rp_url_after_redirects)
|
11
|
+
@relying_party_url = relying_party_url
|
12
|
+
@rp_url_after_redirects = rp_url_after_redirects
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
"Attempting to verify #{@relying_party_url} resulted in " +
|
17
|
+
"redirect to #{@rp_url_after_redirects}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module TrustRoot
|
22
|
+
TOP_LEVEL_DOMAINS = %w[
|
23
|
+
ac
|
24
|
+
ad
|
25
|
+
ae
|
26
|
+
aero
|
27
|
+
af
|
28
|
+
ag
|
29
|
+
ai
|
30
|
+
al
|
31
|
+
am
|
32
|
+
an
|
33
|
+
ao
|
34
|
+
aq
|
35
|
+
ar
|
36
|
+
arpa
|
37
|
+
as
|
38
|
+
asia
|
39
|
+
at
|
40
|
+
au
|
41
|
+
aw
|
42
|
+
ax
|
43
|
+
az
|
44
|
+
ba
|
45
|
+
bb
|
46
|
+
bd
|
47
|
+
be
|
48
|
+
bf
|
49
|
+
bg
|
50
|
+
bh
|
51
|
+
bi
|
52
|
+
biz
|
53
|
+
bj
|
54
|
+
bm
|
55
|
+
bn
|
56
|
+
bo
|
57
|
+
br
|
58
|
+
bs
|
59
|
+
bt
|
60
|
+
bv
|
61
|
+
bw
|
62
|
+
by
|
63
|
+
bz
|
64
|
+
ca
|
65
|
+
cat
|
66
|
+
cc
|
67
|
+
cd
|
68
|
+
cf
|
69
|
+
cg
|
70
|
+
ch
|
71
|
+
ci
|
72
|
+
ck
|
73
|
+
cl
|
74
|
+
cm
|
75
|
+
cn
|
76
|
+
co
|
77
|
+
com
|
78
|
+
coop
|
79
|
+
cr
|
80
|
+
cu
|
81
|
+
cv
|
82
|
+
cx
|
83
|
+
cy
|
84
|
+
cz
|
85
|
+
de
|
86
|
+
dj
|
87
|
+
dk
|
88
|
+
dm
|
89
|
+
do
|
90
|
+
dz
|
91
|
+
ec
|
92
|
+
edu
|
93
|
+
ee
|
94
|
+
eg
|
95
|
+
er
|
96
|
+
es
|
97
|
+
et
|
98
|
+
eu
|
99
|
+
fi
|
100
|
+
fj
|
101
|
+
fk
|
102
|
+
fm
|
103
|
+
fo
|
104
|
+
fr
|
105
|
+
ga
|
106
|
+
gb
|
107
|
+
gd
|
108
|
+
ge
|
109
|
+
gf
|
110
|
+
gg
|
111
|
+
gh
|
112
|
+
gi
|
113
|
+
gl
|
114
|
+
gm
|
115
|
+
gn
|
116
|
+
gov
|
117
|
+
gp
|
118
|
+
gq
|
119
|
+
gr
|
120
|
+
gs
|
121
|
+
gt
|
122
|
+
gu
|
123
|
+
gw
|
124
|
+
gy
|
125
|
+
hk
|
126
|
+
hm
|
127
|
+
hn
|
128
|
+
hr
|
129
|
+
ht
|
130
|
+
hu
|
131
|
+
id
|
132
|
+
ie
|
133
|
+
il
|
134
|
+
im
|
135
|
+
in
|
136
|
+
info
|
137
|
+
int
|
138
|
+
io
|
139
|
+
iq
|
140
|
+
ir
|
141
|
+
is
|
142
|
+
it
|
143
|
+
je
|
144
|
+
jm
|
145
|
+
jo
|
146
|
+
jobs
|
147
|
+
jp
|
148
|
+
ke
|
149
|
+
kg
|
150
|
+
kh
|
151
|
+
ki
|
152
|
+
km
|
153
|
+
kn
|
154
|
+
kp
|
155
|
+
kr
|
156
|
+
kw
|
157
|
+
ky
|
158
|
+
kz
|
159
|
+
la
|
160
|
+
lb
|
161
|
+
lc
|
162
|
+
li
|
163
|
+
lk
|
164
|
+
lr
|
165
|
+
ls
|
166
|
+
lt
|
167
|
+
lu
|
168
|
+
lv
|
169
|
+
ly
|
170
|
+
ma
|
171
|
+
mc
|
172
|
+
md
|
173
|
+
me
|
174
|
+
mg
|
175
|
+
mh
|
176
|
+
mil
|
177
|
+
mk
|
178
|
+
ml
|
179
|
+
mm
|
180
|
+
mn
|
181
|
+
mo
|
182
|
+
mobi
|
183
|
+
mp
|
184
|
+
mq
|
185
|
+
mr
|
186
|
+
ms
|
187
|
+
mt
|
188
|
+
mu
|
189
|
+
museum
|
190
|
+
mv
|
191
|
+
mw
|
192
|
+
mx
|
193
|
+
my
|
194
|
+
mz
|
195
|
+
na
|
196
|
+
name
|
197
|
+
nc
|
198
|
+
ne
|
199
|
+
net
|
200
|
+
nf
|
201
|
+
ng
|
202
|
+
ni
|
203
|
+
nl
|
204
|
+
no
|
205
|
+
np
|
206
|
+
nr
|
207
|
+
nu
|
208
|
+
nz
|
209
|
+
om
|
210
|
+
org
|
211
|
+
pa
|
212
|
+
pe
|
213
|
+
pf
|
214
|
+
pg
|
215
|
+
ph
|
216
|
+
pk
|
217
|
+
pl
|
218
|
+
pm
|
219
|
+
pn
|
220
|
+
pr
|
221
|
+
pro
|
222
|
+
ps
|
223
|
+
pt
|
224
|
+
pw
|
225
|
+
py
|
226
|
+
qa
|
227
|
+
re
|
228
|
+
ro
|
229
|
+
rs
|
230
|
+
ru
|
231
|
+
rw
|
232
|
+
sa
|
233
|
+
sb
|
234
|
+
sc
|
235
|
+
sd
|
236
|
+
se
|
237
|
+
sg
|
238
|
+
sh
|
239
|
+
si
|
240
|
+
sj
|
241
|
+
sk
|
242
|
+
sl
|
243
|
+
sm
|
244
|
+
sn
|
245
|
+
so
|
246
|
+
sr
|
247
|
+
st
|
248
|
+
su
|
249
|
+
sv
|
250
|
+
sy
|
251
|
+
sz
|
252
|
+
tc
|
253
|
+
td
|
254
|
+
tel
|
255
|
+
tf
|
256
|
+
tg
|
257
|
+
th
|
258
|
+
tj
|
259
|
+
tk
|
260
|
+
tl
|
261
|
+
tm
|
262
|
+
tn
|
263
|
+
to
|
264
|
+
tp
|
265
|
+
tr
|
266
|
+
travel
|
267
|
+
tt
|
268
|
+
tv
|
269
|
+
tw
|
270
|
+
tz
|
271
|
+
ua
|
272
|
+
ug
|
273
|
+
uk
|
274
|
+
us
|
275
|
+
uy
|
276
|
+
uz
|
277
|
+
va
|
278
|
+
vc
|
279
|
+
ve
|
280
|
+
vg
|
281
|
+
vi
|
282
|
+
vn
|
283
|
+
vu
|
284
|
+
wf
|
285
|
+
ws
|
286
|
+
xn--0zwm56d
|
287
|
+
xn--11b5bs3a9aj6g
|
288
|
+
xn--80akhbyknj4f
|
289
|
+
xn--9t4b11yi5a
|
290
|
+
xn--deba0ad
|
291
|
+
xn--g6w251d
|
292
|
+
xn--hgbk6aj7f53bba
|
293
|
+
xn--hlcj6aya9esc7a
|
294
|
+
xn--jxalpdlp
|
295
|
+
xn--kgbechtv
|
296
|
+
xn--zckzah
|
297
|
+
ye
|
298
|
+
yt
|
299
|
+
yu
|
300
|
+
za
|
301
|
+
zm
|
302
|
+
zw
|
303
|
+
]
|
304
|
+
|
305
|
+
ALLOWED_PROTOCOLS = %w[http https]
|
306
|
+
|
307
|
+
# The URI for relying party discovery, used in realm verification.
|
308
|
+
#
|
309
|
+
# XXX: This should probably live somewhere else (like in
|
310
|
+
# OpenID or OpenID::Yadis somewhere)
|
311
|
+
RP_RETURN_TO_URL_TYPE = "http://specs.openid.net/auth/2.0/return_to"
|
312
|
+
|
313
|
+
# If the endpoint is a relying party OpenID return_to endpoint,
|
314
|
+
# return the endpoint URL. Otherwise, return None.
|
315
|
+
#
|
316
|
+
# This function is intended to be used as a filter for the Yadis
|
317
|
+
# filtering interface.
|
318
|
+
#
|
319
|
+
# endpoint: An XRDS BasicServiceEndpoint, as returned by
|
320
|
+
# performing Yadis dicovery.
|
321
|
+
#
|
322
|
+
# returns the endpoint URL or None if the endpoint is not a
|
323
|
+
# relying party endpoint.
|
324
|
+
def self._extract_return_url(endpoint)
|
325
|
+
return endpoint.uri if endpoint.matchTypes([RP_RETURN_TO_URL_TYPE])
|
326
|
+
|
327
|
+
nil
|
328
|
+
end
|
329
|
+
|
330
|
+
# Is the return_to URL under one of the supplied allowed
|
331
|
+
# return_to URLs?
|
332
|
+
def self.return_to_matches(allowed_return_to_urls, return_to)
|
333
|
+
allowed_return_to_urls.each do |allowed_return_to|
|
334
|
+
# A return_to pattern works the same as a realm, except that
|
335
|
+
# it's not allowed to use a wildcard. We'll model this by
|
336
|
+
# parsing it as a realm, and not trying to match it if it has
|
337
|
+
# a wildcard.
|
338
|
+
|
339
|
+
return_realm = TrustRoot.parse(allowed_return_to)
|
340
|
+
if !return_realm.nil? and
|
341
|
+
|
342
|
+
# Does not have a wildcard
|
343
|
+
!return_realm.wildcard and
|
344
|
+
|
345
|
+
# Matches the return_to that we passed in with it
|
346
|
+
return_realm.validate_url(return_to) # Parses as a trust root
|
347
|
+
|
348
|
+
return true
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
# No URL in the list matched
|
353
|
+
false
|
354
|
+
end
|
355
|
+
|
356
|
+
# Given a relying party discovery URL return a list of return_to
|
357
|
+
# URLs.
|
358
|
+
def self.get_allowed_return_urls(relying_party_url)
|
359
|
+
rp_url_after_redirects, return_to_urls = services.get_service_endpoints(
|
360
|
+
relying_party_url, _extract_return_url
|
361
|
+
)
|
362
|
+
|
363
|
+
if rp_url_after_redirects != relying_party_url
|
364
|
+
# Verification caused a redirect
|
365
|
+
raise RealmVerificationRedirected.new(
|
366
|
+
relying_party_url, rp_url_after_redirects
|
367
|
+
)
|
368
|
+
end
|
369
|
+
|
370
|
+
return_to_urls
|
371
|
+
end
|
372
|
+
|
373
|
+
# Verify that a return_to URL is valid for the given realm.
|
374
|
+
#
|
375
|
+
# This function builds a discovery URL, performs Yadis discovery
|
376
|
+
# on it, makes sure that the URL does not redirect, parses out
|
377
|
+
# the return_to URLs, and finally checks to see if the current
|
378
|
+
# return_to URL matches the return_to.
|
379
|
+
#
|
380
|
+
# raises DiscoveryFailure when Yadis discovery fails returns
|
381
|
+
# true if the return_to URL is valid for the realm
|
382
|
+
def self.verify_return_to(realm_str, return_to, _vrfy = nil)
|
383
|
+
# _vrfy parameter is there to make testing easier
|
384
|
+
_vrfy = method(:get_allowed_return_urls) if _vrfy.nil?
|
385
|
+
|
386
|
+
raise ArgumentError, "_vrfy must be a Proc or Method" unless _vrfy.is_a?(Proc) or _vrfy.is_a?(Method)
|
387
|
+
|
388
|
+
realm = TrustRoot.parse(realm_str)
|
389
|
+
if realm.nil?
|
390
|
+
# The realm does not parse as a URL pattern
|
391
|
+
return false
|
392
|
+
end
|
393
|
+
|
394
|
+
begin
|
395
|
+
allowable_urls = _vrfy.call(realm.build_discovery_url)
|
396
|
+
rescue RealmVerificationRedirected => e
|
397
|
+
Util.log(e.to_s)
|
398
|
+
return false
|
399
|
+
end
|
400
|
+
|
401
|
+
return true if return_to_matches(allowable_urls, return_to)
|
402
|
+
|
403
|
+
Util.log("Failed to validate return_to #{return_to} for " +
|
404
|
+
"realm #{realm_str}, was not in #{allowable_urls}")
|
405
|
+
false
|
406
|
+
end
|
407
|
+
|
408
|
+
class TrustRoot
|
409
|
+
attr_reader :unparsed, :proto, :wildcard, :host, :port, :path
|
410
|
+
|
411
|
+
@@empty_re = Regexp.new('^http[s]*:\/\/\*\/$')
|
412
|
+
|
413
|
+
def self._build_path(path, query = nil, frag = nil)
|
414
|
+
s = path.dup
|
415
|
+
|
416
|
+
frag = nil if frag == ""
|
417
|
+
query = nil if query == ""
|
418
|
+
|
419
|
+
s << "?" << query if query
|
420
|
+
|
421
|
+
s << "#" << frag if frag
|
422
|
+
|
423
|
+
s
|
424
|
+
end
|
425
|
+
|
426
|
+
def self._parse_url(url)
|
427
|
+
begin
|
428
|
+
url = URINorm.urinorm(url)
|
429
|
+
rescue URI::InvalidURIError
|
430
|
+
nil
|
431
|
+
end
|
432
|
+
|
433
|
+
begin
|
434
|
+
parsed = URI::DEFAULT_PARSER.parse(url)
|
435
|
+
rescue URI::InvalidURIError
|
436
|
+
return
|
437
|
+
end
|
438
|
+
|
439
|
+
path = TrustRoot._build_path(
|
440
|
+
parsed.path,
|
441
|
+
parsed.query,
|
442
|
+
parsed.fragment,
|
443
|
+
)
|
444
|
+
|
445
|
+
[
|
446
|
+
parsed.scheme || "",
|
447
|
+
parsed.host || "",
|
448
|
+
parsed.port || "",
|
449
|
+
path || "",
|
450
|
+
]
|
451
|
+
end
|
452
|
+
|
453
|
+
def self.parse(trust_root)
|
454
|
+
trust_root = trust_root.dup
|
455
|
+
unparsed = trust_root.dup
|
456
|
+
|
457
|
+
# look for wildcard
|
458
|
+
wildcard = !trust_root.index("://*.").nil?
|
459
|
+
trust_root.sub!("*.", "") if wildcard
|
460
|
+
|
461
|
+
# handle http://*/ case
|
462
|
+
if !wildcard and @@empty_re.match(trust_root)
|
463
|
+
proto = trust_root.split(":")[0]
|
464
|
+
port = (proto == "http") ? 80 : 443
|
465
|
+
return new(unparsed, proto, true, "", port, "/")
|
466
|
+
end
|
467
|
+
|
468
|
+
parts = TrustRoot._parse_url(trust_root)
|
469
|
+
return if parts.nil?
|
470
|
+
|
471
|
+
proto, host, port, path = parts
|
472
|
+
return if host[0] == "."
|
473
|
+
|
474
|
+
# check for URI fragment
|
475
|
+
return if path and !path.index("#").nil?
|
476
|
+
|
477
|
+
return unless %w[http https].member?(proto)
|
478
|
+
|
479
|
+
new(unparsed, proto, wildcard, host, port, path)
|
480
|
+
end
|
481
|
+
|
482
|
+
def self.check_sanity(trust_root_string)
|
483
|
+
trust_root = TrustRoot.parse(trust_root_string)
|
484
|
+
return false if trust_root.nil?
|
485
|
+
|
486
|
+
trust_root.sane?
|
487
|
+
end
|
488
|
+
|
489
|
+
# quick func for validating a url against a trust root. See the
|
490
|
+
# TrustRoot class if you need more control.
|
491
|
+
def self.check_url(trust_root, url)
|
492
|
+
tr = parse(trust_root)
|
493
|
+
(!tr.nil? and tr.validate_url(url))
|
494
|
+
end
|
495
|
+
|
496
|
+
# Return a discovery URL for this realm.
|
497
|
+
#
|
498
|
+
# This function does not check to make sure that the realm is
|
499
|
+
# valid. Its behaviour on invalid inputs is undefined.
|
500
|
+
#
|
501
|
+
# return_to:: The relying party return URL of the OpenID
|
502
|
+
# authentication request
|
503
|
+
#
|
504
|
+
# Returns the URL upon which relying party discovery should be
|
505
|
+
# run in order to verify the return_to URL
|
506
|
+
def build_discovery_url
|
507
|
+
return @unparsed unless wildcard
|
508
|
+
|
509
|
+
# Use "www." in place of the star
|
510
|
+
www_domain = "www." + @host
|
511
|
+
port = (!@port.nil? and ![80, 443].member?(@port)) ? (":" + @port.to_s) : ""
|
512
|
+
"#{@proto}://#{www_domain}#{port}#{@path}"
|
513
|
+
end
|
514
|
+
|
515
|
+
def initialize(unparsed, proto, wildcard, host, port, path)
|
516
|
+
@unparsed = unparsed
|
517
|
+
@proto = proto
|
518
|
+
@wildcard = wildcard
|
519
|
+
@host = host
|
520
|
+
@port = port
|
521
|
+
@path = path
|
522
|
+
end
|
523
|
+
|
524
|
+
def sane?
|
525
|
+
return true if @host == "localhost"
|
526
|
+
|
527
|
+
host_parts = @host.split(".")
|
528
|
+
|
529
|
+
# a note: ruby string split does not put an empty string at
|
530
|
+
# the end of the list if the split element is last. for
|
531
|
+
# example, 'foo.com.'.split('.') => ['foo','com']. Mentioned
|
532
|
+
# because the python code differs here.
|
533
|
+
|
534
|
+
return false if host_parts.length == 0
|
535
|
+
|
536
|
+
# no adjacent dots
|
537
|
+
return false if host_parts.member?("")
|
538
|
+
|
539
|
+
# last part must be a tld
|
540
|
+
tld = host_parts[-1]
|
541
|
+
return false unless TOP_LEVEL_DOMAINS.member?(tld)
|
542
|
+
|
543
|
+
return false if host_parts.length == 1
|
544
|
+
|
545
|
+
if @wildcard && (tld.length == 2 and host_parts[-2].length <= 3)
|
546
|
+
# It's a 2-letter tld with a short second to last segment
|
547
|
+
# so there needs to be more than two segments specified
|
548
|
+
# (e.g. *.co.uk is insane)
|
549
|
+
return host_parts.length > 2
|
550
|
+
end
|
551
|
+
|
552
|
+
true
|
553
|
+
end
|
554
|
+
|
555
|
+
def validate_url(url)
|
556
|
+
parts = TrustRoot._parse_url(url)
|
557
|
+
return false if parts.nil?
|
558
|
+
|
559
|
+
proto, host, port, path = parts
|
560
|
+
|
561
|
+
return false unless proto == @proto
|
562
|
+
return false unless port == @port
|
563
|
+
return false unless host.index("*").nil?
|
564
|
+
|
565
|
+
if !@wildcard
|
566
|
+
return false if host != @host
|
567
|
+
elsif (@host != "") and
|
568
|
+
!host.end_with?("." + @host) and
|
569
|
+
(host != @host)
|
570
|
+
return false
|
571
|
+
end
|
572
|
+
|
573
|
+
if path != @path
|
574
|
+
path_len = @path.length
|
575
|
+
trust_prefix = @path[0...path_len]
|
576
|
+
url_prefix = path[0...path_len]
|
577
|
+
|
578
|
+
# must be equal up to the length of the path, at least
|
579
|
+
return false if trust_prefix != url_prefix
|
580
|
+
|
581
|
+
# These characters must be on the boundary between the end
|
582
|
+
# of the trust root's path and the start of the URL's path.
|
583
|
+
allowed = if !@path.index("?").nil?
|
584
|
+
"&"
|
585
|
+
else
|
586
|
+
"?/"
|
587
|
+
end
|
588
|
+
|
589
|
+
return (!allowed.index(@path[-1]).nil? or
|
590
|
+
!allowed.index(path[path_len]).nil?)
|
591
|
+
end
|
592
|
+
|
593
|
+
true
|
594
|
+
end
|
595
|
+
end
|
596
|
+
end
|
597
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# stdlib
|
2
|
+
require "uri"
|
3
|
+
|
4
|
+
module OpenID
|
5
|
+
module URINorm
|
6
|
+
VALID_URI_SCHEMES = %w[http https].freeze
|
7
|
+
|
8
|
+
def self.urinorm(uri)
|
9
|
+
uri = URI.parse(uri)
|
10
|
+
|
11
|
+
raise URI::InvalidURIError.new("no scheme") unless uri.scheme
|
12
|
+
|
13
|
+
uri.scheme = uri.scheme.downcase
|
14
|
+
raise URI::InvalidURIError.new("Not an HTTP or HTTPS URI") unless VALID_URI_SCHEMES.member?(uri.scheme)
|
15
|
+
|
16
|
+
raise URI::InvalidURIError.new("no host") if uri.host.nil? # For Ruby 2.7
|
17
|
+
|
18
|
+
raise URI::InvalidURIError.new("no host") if uri.host.empty? # For Ruby 3+
|
19
|
+
|
20
|
+
uri.host = uri.host.downcase
|
21
|
+
|
22
|
+
uri.path = remove_dot_segments(uri.path)
|
23
|
+
uri.path = "/" if uri.path.empty?
|
24
|
+
|
25
|
+
uri = uri.normalize.to_s
|
26
|
+
uri.gsub(PERCENT_ESCAPE_RE) do
|
27
|
+
sub = ::Regexp.last_match(0)[1..2].to_i(16).chr
|
28
|
+
reserved(sub) ? ::Regexp.last_match(0).upcase : sub
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
RESERVED_RE = /[A-Za-z0-9._~-]/
|
33
|
+
PERCENT_ESCAPE_RE = /%[0-9a-zA-Z]{2}/
|
34
|
+
|
35
|
+
def self.reserved(chr)
|
36
|
+
!(RESERVED_RE =~ chr)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.remove_dot_segments(path)
|
40
|
+
result_segments = []
|
41
|
+
|
42
|
+
while path.length > 0
|
43
|
+
if path.start_with?("../")
|
44
|
+
path = path[3..-1]
|
45
|
+
elsif path.start_with?("./")
|
46
|
+
path = path[2..-1]
|
47
|
+
elsif path.start_with?("/./")
|
48
|
+
path = path[2..-1]
|
49
|
+
elsif path == "/."
|
50
|
+
path = "/"
|
51
|
+
elsif path.start_with?("/../")
|
52
|
+
path = path[3..-1]
|
53
|
+
result_segments.pop if result_segments.length > 0
|
54
|
+
elsif path == "/.."
|
55
|
+
path = "/"
|
56
|
+
result_segments.pop if result_segments.length > 0
|
57
|
+
elsif ["..", "."].include?(path)
|
58
|
+
path = ""
|
59
|
+
else
|
60
|
+
i = 0
|
61
|
+
i = 1 if path[0].chr == "/"
|
62
|
+
i = path.index("/", i)
|
63
|
+
i = path.length if i.nil?
|
64
|
+
result_segments << path[0...i]
|
65
|
+
path = path[i..-1]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
result_segments.join("")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|