1
1
Fork 0
mirror of https://github.com/NixOS/nix.git synced 2025-11-16 07:22:43 +01:00

Add user@address:port support

This patch allows users to specify the connection port
in the store URLS like so:

```
nix store info --store "ssh-ng://localhost:22" --json
```

Previously this failed with: `error: failed to start SSH connection to 'localhost:22'`,
because the code did not distinguish the port from the hostname. This
patch remedies that problem by introducing a ParsedURL::Authority type
for working with parsed authority components of URIs.

Now that the URL parsing code is less ad-hoc we can
add more long-awaited fixes for specifying SSH connection
ports in store URIs.

Builds upon the work from bd1d2d1041.

Co-authored-by: Sergei Zimmerman <sergei@zimmerman.foo>
Co-authored-by: John Ericson <John.Ericson@Obsidian.Systems>
This commit is contained in:
Maciej Krüger 2020-04-25 16:07:41 +02:00 committed by Sergei Zimmerman
parent c98af65da6
commit 49ba06175e
No known key found for this signature in database
18 changed files with 312 additions and 101 deletions

View file

@ -5,10 +5,76 @@
namespace nix {
/**
* Represents a parsed RFC3986 URL.
*
* @note All fields are already percent decoded.
*/
struct ParsedURL
{
/**
* Parsed representation of a URL authority.
*
* It consists of user information, hostname and an optional port number.
* Note that passwords in the userinfo are not yet supported and are ignored.
*
* @todo Maybe support passwords in userinfo part of the url for auth.
*/
struct Authority
{
enum class HostType {
Name, //< Registered name (can be empty)
IPv4,
IPv6,
IPvFuture
};
static Authority parse(std::string_view encodedAuthority);
bool operator==(const Authority & other) const = default;
std::string to_string() const;
friend std::ostream & operator<<(std::ostream & os, const Authority & self);
/**
* Type of the host subcomponent, as specified by rfc3986 3.2.2. Host.
*/
HostType hostType = HostType::Name;
/**
* Host subcomponent. Either a registered name or IPv{4,6,Future} literal addresses.
*
* IPv6 enclosing brackets are already stripped. Percent encoded characters
* in the hostname are decoded.
*/
std::string host;
/** Percent-decoded user part of the userinfo. */
std::optional<std::string> user;
/**
* Password subcomponent of the authority (if specified).
*
* @warning As per the rfc3986, the password syntax is deprecated,
* but it's necessary to make the parse -> to_string roundtrip.
* We don't use it anywhere (at least intentionally).
* @todo Warn about unused password subcomponent.
*/
std::optional<std::string> password;
/** Port subcomponent (if specified). Default value is determined by the scheme. */
std::optional<uint16_t> port;
};
std::string scheme;
std::optional<std::string> authority;
/**
* Optional parsed authority component of the URL.
*
* IMPORTANT: An empty authority (i.e. one with an empty host string) and
* a missing authority (std::nullopt) are drastically different cases. This
* is especially important for "file:///path/to/file" URLs defined by RFC8089.
* The presence of the authority is indicated by `//` following the <scheme>:
* part of the URL.
*/
std::optional<Authority> authority;
std::string path;
StringMap query;
std::string fragment;

View file

@ -34,6 +34,81 @@ static std::string percentEncodeSpaces(std::string_view url)
return replaceStrings(std::string(url), " ", percentEncode(" "));
}
ParsedURL::Authority ParsedURL::Authority::parse(std::string_view encodedAuthority)
{
auto parsed = boost::urls::parse_authority(encodedAuthority);
if (!parsed)
throw BadURL("invalid URL authority: '%s': %s", encodedAuthority, parsed.error().message());
auto hostType = [&]() {
switch (parsed->host_type()) {
case boost::urls::host_type::ipv4:
return HostType::IPv4;
case boost::urls::host_type::ipv6:
return HostType::IPv6;
case boost::urls::host_type::ipvfuture:
return HostType::IPvFuture;
case boost::urls::host_type::none:
case boost::urls::host_type::name:
return HostType::Name;
}
unreachable();
}();
auto port = [&]() -> std::optional<uint16_t> {
if (!parsed->has_port())
return std::nullopt;
/* If the port number is non-zero and representable. */
if (auto portNumber = parsed->port_number())
return portNumber;
throw BadURL("port '%s' is invalid", parsed->port());
}();
return {
.hostType = hostType,
.host = parsed->host_address(),
.user = parsed->has_userinfo() ? parsed->user() : std::optional<std::string>{},
.password = parsed->has_password() ? parsed->password() : std::optional<std::string>{},
.port = port,
};
}
std::ostream & operator<<(std::ostream & os, const ParsedURL::Authority & self)
{
if (self.user) {
os << percentEncode(*self.user);
if (self.password)
os << ":" << percentEncode(*self.password);
os << "@";
}
using HostType = ParsedURL::Authority::HostType;
switch (self.hostType) {
case HostType::Name:
os << percentEncode(self.host);
break;
case HostType::IPv4:
os << self.host;
break;
case HostType::IPv6:
case HostType::IPvFuture:
/* Reencode percent sign for RFC4007 ScopeId literals. */
os << "[" << percentEncode(self.host, ":") << "]";
}
if (self.port)
os << ":" << *self.port;
return os;
}
std::string ParsedURL::Authority::to_string() const
{
std::ostringstream oss;
oss << *this;
return std::move(oss).str();
}
ParsedURL parseURL(const std::string & url)
try {
/* Drop the shevron suffix used for the flakerefs. Shevron character is reserved and
@ -47,14 +122,21 @@ try {
throw BadURL("'%s' doesn't have a scheme", url);
auto scheme = urlView.scheme();
auto authority = [&]() -> std::optional<std::string> {
auto authority = [&]() -> std::optional<ParsedURL::Authority> {
if (urlView.has_authority())
return percentDecode(urlView.authority().buffer());
return ParsedURL::Authority::parse(urlView.authority().buffer());
return std::nullopt;
}();
/* 3.2.2. Host (RFC3986):
* If the URI scheme defines a default for host, then that default
* applies when the host subcomponent is undefined or when the
* registered name is empty (zero length). For example, the "file" URI
* scheme is defined so that no authority, an empty host, and
* "localhost" all mean the end-user's machine, whereas the "http"
* scheme considers a missing authority or empty host invalid. */
auto transportIsFile = parseUrlScheme(scheme).transport == "file";
if (authority && *authority != "" && transportIsFile)
if (authority && authority->host.size() && transportIsFile)
throw BadURL("file:// URL '%s' has unexpected authority '%s'", url, *authority);
auto path = urlView.path(); /* Does pct-decoding */
@ -135,7 +217,7 @@ std::string encodeQuery(const StringMap & ss)
std::string ParsedURL::to_string() const
{
return scheme + ":" + (authority ? "//" + *authority : "") + percentEncode(path, allowedInPath)
return scheme + ":" + (authority ? "//" + authority->to_string() : "") + percentEncode(path, allowedInPath)
+ (query.empty() ? "" : "?" + encodeQuery(query)) + (fragment.empty() ? "" : "#" + percentEncode(fragment));
}
@ -177,7 +259,7 @@ std::string fixGitURL(const std::string & url)
if (hasPrefix(url, "file:"))
return url;
if (url.find("://") == std::string::npos) {
return (ParsedURL{.scheme = "file", .authority = "", .path = url}).to_string();
return (ParsedURL{.scheme = "file", .authority = ParsedURL::Authority{}, .path = url}).to_string();
}
return url;
}