From 6fe110be47c0fc34bc9dce6d377ce9b48ab27c2e Mon Sep 17 00:00:00 2001
From: Thijs Schreijer
+If host is not set, the host is taken from whichever single +one of hostname, ipv4 or ipv6 is set +(hosttype is ignored when building). It is an error to set more +than one of hostname, ipv4 or ipv6 while +host is absent. +
+The function returns a string with the built URL.
@@ -169,6 +177,62 @@ The function returns a string with the built <path> component. + + ++url.classify_host(host) +
+ ++Classifies a raw <host> string into "name", +"ipv4" or "ipv6". This is the same classification +parse uses to fill in hosttype, +exposed for callers that have a host string to classify without a full +URL to parse. +
+ ++Host is a host string as found in a URL's authority. Any +: in host is taken as a sign of an IPv6 literal, so +the enclosing [...] brackets URLs normally require for one are +optional here: they are stripped when present, but classification does +not depend on them. +
+ ++The function returns two values: hosttype, one of +"name", "ipv4" or "ipv6"; and host, +the input with any enclosing [...] brackets stripped, if +present. +
+ ++Note: classification is done by exclusion of shape, not by validating +the address. A host is "ipv4" if it merely has the shape of +four dot-separated digit groups — octet ranges are not checked, +so "999.1.1.1" classifies as "ipv4" — and +"ipv6" if it merely contains a :, whether or not that +is a well-formed IPv6 address. Anything left over is "name", +whether or not it is actually a valid hostname. +
+ +
+-- load url module
+url = require("socket.url")
+
+print(url.classify_host("example.com"))
+-- name example.com
+
+print(url.classify_host("192.168.1.1"))
+-- ipv4 192.168.1.1
+
+print(url.classify_host("[::1]"))
+-- ipv6 ::1
+
+print(url.classify_host("::1"))
+-- ipv6 ::1
+
+
@@ -230,12 +294,28 @@ parsed_url = {
fragment = string,
userinfo = string,
host = string,
+ hosttype = string,
+ hostname = string,
+ ipv4 = string,
+ ipv6 = string,
port = string,
user = string,
password = string
}
+
+When a host is present, hosttype is set to one of +"name", "ipv4" or "ipv6", describing the +syntax of host. Exactly one of hostname, ipv4 +or ipv6 is then also set to the same value as host, +matching hosttype — the other two are left nil. +In case of an ipv6 address the brackets are stripped, both in host and ipv6. +This classification is done by +classify_host, by exclusion of +shape rather than by validating the address (see its notes). +
+
-- load url module
url = require("socket.url")
@@ -248,6 +328,8 @@ parsed_url = url.parse("http://www.example.com/cgilua/index.lua?a=2#there")
-- query = "a=2",
-- fragment = "there",
-- host = "www.puc-rio.br",
+-- hosttype = "name",
+-- hostname = "www.puc-rio.br",
-- }
parsed_url = url.parse("ftp://root:passwd@unsafe.org/pub/virus.exe;type=i")
diff --git a/src/url.lua b/src/url.lua
index aef1698..be82421 100644
--- a/src/url.lua
+++ b/src/url.lua
@@ -122,6 +122,25 @@ local function absolute_path(base_path, relative_path)
return path
end
+-----------------------------------------------------------------------------
+-- Classifies a raw host string into "name", "ipv4" or "ipv6"
+-- Input
+-- raw: host part of the authority, as found in the URL (with its
+-- enclosing [...] brackets, if any)
+-- Returns
+-- hosttype: "name", "ipv4" or "ipv6"
+-- host: raw, with any enclosing [...] brackets stripped
+-----------------------------------------------------------------------------
+function _M.classify_host(raw)
+ if string.find(raw, ":", 1, true) then
+ return "ipv6", string.match(raw, "^%[?(.-)%]?$")
+ end
+ if string.match(raw, "^%d+%.%d+%.%d+%.%d+$") then
+ return "ipv4", raw
+ end
+ return "name", raw
+end
+
-----------------------------------------------------------------------------
-- Parses a url and returns a table with all its parts according to RFC 2396
-- The following grammar describes the names given to the URL parts
@@ -137,6 +156,10 @@ end
-- been preserved:
-- scheme, authority, userinfo, user, password, host, port,
-- path, params, query, fragment
+-- plus, when a host is present:
+-- hosttype: "name", "ipv4" or "ipv6", describing the syntax of host
+-- hostname, ipv4 or ipv6: same value as host, keyed by hosttype
+-- (only one of these three is ever set)
-- Obs:
-- the leading '/' in {/} is considered part of
-----------------------------------------------------------------------------
@@ -180,8 +203,10 @@ function _M.parse(url, default)
authority = string.gsub(authority, ":([^:%]]*)$",
function(p) parsed.port = p; return "" end)
if authority ~= "" then
- -- IPv6?
- parsed.host = string.match(authority, "^%[(.+)%]$") or authority
+ parsed.hosttype, parsed.host = _M.classify_host(authority)
+ if parsed.hosttype == "name" then parsed.hostname = parsed.host
+ elseif parsed.hosttype == "ipv4" then parsed.ipv4 = parsed.host
+ else parsed.ipv6 = parsed.host end
end
local userinfo = parsed.userinfo
if not userinfo then return parsed end
@@ -195,7 +220,11 @@ end
-- Rebuilds a parsed URL from its components.
-- Components are protected if any reserved or unallowed characters are found
-- Input
--- parsed: parsed URL, as returned by parse
+-- parsed: parsed URL, as returned by parse. If parsed.host is absent,
+-- the host is taken from whichever single one of hostname/ipv4/ipv6
+-- is set (parsed.hosttype is ignored for building). It is an error
+-- (raised with error()) for more than one of hostname/ipv4/ipv6 to
+-- be set when parsed.host is absent.
-- Returns
-- a stringing with the corresponding URL
-----------------------------------------------------------------------------
@@ -206,9 +235,21 @@ function _M.build(parsed)
if parsed.params then url = url .. ";" .. parsed.params end
if parsed.query then url = url .. "?" .. parsed.query end
local authority = parsed.authority
- if parsed.host then
- authority = parsed.host
- if string.find(authority, ":") then -- IPv6?
+ local host = parsed.host
+ if not host then
+ local set = 0
+ if parsed.hostname then host, set = parsed.hostname, set+1 end
+ if parsed.ipv4 then host, set = parsed.ipv4, set+1 end
+ if parsed.ipv6 then host, set = parsed.ipv6, set+1 end
+ if set > 1 then
+ base.error("url.build: ambiguous host, more than one of " ..
+ "hostname/ipv4/ipv6 is set")
+ end
+ end
+ if host then
+ local hosttype
+ hosttype, authority = _M.classify_host(host)
+ if hosttype == "ipv6" then
authority = "[" .. authority .. "]"
end
if parsed.port then authority = authority .. ":" .. base.tostring(parsed.port) end
diff --git a/test/urltest.lua b/test/urltest.lua
index 9a3c470..8c50f50 100644
--- a/test/urltest.lua
+++ b/test/urltest.lua
@@ -95,6 +95,8 @@ check_parse_url{
scheme = "scheme",
authority = "user:pass$%?#wd@host:port",
host = "host",
+ hosttype = "name",
+ hostname = "host",
port = "port",
userinfo = "user:pass$%?#wd",
password = "pass$%?#wd",
@@ -109,6 +111,8 @@ check_parse_url{
scheme = "scheme",
authority = "user:pass?#wd@host:port",
host = "host",
+ hosttype = "name",
+ hostname = "host",
port = "port",
userinfo = "user:pass?#wd",
password = "pass?#wd",
@@ -123,6 +127,8 @@ check_parse_url{
scheme = "scheme",
authority = "user:pass-wd@host:port",
host = "host",
+ hosttype = "name",
+ hostname = "host",
port = "port",
userinfo = "user:pass-wd",
password = "pass-wd",
@@ -137,6 +143,8 @@ check_parse_url{
scheme = "scheme",
authority = "user:pass#wd@host:port",
host = "host",
+ hosttype = "name",
+ hostname = "host",
port = "port",
userinfo = "user:pass#wd",
password = "pass#wd",
@@ -151,6 +159,8 @@ check_parse_url{
scheme = "scheme",
authority = "user:pass#wd@host:port",
host = "host",
+ hosttype = "name",
+ hostname = "host",
port = "port",
userinfo = "user:pass#wd",
password = "pass#wd",
@@ -164,6 +174,8 @@ check_parse_url{
scheme = "scheme",
authority = "userinfo@host:port",
host = "host",
+ hosttype = "name",
+ hostname = "host",
port = "port",
userinfo = "userinfo",
user = "userinfo",
@@ -178,6 +190,8 @@ check_parse_url{
scheme = "scheme",
authority = "user:password@host:port",
host = "host",
+ hosttype = "name",
+ hostname = "host",
port = "port",
userinfo = "user:password",
user = "user",
@@ -193,6 +207,8 @@ check_parse_url{
scheme = "scheme",
authority = "userinfo@host:port",
host = "host",
+ hosttype = "name",
+ hostname = "host",
port = "port",
userinfo = "userinfo",
user = "userinfo",
@@ -207,6 +223,8 @@ check_parse_url{
scheme = "scheme",
authority = "userinfo@host:port",
host = "host",
+ hosttype = "name",
+ hostname = "host",
port = "port",
userinfo = "userinfo",
user = "userinfo",
@@ -221,6 +239,8 @@ check_parse_url{
scheme = "scheme",
authority = "userinfo@host:port",
host = "host",
+ hosttype = "name",
+ hostname = "host",
port = "port",
userinfo = "userinfo",
user = "userinfo",
@@ -234,6 +254,8 @@ check_parse_url{
scheme = "scheme",
authority = "userinfo@host:port",
host = "host",
+ hosttype = "name",
+ hostname = "host",
port = "port",
userinfo = "userinfo",
user = "userinfo",
@@ -248,6 +270,8 @@ check_parse_url{
scheme = "scheme",
authority = "userinfo@host:port",
host = "host",
+ hosttype = "name",
+ hostname = "host",
port = "port",
userinfo = "userinfo",
user = "userinfo",
@@ -261,6 +285,8 @@ check_parse_url{
scheme = "scheme",
authority = "userinfo@host:port",
host = "host",
+ hosttype = "name",
+ hostname = "host",
port = "port",
userinfo = "userinfo",
user = "userinfo",
@@ -275,6 +301,8 @@ check_parse_url{
scheme = "scheme",
authority = "userinfo@host:port",
host = "host",
+ hosttype = "name",
+ hostname = "host",
port = "port",
userinfo = "userinfo",
user = "userinfo",
@@ -284,6 +312,8 @@ check_parse_url{
url = "//userinfo@host:port/path;params?query#fragment",
authority = "userinfo@host:port",
host = "host",
+ hosttype = "name",
+ hostname = "host",
port = "port",
userinfo = "userinfo",
user = "userinfo",
@@ -297,6 +327,8 @@ check_parse_url{
url = "//userinfo@host:port/path",
authority = "userinfo@host:port",
host = "host",
+ hosttype = "name",
+ hostname = "host",
port = "port",
userinfo = "userinfo",
user = "userinfo",
@@ -307,6 +339,8 @@ check_parse_url{
url = "//userinfo@host/path",
authority = "userinfo@host",
host = "host",
+ hosttype = "name",
+ hostname = "host",
userinfo = "userinfo",
user = "userinfo",
path = "/path",
@@ -316,6 +350,8 @@ check_parse_url{
url = "//user:password@host/path",
authority = "user:password@host",
host = "host",
+ hosttype = "name",
+ hostname = "host",
userinfo = "user:password",
password = "password",
user = "user",
@@ -326,6 +362,8 @@ check_parse_url{
url = "//user:@host/path",
authority = "user:@host",
host = "host",
+ hosttype = "name",
+ hostname = "host",
userinfo = "user:",
password = "",
user = "user",
@@ -336,6 +374,8 @@ check_parse_url{
url = "//user@host:port/path",
authority = "user@host:port",
host = "host",
+ hosttype = "name",
+ hostname = "host",
userinfo = "user",
user = "user",
port = "port",
@@ -347,6 +387,8 @@ check_parse_url{
authority = "host:port",
port = "port",
host = "host",
+ hosttype = "name",
+ hostname = "host",
path = "/path",
}
@@ -354,6 +396,8 @@ check_parse_url{
url = "//host/path",
authority = "host",
host = "host",
+ hosttype = "name",
+ hostname = "host",
path = "/path",
}
@@ -361,6 +405,8 @@ check_parse_url{
url = "//host",
authority = "host",
host = "host",
+ hosttype = "name",
+ hostname = "host",
}
check_parse_url{
@@ -379,6 +425,8 @@ check_parse_url{
url = "http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html",
scheme = "http",
host = "FEDC:BA98:7654:3210:FEDC:BA98:7654:3210",
+ hosttype = "ipv6",
+ ipv6 = "FEDC:BA98:7654:3210:FEDC:BA98:7654:3210",
authority = "[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80",
port = "80",
path = "/index.html"
@@ -388,6 +436,8 @@ check_parse_url{
url = "http://[1080:0:0:0:8:800:200C:417A]/index.html",
scheme = "http",
host = "1080:0:0:0:8:800:200C:417A",
+ hosttype = "ipv6",
+ ipv6 = "1080:0:0:0:8:800:200C:417A",
authority = "[1080:0:0:0:8:800:200C:417A]",
path = "/index.html"
}
@@ -396,6 +446,8 @@ check_parse_url{
url = "http://[3ffe:2a00:100:7031::1]",
scheme = "http",
host = "3ffe:2a00:100:7031::1",
+ hosttype = "ipv6",
+ ipv6 = "3ffe:2a00:100:7031::1",
authority = "[3ffe:2a00:100:7031::1]",
}
@@ -403,6 +455,8 @@ check_parse_url{
url = "http://[1080::8:800:200C:417A]/foo",
scheme = "http",
host = "1080::8:800:200C:417A",
+ hosttype = "ipv6",
+ ipv6 = "1080::8:800:200C:417A",
authority = "[1080::8:800:200C:417A]",
path = "/foo"
}
@@ -411,6 +465,8 @@ check_parse_url{
url = "http://[::192.9.5.5]/ipng",
scheme = "http",
host = "::192.9.5.5",
+ hosttype = "ipv6",
+ ipv6 = "::192.9.5.5",
authority = "[::192.9.5.5]",
path = "/ipng"
}
@@ -419,6 +475,8 @@ check_parse_url{
url = "http://[::FFFF:129.144.52.38]:80/index.html",
scheme = "http",
host = "::FFFF:129.144.52.38",
+ hosttype = "ipv6",
+ ipv6 = "::FFFF:129.144.52.38",
port = "80",
authority = "[::FFFF:129.144.52.38]:80",
path = "/index.html"
@@ -428,6 +486,8 @@ check_parse_url{
url = "http://[2010:836B:4179::836B:4179]",
scheme = "http",
host = "2010:836B:4179::836B:4179",
+ hosttype = "ipv6",
+ ipv6 = "2010:836B:4179::836B:4179",
authority = "[2010:836B:4179::836B:4179]",
}
@@ -435,6 +495,8 @@ check_parse_url{
url = "//userinfo@[::FFFF:129.144.52.38]:port/path;params?query#fragment",
authority = "userinfo@[::FFFF:129.144.52.38]:port",
host = "::FFFF:129.144.52.38",
+ hosttype = "ipv6",
+ ipv6 = "::FFFF:129.144.52.38",
port = "port",
userinfo = "userinfo",
user = "userinfo",
@@ -449,6 +511,8 @@ check_parse_url{
scheme = "scheme",
authority = "user:password@[::192.9.5.5]:port",
host = "::192.9.5.5",
+ hosttype = "ipv6",
+ ipv6 = "::192.9.5.5",
port = "port",
userinfo = "user:password",
user = "user",
@@ -459,6 +523,138 @@ check_parse_url{
fragment = "fragment"
}
+print("testing host classification (hosttype/hostname/ipv4/ipv6)")
+check_parse_url{
+ url = "http://example.com/path",
+ scheme = "http",
+ authority = "example.com",
+ host = "example.com",
+ hosttype = "name",
+ hostname = "example.com",
+ path = "/path",
+}
+
+check_parse_url{
+ url = "http://192.168.1.1:8080/path",
+ scheme = "http",
+ authority = "192.168.1.1:8080",
+ host = "192.168.1.1",
+ hosttype = "ipv4",
+ ipv4 = "192.168.1.1",
+ port = "8080",
+ path = "/path",
+}
+
+-- octet ranges aren't validated: any dotted-quad shape is classified ipv4,
+-- even with an out-of-range octet like this one
+check_parse_url{
+ url = "http://999.1.1.1/path",
+ scheme = "http",
+ authority = "999.1.1.1",
+ host = "999.1.1.1",
+ hosttype = "ipv4",
+ ipv4 = "999.1.1.1",
+ path = "/path",
+}
+
+check_parse_url{
+ url = "http://[::1]:8080/path",
+ scheme = "http",
+ authority = "[::1]:8080",
+ host = "::1",
+ hosttype = "ipv6",
+ ipv6 = "::1",
+ port = "8080",
+ path = "/path",
+}
+
+check_parse_url{
+ url = "http://[2010:836B:4179::836B:4179]/",
+ scheme = "http",
+ authority = "[2010:836B:4179::836B:4179]",
+ host = "2010:836B:4179::836B:4179",
+ hosttype = "ipv6",
+ ipv6 = "2010:836B:4179::836B:4179",
+ path = "/",
+}
+
+-- no authority at all: no host, no hosttype
+check_parse_url{
+ url = "relative/path",
+ path = "relative/path",
+}
+
+check_build_url{
+ url = "http://example.com/path",
+ scheme = "http",
+ hostname = "example.com",
+ path = "/path",
+}
+
+check_build_url{
+ url = "http://1.2.3.4/path",
+ scheme = "http",
+ ipv4 = "1.2.3.4",
+ path = "/path",
+}
+
+check_build_url{
+ url = "http://[::1]/path",
+ scheme = "http",
+ ipv6 = "::1",
+ path = "/path",
+}
+
+-- an already-bracketed ipv6/host value must not be bracketed again
+check_build_url{
+ url = "http://[::1]/path",
+ scheme = "http",
+ ipv6 = "[::1]",
+ path = "/path",
+}
+
+check_build_url{
+ url = "http://[::1]/path",
+ scheme = "http",
+ host = "[::1]",
+ path = "/path",
+}
+
+local check_classify_host = function(raw, expect_hosttype, expect_host)
+ local hosttype, host = socket.url.classify_host(raw)
+ if hosttype ~= expect_hosttype or host ~= expect_host then
+ io.write("classify_host: for '", raw, "' expected ", expect_hosttype,
+ " '", expect_host, "' but got ", tostring(hosttype), " '",
+ tostring(host), "'\n")
+ os.exit()
+ end
+end
+
+check_classify_host("example.com", "name", "example.com")
+check_classify_host("999.1.1.1", "ipv4", "999.1.1.1")
+check_classify_host("192.168.1.1", "ipv4", "192.168.1.1")
+check_classify_host("[::1]", "ipv6", "::1")
+check_classify_host("2010:836B:4179::836B:4179", "ipv6", "2010:836B:4179::836B:4179")
+
+-- legacy 'host' field takes precedence over hostname/ipv4/ipv6 if both given
+check_build_url{
+ url = "http://legacy.example/path",
+ scheme = "http",
+ host = "legacy.example",
+ hostname = "ignored.example",
+ path = "/path",
+}
+
+-- ambiguous: more than one of hostname/ipv4/ipv6 set, no 'host' to disambiguate
+do
+ local ok = pcall(socket.url.build,
+ {scheme = "http", ipv4 = "1.2.3.4", hostname = "example.com", path = "/path"})
+ if ok then
+ print("build: expected error for ambiguous host, got none")
+ os.exit()
+ end
+end
+
print("testing URL building")
check_build_url {
url = "scheme://user:password@host:port/path;params?query#fragment",
--
cgit v1.2.3-55-g6feb