Hostname validation for HTTP Host headers (RFC 1123)

Files
README.md

hostname #

Hostname validation for HTTP Host headers (RFC 1123).

A web server that routes on the Host header, or that checks a request is for one of its own subdomains, first has to decide whether the header names a host at all. RFC 1035 lets a DNS label hold almost any byte, so a DNS name library will accept values that HTTP should refuse. HTTP Host headers and cookies (RFC 6265) follow the stricter rules of RFC 1123 section 2.1, and so does this library: a label starts and ends with a letter or a digit, and a host is ASCII. The library also strips the port and accepts IPv4 and bracketed IPv6 literals. validate_subdomain checks that a host is a given base domain or exactly one label below it.

Installation #

Install with opam:

$ opam install hostname

If opam cannot find the package, it may not yet be released in the public opam-repository. Add the overlay repository, then install it:

$ opam repo add samoht https://tangled.org/gazagnaire.org/opam-overlay.git
$ opam update
$ opam install hostname

Usage #

(* Validate a hostname. *)
let () =
  match Hostname.validate "example.com" with
  | Ok host -> assert (host = "example.com")
  | Error _ -> failwith "expected valid"

(* Quick boolean check. *)
let () = assert (Hostname.is_valid "example.com")
let () = assert (not (Hostname.is_valid "exam ple"))

(* Strip port from Host header. *)
let () = assert (Hostname.strip_port "example.com:8080" = "example.com")

(* Validate subdomain against a base domain. *)
let () =
  match Hostname.validate_subdomain ~base:"example.com" "api.example.com" with
  | Ok () -> ()
  | Error _ -> failwith "expected valid subdomain"

API #

  • Hostname.validate checks a Host header value and returns the host without its port.
  • Hostname.validate_subdomain checks that a host is the base domain or one label below it.
  • Hostname.is_valid is validate as a Boolean.
  • Hostname.strip_port removes a :port suffix.

Command-line arguments #

The hostname.cli library holds Hostname_cli.host, the cmdliner converter for a host name argument. It parses the argument into the [ host ] Domain_name.t` the TLS, X.509 and ACME libraries take, and refuses what RFC 1123 does not call a host name, and the root:

let names =
  Cmdliner.Arg.(
    non_empty
    & pos_all Hostname_cli.host []
    & info [] ~docv:"NAME" ~doc:"A domain name to certify.")

let () =
  match Cmdliner.Arg.Conv.parser Hostname_cli.host "a_b.example.org" with
  | Error _ -> ()
  | Ok _ -> failwith "expected a refusal"

Edge cases #

Unicode / IDNA #

Non-ASCII input is rejected, which keeps look-alike characters out of a Host header (a Cyrillic 'а' in place of a Latin 'a', say). The library does not do any IDNA or punycode conversion, so to accept an international domain name, punycode-encode it before you call validate:

"xn--nxasmq6b.example"   (* ASCII-safe, accepted *)
"exâmple.com"            (* Rejected: non-ASCII *)

ocaml-punycode does the conversion from Unicode to ASCII.

Ports and IPv6 #

For a DNS name or an IPv4 address, strip_port removes a trailing :port. For a bracketed IPv6 host it removes the port and leaves the brackets on. validate strips the port the same way, then also drops the brackets from an IPv6 address:

Input strip_port validate
example.com example.com Ok "example.com"
example.com:8080 example.com Ok "example.com"
192.168.1.1:443 192.168.1.1 Ok "192.168.1.1"
[::1]:8080 [::1] Ok "::1"
::1:8080 ::1:8080 ambiguous: bracket IPv6 hosts

Put brackets around IPv6 literals in Host headers. Without them, ::1:8080 could be a host and a port, or an IPv6 address whose last group is 8080, and nothing can tell the two apart.

License #

ISC. See LICENSE.md.