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:
parent
c98af65da6
commit
49ba06175e
18 changed files with 312 additions and 101 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue