defmodule HttpCookie.URL do @moduledoc false # 5.1.2. Canonicalized Host Names # # A canonicalized host name is the string generated by the following # algorithm: # # 1. Convert the host name to a sequence of individual domain name # labels. # # 2. Convert each label that is not a Non-Reserved LDH (NR-LDH) label, # to an A-label (see Section 2.3.2.1 of [RFC5890] for the former # and latter), or to a "punycode label" (a label resulting from the # "ToASCII" conversion in Section 4 of [RFC3490]), as appropriate # (see Section 6.3 of this specification). # # 3. Concatenate the resulting labels, separated by a %x2E (".") # character. def canonicalize_domain(domain) do domain |> :idna.to_ascii() |> to_string() |> String.downcase() end def normalize_path(path) when path in [nil, ""], do: "/" def normalize_path(path), do: path # 5.1.4. Paths and Path-Match # # The user agent MUST use an algorithm equivalent to the following # algorithm to compute the default-path of a cookie: # # 1. Let uri-path be the path portion of the request-uri if such a # portion exists (and empty otherwise). For example, if the # request-uri contains just a path (and optional query string), # then the uri-path is that path (without the %x3F ("?") character # or query string), and if the request-uri contains a full # absoluteURI, the uri-path is the path component of that URI. # # 2. If the uri-path is empty or if the first character of the uri- # path is not a %x2F ("/") character, output %x2F ("/") and skip # the remaining steps. # # 3. If the uri-path contains no more than one %x2F ("/") character, # output %x2F ("/") and skip the remaining step. # # 4. Output the characters of the uri-path from the first character up # to, but not including, the right-most %x2F ("/"). def default_path(request_url) do request_path = normalize_path(request_url.path) cond do request_path == "/" -> "/" request_path =~ ~r|^[^/]| -> "/" request_path =~ ~r|^/[^/]*$| -> "/" true -> String.split(request_path, ~r|/[^/]*$|) |> hd() end end # 5.1.4. Paths and Path-Match # [...] # A request-path path-matches a given cookie-path if at least one of # the following conditions holds: # # o The cookie-path and the request-path are identical. def path_match?(path, path), do: true # (continued) # o The cookie-path is a prefix of the request-path, and the last # character of the cookie-path is %x2F ("/"). # # o The cookie-path is a prefix of the request-path, and the first # character of the request-path that is not included in the cookie- # path is a %x2F ("/") character. def path_match?(request_path, cookie_path) do cond do String.starts_with?(request_path, cookie_path) and String.ends_with?(cookie_path, "/") -> true request_path =~ ~r|^#{Regex.escape(cookie_path)}/| -> true true -> false end end # 5.1.3. Domain Matching # # A string domain-matches a given domain string if at least one of the # following conditions hold: # # o The domain string and the string are identical. (Note that both # the domain string and the string will have been canonicalized to # lower case at this point.) def domain_match?(domain, domain), do: true # (continued) # o All of the following conditions hold: # # * The domain string is a suffix of the string. # # * The last character of the string that is not included in the # domain string is a %x2E (".") character. # # * The string is a host name (i.e., not an IP address). def domain_match?(request_domain, cookie_domain) do request_domain =~ ~r/\.#{Regex.escape(cookie_domain)}$/ and !ip_address?(request_domain) end # NOTE: A "public suffix" is a domain that is controlled by a # public registry, such as "com", "co.uk", and "pvt.k12.wy.us". # This step is essential for preventing attacker.com from # disrupting the integrity of example.com by setting a cookie # with a Domain attribute of "com". Unfortunately, the set of # public suffixes (also known as "registry controlled domains") # changes over time. If feasible, user agents SHOULD use an # up-to-date public suffix list, such as the one maintained by # the Mozilla project at . def public_suffix?(""), do: false if Code.ensure_loaded?(PublicSuffix) do def public_suffix?(domain) do domain == PublicSuffix.public_suffix(domain) end else def public_suffix?(_domain) do # work around for typing violation that's reported # because the function only returns false when the # dependency is missing - but it should not be called in that # case anyway because the option velidation should fail before that if :rand.uniform(10) <= 10 do raise "Missing :public_suffix dependency" else true end end end def ip_address?(str) do charlist = String.to_charlist(str) case :inet.parse_address(charlist) do {:ok, _ip} -> true _ -> false end end end