aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThijs Schreijer <thijs@thijsschreijer.nl>2026-08-30 15:34:15 +0200
committerThijs Schreijer <thijs@thijsschreijer.nl>2026-08-30 15:34:15 +0200
commit6fe110be47c0fc34bc9dce6d377ce9b48ab27c2e (patch)
tree15eabb11745033d916e61c565ea4205bae84d0fe
parent84c2bb905bda5f1fc58195171c75723b5fec6e87 (diff)
downloadluasocket-6fe110be47c0fc34bc9dce6d377ce9b48ab27c2e.tar.gz
luasocket-6fe110be47c0fc34bc9dce6d377ce9b48ab27c2e.tar.bz2
luasocket-6fe110be47c0fc34bc9dce6d377ce9b48ab27c2e.zip
feat(url): classify host as name/ipv4/ipv6 in parse and build
parse() now sets hosttype ("name"/"ipv4"/"ipv6") plus a matching hostname/ipv4/ipv6 field alongside host. build() accepts those same fields when host is absent, erroring if more than one is set. classify_host is exported for standalone use; it classifies by shape (colon present -> ipv6, dotted-quad shape -> ipv4) rather than validating the address.
-rw-r--r--docs/url.html82
-rw-r--r--src/url.lua53
-rw-r--r--test/urltest.lua196
3 files changed, 325 insertions, 6 deletions
diff --git a/docs/url.html b/docs/url.html
index e03b094..788f409 100644
--- a/docs/url.html
+++ b/docs/url.html
@@ -140,6 +140,14 @@ Lower level components, if specified,
140take precedence over high level components of the URL grammar. 140take precedence over high level components of the URL grammar.
141</p> 141</p>
142 142
143<p class=parameters>
144If <tt>host</tt> is not set, the host is taken from whichever single
145one of <tt>hostname</tt>, <tt>ipv4</tt> or <tt>ipv6</tt> is set
146(<tt>hosttype</tt> is ignored when building). It is an error to set more
147than one of <tt>hostname</tt>, <tt>ipv4</tt> or <tt>ipv6</tt> while
148<tt>host</tt> is absent.
149</p>
150
143<p class=return> 151<p class=return>
144The function returns a string with the built URL. 152The function returns a string with the built URL.
145</p> 153</p>
@@ -169,6 +177,62 @@ The function returns a string with the
169built <tt>&lt;path&gt;</tt> component. 177built <tt>&lt;path&gt;</tt> component.
170</p> 178</p>
171 179
180<!-- classify_host ++++++++++++++++++++++++++++++++++++++++++++++++++++++ -->
181
182<p class=name id="classify_host">
183url.<b>classify_host(</b>host<b>)</b>
184</p>
185
186<p class=description>
187Classifies a raw <tt>&lt;host&gt;</tt> string into <tt>"name"</tt>,
188<tt>"ipv4"</tt> or <tt>"ipv6"</tt>. This is the same classification
189<a href="#parse"><tt>parse</tt></a> uses to fill in <tt>hosttype</tt>,
190exposed for callers that have a host string to classify without a full
191URL to parse.
192</p>
193
194<p class=parameters>
195<tt>Host</tt> is a host string as found in a URL's authority. Any
196<tt>:</tt> in <tt>host</tt> is taken as a sign of an IPv6 literal, so
197the enclosing <tt>[...]</tt> brackets URLs normally require for one are
198optional here: they are stripped when present, but classification does
199not depend on them.
200</p>
201
202<p class=return>
203The function returns two values: <tt>hosttype</tt>, one of
204<tt>"name"</tt>, <tt>"ipv4"</tt> or <tt>"ipv6"</tt>; and <tt>host</tt>,
205the input with any enclosing <tt>[...]</tt> brackets stripped, if
206present.
207</p>
208
209<p class=note>
210Note: classification is done by exclusion of shape, not by validating
211the address. A host is <tt>"ipv4"</tt> if it merely has the shape of
212four dot-separated digit groups &mdash; octet ranges are not checked,
213so <tt>"999.1.1.1"</tt> classifies as <tt>"ipv4"</tt> &mdash; and
214<tt>"ipv6"</tt> if it merely contains a <tt>:</tt>, whether or not that
215is a well-formed IPv6 address. Anything left over is <tt>"name"</tt>,
216whether or not it is actually a valid hostname.
217</p>
218
219<pre class=example>
220-- load url module
221url = require("socket.url")
222
223print(url.classify_host("example.com"))
224-- name example.com
225
226print(url.classify_host("192.168.1.1"))
227-- ipv4 192.168.1.1
228
229print(url.classify_host("[::1]"))
230-- ipv6 ::1
231
232print(url.classify_host("::1"))
233-- ipv6 ::1
234</pre>
235
172<!-- escape +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ --> 236<!-- escape +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -->
173 237
174<p class=name id="escape"> 238<p class=name id="escape">
@@ -230,12 +294,28 @@ parsed_url = {<br>
230&nbsp;&nbsp;fragment = <i>string</i>,<br> 294&nbsp;&nbsp;fragment = <i>string</i>,<br>
231&nbsp;&nbsp;userinfo = <i>string</i>,<br> 295&nbsp;&nbsp;userinfo = <i>string</i>,<br>
232&nbsp;&nbsp;host = <i>string</i>,<br> 296&nbsp;&nbsp;host = <i>string</i>,<br>
297&nbsp;&nbsp;hosttype = <i>string</i>,<br>
298&nbsp;&nbsp;hostname = <i>string</i>,<br>
299&nbsp;&nbsp;ipv4 = <i>string</i>,<br>
300&nbsp;&nbsp;ipv6 = <i>string</i>,<br>
233&nbsp;&nbsp;port = <i>string</i>,<br> 301&nbsp;&nbsp;port = <i>string</i>,<br>
234&nbsp;&nbsp;user = <i>string</i>,<br> 302&nbsp;&nbsp;user = <i>string</i>,<br>
235&nbsp;&nbsp;password = <i>string</i><br> 303&nbsp;&nbsp;password = <i>string</i><br>
236} 304}
237</tt></blockquote> 305</tt></blockquote>
238 306
307<p class=parameters>
308When a host is present, <tt>hosttype</tt> is set to one of
309<tt>"name"</tt>, <tt>"ipv4"</tt> or <tt>"ipv6"</tt>, describing the
310syntax of <tt>host</tt>. Exactly one of <tt>hostname</tt>, <tt>ipv4</tt>
311or <tt>ipv6</tt> is then also set to the same value as <tt>host</tt>,
312matching <tt>hosttype</tt> &mdash; the other two are left <b><tt>nil</tt></b>.
313In case of an ipv6 address the brackets are stripped, both in <tt>host</tt> and <tt>ipv6</tt>.
314This classification is done by
315<a href="#classify_host"><tt>classify_host</tt></a>, by exclusion of
316shape rather than by validating the address (see its notes).
317</p>
318
239<pre class=example> 319<pre class=example>
240-- load url module 320-- load url module
241url = require("socket.url") 321url = require("socket.url")
@@ -248,6 +328,8 @@ parsed_url = url.parse("http://www.example.com/cgilua/index.lua?a=2#there")
248-- query = "a=2", 328-- query = "a=2",
249-- fragment = "there", 329-- fragment = "there",
250-- host = "www.puc-rio.br", 330-- host = "www.puc-rio.br",
331-- hosttype = "name",
332-- hostname = "www.puc-rio.br",
251-- } 333-- }
252 334
253parsed_url = url.parse("ftp://root:passwd@unsafe.org/pub/virus.exe;type=i") 335parsed_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
@@ -123,6 +123,25 @@ local function absolute_path(base_path, relative_path)
123end 123end
124 124
125----------------------------------------------------------------------------- 125-----------------------------------------------------------------------------
126-- Classifies a raw host string into "name", "ipv4" or "ipv6"
127-- Input
128-- raw: host part of the authority, as found in the URL (with its
129-- enclosing [...] brackets, if any)
130-- Returns
131-- hosttype: "name", "ipv4" or "ipv6"
132-- host: raw, with any enclosing [...] brackets stripped
133-----------------------------------------------------------------------------
134function _M.classify_host(raw)
135 if string.find(raw, ":", 1, true) then
136 return "ipv6", string.match(raw, "^%[?(.-)%]?$")
137 end
138 if string.match(raw, "^%d+%.%d+%.%d+%.%d+$") then
139 return "ipv4", raw
140 end
141 return "name", raw
142end
143
144-----------------------------------------------------------------------------
126-- Parses a url and returns a table with all its parts according to RFC 2396 145-- Parses a url and returns a table with all its parts according to RFC 2396
127-- The following grammar describes the names given to the URL parts 146-- The following grammar describes the names given to the URL parts
128-- <url> ::= <scheme>://<authority>/<path>;<params>?<query>#<fragment> 147-- <url> ::= <scheme>://<authority>/<path>;<params>?<query>#<fragment>
@@ -137,6 +156,10 @@ end
137-- been preserved: 156-- been preserved:
138-- scheme, authority, userinfo, user, password, host, port, 157-- scheme, authority, userinfo, user, password, host, port,
139-- path, params, query, fragment 158-- path, params, query, fragment
159-- plus, when a host is present:
160-- hosttype: "name", "ipv4" or "ipv6", describing the syntax of host
161-- hostname, ipv4 or ipv6: same value as host, keyed by hosttype
162-- (only one of these three is ever set)
140-- Obs: 163-- Obs:
141-- the leading '/' in {/<path>} is considered part of <path> 164-- the leading '/' in {/<path>} is considered part of <path>
142----------------------------------------------------------------------------- 165-----------------------------------------------------------------------------
@@ -180,8 +203,10 @@ function _M.parse(url, default)
180 authority = string.gsub(authority, ":([^:%]]*)$", 203 authority = string.gsub(authority, ":([^:%]]*)$",
181 function(p) parsed.port = p; return "" end) 204 function(p) parsed.port = p; return "" end)
182 if authority ~= "" then 205 if authority ~= "" then
183 -- IPv6? 206 parsed.hosttype, parsed.host = _M.classify_host(authority)
184 parsed.host = string.match(authority, "^%[(.+)%]$") or authority 207 if parsed.hosttype == "name" then parsed.hostname = parsed.host
208 elseif parsed.hosttype == "ipv4" then parsed.ipv4 = parsed.host
209 else parsed.ipv6 = parsed.host end
185 end 210 end
186 local userinfo = parsed.userinfo 211 local userinfo = parsed.userinfo
187 if not userinfo then return parsed end 212 if not userinfo then return parsed end
@@ -195,7 +220,11 @@ end
195-- Rebuilds a parsed URL from its components. 220-- Rebuilds a parsed URL from its components.
196-- Components are protected if any reserved or unallowed characters are found 221-- Components are protected if any reserved or unallowed characters are found
197-- Input 222-- Input
198-- parsed: parsed URL, as returned by parse 223-- parsed: parsed URL, as returned by parse. If parsed.host is absent,
224-- the host is taken from whichever single one of hostname/ipv4/ipv6
225-- is set (parsed.hosttype is ignored for building). It is an error
226-- (raised with error()) for more than one of hostname/ipv4/ipv6 to
227-- be set when parsed.host is absent.
199-- Returns 228-- Returns
200-- a stringing with the corresponding URL 229-- a stringing with the corresponding URL
201----------------------------------------------------------------------------- 230-----------------------------------------------------------------------------
@@ -206,9 +235,21 @@ function _M.build(parsed)
206 if parsed.params then url = url .. ";" .. parsed.params end 235 if parsed.params then url = url .. ";" .. parsed.params end
207 if parsed.query then url = url .. "?" .. parsed.query end 236 if parsed.query then url = url .. "?" .. parsed.query end
208 local authority = parsed.authority 237 local authority = parsed.authority
209 if parsed.host then 238 local host = parsed.host
210 authority = parsed.host 239 if not host then
211 if string.find(authority, ":") then -- IPv6? 240 local set = 0
241 if parsed.hostname then host, set = parsed.hostname, set+1 end
242 if parsed.ipv4 then host, set = parsed.ipv4, set+1 end
243 if parsed.ipv6 then host, set = parsed.ipv6, set+1 end
244 if set > 1 then
245 base.error("url.build: ambiguous host, more than one of " ..
246 "hostname/ipv4/ipv6 is set")
247 end
248 end
249 if host then
250 local hosttype
251 hosttype, authority = _M.classify_host(host)
252 if hosttype == "ipv6" then
212 authority = "[" .. authority .. "]" 253 authority = "[" .. authority .. "]"
213 end 254 end
214 if parsed.port then authority = authority .. ":" .. base.tostring(parsed.port) end 255 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{
95 scheme = "scheme", 95 scheme = "scheme",
96 authority = "user:pass$%?#wd@host:port", 96 authority = "user:pass$%?#wd@host:port",
97 host = "host", 97 host = "host",
98 hosttype = "name",
99 hostname = "host",
98 port = "port", 100 port = "port",
99 userinfo = "user:pass$%?#wd", 101 userinfo = "user:pass$%?#wd",
100 password = "pass$%?#wd", 102 password = "pass$%?#wd",
@@ -109,6 +111,8 @@ check_parse_url{
109 scheme = "scheme", 111 scheme = "scheme",
110 authority = "user:pass?#wd@host:port", 112 authority = "user:pass?#wd@host:port",
111 host = "host", 113 host = "host",
114 hosttype = "name",
115 hostname = "host",
112 port = "port", 116 port = "port",
113 userinfo = "user:pass?#wd", 117 userinfo = "user:pass?#wd",
114 password = "pass?#wd", 118 password = "pass?#wd",
@@ -123,6 +127,8 @@ check_parse_url{
123 scheme = "scheme", 127 scheme = "scheme",
124 authority = "user:pass-wd@host:port", 128 authority = "user:pass-wd@host:port",
125 host = "host", 129 host = "host",
130 hosttype = "name",
131 hostname = "host",
126 port = "port", 132 port = "port",
127 userinfo = "user:pass-wd", 133 userinfo = "user:pass-wd",
128 password = "pass-wd", 134 password = "pass-wd",
@@ -137,6 +143,8 @@ check_parse_url{
137 scheme = "scheme", 143 scheme = "scheme",
138 authority = "user:pass#wd@host:port", 144 authority = "user:pass#wd@host:port",
139 host = "host", 145 host = "host",
146 hosttype = "name",
147 hostname = "host",
140 port = "port", 148 port = "port",
141 userinfo = "user:pass#wd", 149 userinfo = "user:pass#wd",
142 password = "pass#wd", 150 password = "pass#wd",
@@ -151,6 +159,8 @@ check_parse_url{
151 scheme = "scheme", 159 scheme = "scheme",
152 authority = "user:pass#wd@host:port", 160 authority = "user:pass#wd@host:port",
153 host = "host", 161 host = "host",
162 hosttype = "name",
163 hostname = "host",
154 port = "port", 164 port = "port",
155 userinfo = "user:pass#wd", 165 userinfo = "user:pass#wd",
156 password = "pass#wd", 166 password = "pass#wd",
@@ -164,6 +174,8 @@ check_parse_url{
164 scheme = "scheme", 174 scheme = "scheme",
165 authority = "userinfo@host:port", 175 authority = "userinfo@host:port",
166 host = "host", 176 host = "host",
177 hosttype = "name",
178 hostname = "host",
167 port = "port", 179 port = "port",
168 userinfo = "userinfo", 180 userinfo = "userinfo",
169 user = "userinfo", 181 user = "userinfo",
@@ -178,6 +190,8 @@ check_parse_url{
178 scheme = "scheme", 190 scheme = "scheme",
179 authority = "user:password@host:port", 191 authority = "user:password@host:port",
180 host = "host", 192 host = "host",
193 hosttype = "name",
194 hostname = "host",
181 port = "port", 195 port = "port",
182 userinfo = "user:password", 196 userinfo = "user:password",
183 user = "user", 197 user = "user",
@@ -193,6 +207,8 @@ check_parse_url{
193 scheme = "scheme", 207 scheme = "scheme",
194 authority = "userinfo@host:port", 208 authority = "userinfo@host:port",
195 host = "host", 209 host = "host",
210 hosttype = "name",
211 hostname = "host",
196 port = "port", 212 port = "port",
197 userinfo = "userinfo", 213 userinfo = "userinfo",
198 user = "userinfo", 214 user = "userinfo",
@@ -207,6 +223,8 @@ check_parse_url{
207 scheme = "scheme", 223 scheme = "scheme",
208 authority = "userinfo@host:port", 224 authority = "userinfo@host:port",
209 host = "host", 225 host = "host",
226 hosttype = "name",
227 hostname = "host",
210 port = "port", 228 port = "port",
211 userinfo = "userinfo", 229 userinfo = "userinfo",
212 user = "userinfo", 230 user = "userinfo",
@@ -221,6 +239,8 @@ check_parse_url{
221 scheme = "scheme", 239 scheme = "scheme",
222 authority = "userinfo@host:port", 240 authority = "userinfo@host:port",
223 host = "host", 241 host = "host",
242 hosttype = "name",
243 hostname = "host",
224 port = "port", 244 port = "port",
225 userinfo = "userinfo", 245 userinfo = "userinfo",
226 user = "userinfo", 246 user = "userinfo",
@@ -234,6 +254,8 @@ check_parse_url{
234 scheme = "scheme", 254 scheme = "scheme",
235 authority = "userinfo@host:port", 255 authority = "userinfo@host:port",
236 host = "host", 256 host = "host",
257 hosttype = "name",
258 hostname = "host",
237 port = "port", 259 port = "port",
238 userinfo = "userinfo", 260 userinfo = "userinfo",
239 user = "userinfo", 261 user = "userinfo",
@@ -248,6 +270,8 @@ check_parse_url{
248 scheme = "scheme", 270 scheme = "scheme",
249 authority = "userinfo@host:port", 271 authority = "userinfo@host:port",
250 host = "host", 272 host = "host",
273 hosttype = "name",
274 hostname = "host",
251 port = "port", 275 port = "port",
252 userinfo = "userinfo", 276 userinfo = "userinfo",
253 user = "userinfo", 277 user = "userinfo",
@@ -261,6 +285,8 @@ check_parse_url{
261 scheme = "scheme", 285 scheme = "scheme",
262 authority = "userinfo@host:port", 286 authority = "userinfo@host:port",
263 host = "host", 287 host = "host",
288 hosttype = "name",
289 hostname = "host",
264 port = "port", 290 port = "port",
265 userinfo = "userinfo", 291 userinfo = "userinfo",
266 user = "userinfo", 292 user = "userinfo",
@@ -275,6 +301,8 @@ check_parse_url{
275 scheme = "scheme", 301 scheme = "scheme",
276 authority = "userinfo@host:port", 302 authority = "userinfo@host:port",
277 host = "host", 303 host = "host",
304 hosttype = "name",
305 hostname = "host",
278 port = "port", 306 port = "port",
279 userinfo = "userinfo", 307 userinfo = "userinfo",
280 user = "userinfo", 308 user = "userinfo",
@@ -284,6 +312,8 @@ check_parse_url{
284 url = "//userinfo@host:port/path;params?query#fragment", 312 url = "//userinfo@host:port/path;params?query#fragment",
285 authority = "userinfo@host:port", 313 authority = "userinfo@host:port",
286 host = "host", 314 host = "host",
315 hosttype = "name",
316 hostname = "host",
287 port = "port", 317 port = "port",
288 userinfo = "userinfo", 318 userinfo = "userinfo",
289 user = "userinfo", 319 user = "userinfo",
@@ -297,6 +327,8 @@ check_parse_url{
297 url = "//userinfo@host:port/path", 327 url = "//userinfo@host:port/path",
298 authority = "userinfo@host:port", 328 authority = "userinfo@host:port",
299 host = "host", 329 host = "host",
330 hosttype = "name",
331 hostname = "host",
300 port = "port", 332 port = "port",
301 userinfo = "userinfo", 333 userinfo = "userinfo",
302 user = "userinfo", 334 user = "userinfo",
@@ -307,6 +339,8 @@ check_parse_url{
307 url = "//userinfo@host/path", 339 url = "//userinfo@host/path",
308 authority = "userinfo@host", 340 authority = "userinfo@host",
309 host = "host", 341 host = "host",
342 hosttype = "name",
343 hostname = "host",
310 userinfo = "userinfo", 344 userinfo = "userinfo",
311 user = "userinfo", 345 user = "userinfo",
312 path = "/path", 346 path = "/path",
@@ -316,6 +350,8 @@ check_parse_url{
316 url = "//user:password@host/path", 350 url = "//user:password@host/path",
317 authority = "user:password@host", 351 authority = "user:password@host",
318 host = "host", 352 host = "host",
353 hosttype = "name",
354 hostname = "host",
319 userinfo = "user:password", 355 userinfo = "user:password",
320 password = "password", 356 password = "password",
321 user = "user", 357 user = "user",
@@ -326,6 +362,8 @@ check_parse_url{
326 url = "//user:@host/path", 362 url = "//user:@host/path",
327 authority = "user:@host", 363 authority = "user:@host",
328 host = "host", 364 host = "host",
365 hosttype = "name",
366 hostname = "host",
329 userinfo = "user:", 367 userinfo = "user:",
330 password = "", 368 password = "",
331 user = "user", 369 user = "user",
@@ -336,6 +374,8 @@ check_parse_url{
336 url = "//user@host:port/path", 374 url = "//user@host:port/path",
337 authority = "user@host:port", 375 authority = "user@host:port",
338 host = "host", 376 host = "host",
377 hosttype = "name",
378 hostname = "host",
339 userinfo = "user", 379 userinfo = "user",
340 user = "user", 380 user = "user",
341 port = "port", 381 port = "port",
@@ -347,6 +387,8 @@ check_parse_url{
347 authority = "host:port", 387 authority = "host:port",
348 port = "port", 388 port = "port",
349 host = "host", 389 host = "host",
390 hosttype = "name",
391 hostname = "host",
350 path = "/path", 392 path = "/path",
351} 393}
352 394
@@ -354,6 +396,8 @@ check_parse_url{
354 url = "//host/path", 396 url = "//host/path",
355 authority = "host", 397 authority = "host",
356 host = "host", 398 host = "host",
399 hosttype = "name",
400 hostname = "host",
357 path = "/path", 401 path = "/path",
358} 402}
359 403
@@ -361,6 +405,8 @@ check_parse_url{
361 url = "//host", 405 url = "//host",
362 authority = "host", 406 authority = "host",
363 host = "host", 407 host = "host",
408 hosttype = "name",
409 hostname = "host",
364} 410}
365 411
366check_parse_url{ 412check_parse_url{
@@ -379,6 +425,8 @@ check_parse_url{
379 url = "http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html", 425 url = "http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html",
380 scheme = "http", 426 scheme = "http",
381 host = "FEDC:BA98:7654:3210:FEDC:BA98:7654:3210", 427 host = "FEDC:BA98:7654:3210:FEDC:BA98:7654:3210",
428 hosttype = "ipv6",
429 ipv6 = "FEDC:BA98:7654:3210:FEDC:BA98:7654:3210",
382 authority = "[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80", 430 authority = "[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80",
383 port = "80", 431 port = "80",
384 path = "/index.html" 432 path = "/index.html"
@@ -388,6 +436,8 @@ check_parse_url{
388 url = "http://[1080:0:0:0:8:800:200C:417A]/index.html", 436 url = "http://[1080:0:0:0:8:800:200C:417A]/index.html",
389 scheme = "http", 437 scheme = "http",
390 host = "1080:0:0:0:8:800:200C:417A", 438 host = "1080:0:0:0:8:800:200C:417A",
439 hosttype = "ipv6",
440 ipv6 = "1080:0:0:0:8:800:200C:417A",
391 authority = "[1080:0:0:0:8:800:200C:417A]", 441 authority = "[1080:0:0:0:8:800:200C:417A]",
392 path = "/index.html" 442 path = "/index.html"
393} 443}
@@ -396,6 +446,8 @@ check_parse_url{
396 url = "http://[3ffe:2a00:100:7031::1]", 446 url = "http://[3ffe:2a00:100:7031::1]",
397 scheme = "http", 447 scheme = "http",
398 host = "3ffe:2a00:100:7031::1", 448 host = "3ffe:2a00:100:7031::1",
449 hosttype = "ipv6",
450 ipv6 = "3ffe:2a00:100:7031::1",
399 authority = "[3ffe:2a00:100:7031::1]", 451 authority = "[3ffe:2a00:100:7031::1]",
400} 452}
401 453
@@ -403,6 +455,8 @@ check_parse_url{
403 url = "http://[1080::8:800:200C:417A]/foo", 455 url = "http://[1080::8:800:200C:417A]/foo",
404 scheme = "http", 456 scheme = "http",
405 host = "1080::8:800:200C:417A", 457 host = "1080::8:800:200C:417A",
458 hosttype = "ipv6",
459 ipv6 = "1080::8:800:200C:417A",
406 authority = "[1080::8:800:200C:417A]", 460 authority = "[1080::8:800:200C:417A]",
407 path = "/foo" 461 path = "/foo"
408} 462}
@@ -411,6 +465,8 @@ check_parse_url{
411 url = "http://[::192.9.5.5]/ipng", 465 url = "http://[::192.9.5.5]/ipng",
412 scheme = "http", 466 scheme = "http",
413 host = "::192.9.5.5", 467 host = "::192.9.5.5",
468 hosttype = "ipv6",
469 ipv6 = "::192.9.5.5",
414 authority = "[::192.9.5.5]", 470 authority = "[::192.9.5.5]",
415 path = "/ipng" 471 path = "/ipng"
416} 472}
@@ -419,6 +475,8 @@ check_parse_url{
419 url = "http://[::FFFF:129.144.52.38]:80/index.html", 475 url = "http://[::FFFF:129.144.52.38]:80/index.html",
420 scheme = "http", 476 scheme = "http",
421 host = "::FFFF:129.144.52.38", 477 host = "::FFFF:129.144.52.38",
478 hosttype = "ipv6",
479 ipv6 = "::FFFF:129.144.52.38",
422 port = "80", 480 port = "80",
423 authority = "[::FFFF:129.144.52.38]:80", 481 authority = "[::FFFF:129.144.52.38]:80",
424 path = "/index.html" 482 path = "/index.html"
@@ -428,6 +486,8 @@ check_parse_url{
428 url = "http://[2010:836B:4179::836B:4179]", 486 url = "http://[2010:836B:4179::836B:4179]",
429 scheme = "http", 487 scheme = "http",
430 host = "2010:836B:4179::836B:4179", 488 host = "2010:836B:4179::836B:4179",
489 hosttype = "ipv6",
490 ipv6 = "2010:836B:4179::836B:4179",
431 authority = "[2010:836B:4179::836B:4179]", 491 authority = "[2010:836B:4179::836B:4179]",
432} 492}
433 493
@@ -435,6 +495,8 @@ check_parse_url{
435 url = "//userinfo@[::FFFF:129.144.52.38]:port/path;params?query#fragment", 495 url = "//userinfo@[::FFFF:129.144.52.38]:port/path;params?query#fragment",
436 authority = "userinfo@[::FFFF:129.144.52.38]:port", 496 authority = "userinfo@[::FFFF:129.144.52.38]:port",
437 host = "::FFFF:129.144.52.38", 497 host = "::FFFF:129.144.52.38",
498 hosttype = "ipv6",
499 ipv6 = "::FFFF:129.144.52.38",
438 port = "port", 500 port = "port",
439 userinfo = "userinfo", 501 userinfo = "userinfo",
440 user = "userinfo", 502 user = "userinfo",
@@ -449,6 +511,8 @@ check_parse_url{
449 scheme = "scheme", 511 scheme = "scheme",
450 authority = "user:password@[::192.9.5.5]:port", 512 authority = "user:password@[::192.9.5.5]:port",
451 host = "::192.9.5.5", 513 host = "::192.9.5.5",
514 hosttype = "ipv6",
515 ipv6 = "::192.9.5.5",
452 port = "port", 516 port = "port",
453 userinfo = "user:password", 517 userinfo = "user:password",
454 user = "user", 518 user = "user",
@@ -459,6 +523,138 @@ check_parse_url{
459 fragment = "fragment" 523 fragment = "fragment"
460} 524}
461 525
526print("testing host classification (hosttype/hostname/ipv4/ipv6)")
527check_parse_url{
528 url = "http://example.com/path",
529 scheme = "http",
530 authority = "example.com",
531 host = "example.com",
532 hosttype = "name",
533 hostname = "example.com",
534 path = "/path",
535}
536
537check_parse_url{
538 url = "http://192.168.1.1:8080/path",
539 scheme = "http",
540 authority = "192.168.1.1:8080",
541 host = "192.168.1.1",
542 hosttype = "ipv4",
543 ipv4 = "192.168.1.1",
544 port = "8080",
545 path = "/path",
546}
547
548-- octet ranges aren't validated: any dotted-quad shape is classified ipv4,
549-- even with an out-of-range octet like this one
550check_parse_url{
551 url = "http://999.1.1.1/path",
552 scheme = "http",
553 authority = "999.1.1.1",
554 host = "999.1.1.1",
555 hosttype = "ipv4",
556 ipv4 = "999.1.1.1",
557 path = "/path",
558}
559
560check_parse_url{
561 url = "http://[::1]:8080/path",
562 scheme = "http",
563 authority = "[::1]:8080",
564 host = "::1",
565 hosttype = "ipv6",
566 ipv6 = "::1",
567 port = "8080",
568 path = "/path",
569}
570
571check_parse_url{
572 url = "http://[2010:836B:4179::836B:4179]/",
573 scheme = "http",
574 authority = "[2010:836B:4179::836B:4179]",
575 host = "2010:836B:4179::836B:4179",
576 hosttype = "ipv6",
577 ipv6 = "2010:836B:4179::836B:4179",
578 path = "/",
579}
580
581-- no authority at all: no host, no hosttype
582check_parse_url{
583 url = "relative/path",
584 path = "relative/path",
585}
586
587check_build_url{
588 url = "http://example.com/path",
589 scheme = "http",
590 hostname = "example.com",
591 path = "/path",
592}
593
594check_build_url{
595 url = "http://1.2.3.4/path",
596 scheme = "http",
597 ipv4 = "1.2.3.4",
598 path = "/path",
599}
600
601check_build_url{
602 url = "http://[::1]/path",
603 scheme = "http",
604 ipv6 = "::1",
605 path = "/path",
606}
607
608-- an already-bracketed ipv6/host value must not be bracketed again
609check_build_url{
610 url = "http://[::1]/path",
611 scheme = "http",
612 ipv6 = "[::1]",
613 path = "/path",
614}
615
616check_build_url{
617 url = "http://[::1]/path",
618 scheme = "http",
619 host = "[::1]",
620 path = "/path",
621}
622
623local check_classify_host = function(raw, expect_hosttype, expect_host)
624 local hosttype, host = socket.url.classify_host(raw)
625 if hosttype ~= expect_hosttype or host ~= expect_host then
626 io.write("classify_host: for '", raw, "' expected ", expect_hosttype,
627 " '", expect_host, "' but got ", tostring(hosttype), " '",
628 tostring(host), "'\n")
629 os.exit()
630 end
631end
632
633check_classify_host("example.com", "name", "example.com")
634check_classify_host("999.1.1.1", "ipv4", "999.1.1.1")
635check_classify_host("192.168.1.1", "ipv4", "192.168.1.1")
636check_classify_host("[::1]", "ipv6", "::1")
637check_classify_host("2010:836B:4179::836B:4179", "ipv6", "2010:836B:4179::836B:4179")
638
639-- legacy 'host' field takes precedence over hostname/ipv4/ipv6 if both given
640check_build_url{
641 url = "http://legacy.example/path",
642 scheme = "http",
643 host = "legacy.example",
644 hostname = "ignored.example",
645 path = "/path",
646}
647
648-- ambiguous: more than one of hostname/ipv4/ipv6 set, no 'host' to disambiguate
649do
650 local ok = pcall(socket.url.build,
651 {scheme = "http", ipv4 = "1.2.3.4", hostname = "example.com", path = "/path"})
652 if ok then
653 print("build: expected error for ambiguous host, got none")
654 os.exit()
655 end
656end
657
462print("testing URL building") 658print("testing URL building")
463check_build_url { 659check_build_url {
464 url = "scheme://user:password@host:port/path;params?query#fragment", 660 url = "scheme://user:password@host:port/path;params?query#fragment",