Module: Dommy::Rack::Url

Defined in:
lib/dommy/rack/url.rb

Overview

IRI → URI normalization. Links and redirect Locations on real pages often carry raw, unescaped UTF-8 (e.g. https://note.com/hashtag/応援). Ruby's stdlib URI parser is ASCII-only and raises URI::InvalidURIError on such a string, which would crash navigation / cookie matching. A browser percent-encodes the non-ASCII bytes (UTF-8) before parsing; this does the same.

Class Method Summary collapse

Class Method Details

.encode_iri(url) ⇒ Object

Percent-encode the non-ASCII characters in url so the ASCII-only URI parser accepts it. Already-encoded %XX and every ASCII character (including reserved / sub-delims and % itself) are left untouched, so the result is idempotent. The authority (host) is left alone: a non-ASCII host needs IDNA/Punycode, which is a separate concern — only the path / query / fragment bytes are escaped.



22
23
24
25
26
27
28
# File 'lib/dommy/rack/url.rb', line 22

def encode_iri(url)
  str = url.to_s
  return str if str.ascii_only?

  prefix, rest = split_authority(str)
  prefix + escape_non_ascii(rest)
end

.escape_non_ascii(str) ⇒ Object

Escape every non-ASCII byte as %XX (UTF-8), so a multibyte character becomes its sequence of percent-encoded bytes (応 → %E5%BF%9C).



43
44
45
# File 'lib/dommy/rack/url.rb', line 43

def escape_non_ascii(str)
  str.b.gsub(/[^\x00-\x7F]/n) { |byte| format("%%%02X", byte.unpack1("C")) }
end

.http_host(uri) ⇒ Object

host or host:port, omitting a default port (the Host header / tuple-origin serialization rule shared by HTTP and WebSocket).



49
50
51
# File 'lib/dommy/rack/url.rb', line 49

def http_host(uri)
  uri.port == uri.default_port ? uri.host : "#{uri.host}:#{uri.port}"
end

.origin(uri) ⇒ Object

scheme://host[:port], the tuple origin for uri (used for the Origin header a same-origin WebSocket connection presents).



55
56
57
# File 'lib/dommy/rack/url.rb', line 55

def origin(uri)
  "#{uri.scheme}://#{http_host(uri)}"
end

.split_authority(str) ⇒ Object

Split scheme://authority (left intact) from the path/query/fragment remainder. A string with no scheme://authority (a relative ref like /hashtag/応援) yields an empty prefix and is escaped whole.



33
34
35
36
37
38
39
# File 'lib/dommy/rack/url.rb', line 33

def split_authority(str)
  if (m = str.match(%r{\A([a-zA-Z][a-zA-Z0-9+.\-]*://[^/?#]*)(.*)\z}m))
    [m[1], m[2]]
  else
    ["", str]
  end
end