Skip to content

v3.0.0 - Internationalized hosts can be checked, and strings became objects

Choose a tag to compare

@spaze spaze released this 12 Sep 01:17
· 84 commits to main since this release
v3.0.0
afee043

An internationalized host can be checked at all now. DNS does not resolve háčkyčárky.cz, it resolves xn--hkyrky-ptac70bc.cz, the same name written in ASCII: each part of it is an A-label, the xn-- spelling, and the readable one is a U-label. dns_get_record() was being handed the U-labels, so the bundled DNS provider never resolved an internationalized host and never had.

The rest of the release is why it is a major one. Values that were strings are now the things they describe: a host is a SecurityTxtHost, a URL is a Uri\WhatWg\Url, an address family is an enum case, a redirect chain is a SecurityTxtRedirects. And a host has one name wherever it is read, instead of one spelling in a message and another in a cache key.

Fetching and validation behave as they did, apart from the fixes below. What changed is the shape of nearly everything the library hands back.

A host has one name

  • Look a host up, and talk to it, by the name those understand (#133)
  • Let a host have one name (#141)
  • Spell a host one way, everywhere but the network (#144)
  • Settle the URL once, and take the host from the one that gets fetched (#140)
  • Decide once what a check is about (#145)

The A-labels now go to DNS, the Host: header and CURLOPT_RESOLVE, and nowhere else; everywhere a person reads a host, it is the U-labels.

Everything else reads the host as it was written, and reads it the same way everywhere, because the host is derived once from the URL that actually gets fetched rather than copied out of it at each call site. Not every A-label survives being turned back into a U-label, and where one does not, the whole host stays in A-labels rather than mixing the two: xn--khby.example decodes to a name that re-encodes as xn--jgb.example, a different host, so decoding it would quietly name somewhere nobody asked about.

A value arrives as the thing it is

  • Return the host and the URLs from a check as objects, not strings (#128)
  • Hand the callbacks a parsed URL and host, not a string (#127)
  • Hand back the IP address type as the case it is (#134)
  • Return the IP address type from getIpAddresses() as the enum, not its value (#114)
  • Let the last two exceptions that name a host hold one (#136)
  • Turn a host back into one when an exception is rebuilt from JSON (#135)
  • Let the invalid address exception say which family it checked (#137)
  • One type per parameter (#142)
  • Let the wire carry the call and nothing else (#143)
  • Hand a violation the URL or the host, not a string of one (#146)
  • Read a stored host where the other stored values are read (#147)

A value the library holds as a URL or a host is passed on as that object rather than flattened first, so nothing downstream has to guess what a string was. SecurityTxtRedirects replaces the bare arrays a redirect chain used to travel in.

Fixes

  • Compare a canonical URI as a URI, not as a string (#131)
  • Report an http file location instead of throwing out of the parser (#132)
  • Record the code a host answered with, not a hardcoded 200 (#138)
  • Name the IP address and the curl error when a URL cannot be opened (#115)

A Canonical field was compared byte for byte, so HTTPS://example.com/… was reported as not matching the URL it was fetched from. An http:// file location threw out of the parser with nothing catching it, which ended the process; that one is also in 2.2.2.

Output

  • Print messages from a format and values, encode the values (#116)
  • Print a URL as it reads when it is one (#117)
  • Print a violation's values as they read, and drop the punycode alongside them (#130)

getMessage() encodes server-supplied values down to printable ASCII itself now, so it is safe to print wherever, not only through ConsolePrinter. A URL or a host is the exception: it exists because it parsed, so it is quoted as it reads.

JSON

  • Say which shape a stored result was written in (#139)
  • Make the reason of a common mistake an enum, unforgeable through JSON (#129)

Stored results carry a formatVersion, and decoding refuses one written by a newer version by name instead of failing on whichever field moved. A result stored by 2.x carries no such key and is still read.

Internal changes

  • End 1.x support (#126)
  • Run the release hosts as a test (#148)
  • Put @testCase where Tester reads it (#149)
  • Update the README for 3.x (#152)

Upgrading

SecurityTxtCheckHost takes a fourth constructor argument, a SecurityTxtUrlParser. That is the first thing you will hit.

Getters hand back objects. SecurityTxtCheckHostResult::getHost() returns a SecurityTxtHost, getConstructedUrl() and getFinalUrl() return a Uri\WhatWg\Url, and so do the same two on SecurityTxtFetchResult and SecurityTxtFetcherFetchHostResult. SecurityTxtFetcherException::getUrl() returns ?Url and getRedirects() a SecurityTxtRedirects. SecurityTxtSpecViolation::getCorrectValue() returns string|Url|null. SecurityTxtUrlNotFoundException::getIpAddressType() returns the enum case.

Callbacks are handed objects too, which PHP will not tell you about until one runs: addOnUrl() and addOnFinalUrl() get a Url, addOnRedirect() two of them, addOnHost() a SecurityTxtHost.

SecurityTxtHost is new here, and you build one from a parsed URL: new SecurityTxtHost($url). Neither it nor Url has a string form of its own, so do not pass one to implode() or htmlspecialchars(). Render it with new SecurityTxtPrintableValue($value)->render(), which takes all three kinds and spells a host the way the rest of the library does.

If you implement SecurityTxtDnsProvider or SecurityTxtFetcherHttpClient yourself, both now take a SecurityTxtHost where they took a string, and use getAscii() for anything that goes on the wire. SecurityTxtIpAddressValidator::validate() takes a SecurityTxtHost and a Url.

ConsolePrinter::ok(), info(), error() and warning() take a format and its values rather than a finished string, enableColors() is setColors(bool), and the color*() helpers are gone.

Results stored as JSON by 2.x still decode here, and a 2.x decoder still reads most of what 3.x writes, since the values on the wire are the same strings. One violation breaks both directions: SecurityTxtPreferredLanguagesCommonMistake took five constructor arguments and now takes four, its reason having become an enum, so a result carrying it is refused by the other side either way. If a fetching side and a reading side deploy separately, upgrade the reader first, or treat what is cached as a miss and check again.