|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Text;
|
|
|
|
using System.Text.RegularExpressions;
|
|
|
|
using NzbDrone.Common.Extensions;
|
|
|
|
|
|
|
|
namespace NzbDrone.Common.Http
|
|
|
|
{
|
|
|
|
public class HttpUri : IEquatable<HttpUri>
|
|
|
|
{
|
|
|
|
private static readonly Regex RegexUri = new Regex(@"^(?:(?<scheme>[a-z]+):)?(?://(?<host>[-_A-Z0-9.]+|\[[[A-F0-9:]+\])(?::(?<port>[0-9]{1,5}))?)?(?<path>(?:(?:(?<=^)|/+)[^/?#\r\n]+)+/*|/+)?(?:\?(?<query>[^#\r\n]*))?(?:\#(?<fragment>.*))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
|
|
|
|
|
|
|
private readonly string _uri;
|
|
|
|
public string FullUri => _uri;
|
|
|
|
|
|
|
|
public HttpUri(string uri)
|
|
|
|
{
|
|
|
|
_uri = uri ?? string.Empty;
|
|
|
|
|
|
|
|
Parse();
|
|
|
|
}
|
|
|
|
|
|
|
|
public HttpUri(string scheme, string host, int? port, string path, string query, string fragment)
|
|
|
|
{
|
|
|
|
var builder = new StringBuilder();
|
|
|
|
|
|
|
|
if (scheme.IsNotNullOrWhiteSpace())
|
|
|
|
{
|
|
|
|
builder.Append(scheme);
|
|
|
|
builder.Append(':');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (host.IsNotNullOrWhiteSpace())
|
|
|
|
{
|
|
|
|
builder.Append("//");
|
|
|
|
builder.Append(host);
|
|
|
|
if (port.HasValue)
|
|
|
|
{
|
|
|
|
builder.Append(':');
|
|
|
|
builder.Append(port);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (path.IsNotNullOrWhiteSpace())
|
|
|
|
{
|
|
|
|
if (host.IsNotNullOrWhiteSpace() || path.StartsWith("/"))
|
|
|
|
{
|
|
|
|
builder.Append('/');
|
|
|
|
}
|
|
|
|
|
|
|
|
builder.Append(path.TrimStart('/'));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (query.IsNotNullOrWhiteSpace())
|
|
|
|
{
|
|
|
|
builder.Append('?');
|
|
|
|
builder.Append(query);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (fragment.IsNotNullOrWhiteSpace())
|
|
|
|
{
|
|
|
|
builder.Append('#');
|
|
|
|
builder.Append(fragment);
|
|
|
|
}
|
|
|
|
|
|
|
|
_uri = builder.ToString();
|
|
|
|
|
|
|
|
Parse();
|
|
|
|
}
|
|
|
|
|
|
|
|
private void Parse()
|
|
|
|
{
|
|
|
|
var parseSuccess = Uri.TryCreate(_uri, UriKind.RelativeOrAbsolute, out var uri);
|
|
|
|
|
|
|
|
var match = RegexUri.Match(_uri);
|
|
|
|
|
|
|
|
var scheme = match.Groups["scheme"];
|
|
|
|
var host = match.Groups["host"];
|
|
|
|
var port = match.Groups["port"];
|
|
|
|
var path = match.Groups["path"];
|
|
|
|
var query = match.Groups["query"];
|
|
|
|
var fragment = match.Groups["fragment"];
|
|
|
|
|
|
|
|
if (!parseSuccess || (scheme.Success && !host.Success && path.Success))
|
|
|
|
{
|
|
|
|
throw new ArgumentException("Uri didn't match expected pattern: " + _uri);
|
|
|
|
}
|
|
|
|
|
|
|
|
Scheme = scheme.Value;
|
|
|
|
Host = host.Value;
|
|
|
|
Port = port.Success ? (int?)int.Parse(port.Value) : null;
|
|
|
|
Path = path.Value;
|
|
|
|
Query = query.Value;
|
|
|
|
Fragment = fragment.Value;
|
|
|
|
}
|
|
|
|
|
|
|
|
public string Scheme { get; private set; }
|
|
|
|
public string Host { get; private set; }
|
|
|
|
public int? Port { get; private set; }
|
|
|
|
public string Path { get; private set; }
|
|
|
|
public string Query { get; private set; }
|
|
|
|
public string Fragment { get; private set; }
|
|
|
|
|
|
|
|
private IList<KeyValuePair<string, string>> _queryParams;
|
|
|
|
private IList<KeyValuePair<string, string>> QueryParams
|
|
|
|
{
|
|
|
|
get
|
|
|
|
{
|
|
|
|
if (_queryParams == null)
|
|
|
|
{
|
|
|
|
var dict = new List<KeyValuePair<string, string>>();
|
|
|
|
|
|
|
|
if (Query.IsNotNullOrWhiteSpace())
|
|
|
|
{
|
|
|
|
foreach (var pair in Query.Split('&'))
|
|
|
|
{
|
|
|
|
var split = pair.Split(new[] { '=' }, 2);
|
|
|
|
|
|
|
|
if (split.Length == 1)
|
|
|
|
{
|
|
|
|
dict.Add(new KeyValuePair<string, string>(Uri.UnescapeDataString(split[0]), null));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
dict.Add(new KeyValuePair<string, string>(Uri.UnescapeDataString(split[0]), Uri.UnescapeDataString(split[1])));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
_queryParams = dict.AsReadOnly();
|
|
|
|
}
|
|
|
|
|
|
|
|
return _queryParams;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public HttpUri CombinePath(string path)
|
|
|
|
{
|
|
|
|
return new HttpUri(Scheme, Host, Port, CombinePath(Path, path), Query, Fragment);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static string CombinePath(string basePath, string relativePath)
|
|
|
|
{
|
|
|
|
if (relativePath.IsNullOrWhiteSpace())
|
|
|
|
{
|
|
|
|
return basePath;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (basePath.IsNullOrWhiteSpace())
|
|
|
|
{
|
|
|
|
return relativePath;
|
|
|
|
}
|
|
|
|
|
|
|
|
return basePath.TrimEnd('/') + "/" + relativePath.TrimStart('/');
|
|
|
|
}
|
|
|
|
|
|
|
|
private static string CombineRelativePath(string basePath, string relativePath)
|
|
|
|
{
|
|
|
|
if (relativePath.IsNullOrWhiteSpace())
|
|
|
|
{
|
|
|
|
return basePath;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (relativePath.StartsWith("/"))
|
|
|
|
{
|
|
|
|
return relativePath;
|
|
|
|
}
|
|
|
|
|
|
|
|
var baseSlashIndex = basePath.LastIndexOf('/');
|
|
|
|
|
|
|
|
if (baseSlashIndex >= 0)
|
|
|
|
{
|
|
|
|
return $"{basePath.AsSpan(0, baseSlashIndex)}/{relativePath}";
|
|
|
|
}
|
|
|
|
|
|
|
|
return relativePath;
|
|
|
|
}
|
|
|
|
|
|
|
|
public HttpUri SetQuery(string query)
|
|
|
|
{
|
|
|
|
return new HttpUri(Scheme, Host, Port, Path, query, Fragment);
|
|
|
|
}
|
|
|
|
|
|
|
|
public HttpUri AddQueryParam(string key, object value)
|
|
|
|
{
|
|
|
|
var newQuery = string.Concat(Uri.EscapeDataString(key), "=", Uri.EscapeDataString(value.ToString()));
|
|
|
|
|
|
|
|
if (Query.IsNotNullOrWhiteSpace())
|
|
|
|
{
|
|
|
|
newQuery = string.Concat(Query, "&", newQuery);
|
|
|
|
}
|
|
|
|
|
|
|
|
return SetQuery(newQuery);
|
|
|
|
}
|
|
|
|
|
|
|
|
public HttpUri AddQueryParams(IEnumerable<KeyValuePair<string, string>> queryParams)
|
|
|
|
{
|
|
|
|
var builder = new StringBuilder();
|
|
|
|
builder.Append(Query);
|
|
|
|
|
|
|
|
foreach (var pair in queryParams)
|
|
|
|
{
|
|
|
|
if (builder.Length != 0)
|
|
|
|
{
|
|
|
|
builder.Append('&');
|
|
|
|
}
|
|
|
|
|
|
|
|
builder.Append(Uri.EscapeDataString(pair.Key));
|
|
|
|
builder.Append('=');
|
|
|
|
builder.Append(Uri.EscapeDataString(pair.Value));
|
|
|
|
}
|
|
|
|
|
|
|
|
return SetQuery(builder.ToString());
|
|
|
|
}
|
|
|
|
|
|
|
|
public override int GetHashCode()
|
|
|
|
{
|
|
|
|
return _uri.GetHashCode();
|
|
|
|
}
|
|
|
|
|
|
|
|
public override string ToString()
|
|
|
|
{
|
|
|
|
return _uri;
|
|
|
|
}
|
|
|
|
|
|
|
|
public override bool Equals(object obj)
|
|
|
|
{
|
|
|
|
if (obj is string)
|
|
|
|
{
|
|
|
|
return _uri.Equals((string)obj);
|
|
|
|
}
|
|
|
|
else if (obj is Uri)
|
|
|
|
{
|
|
|
|
return _uri.Equals(((Uri)obj).OriginalString);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
return Equals(obj as HttpUri);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public bool Equals(HttpUri other)
|
|
|
|
{
|
|
|
|
if (object.ReferenceEquals(other, null))
|
|
|
|
{
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return _uri.Equals(other._uri);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static explicit operator Uri(HttpUri url)
|
|
|
|
{
|
|
|
|
return new Uri(url.FullUri);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static HttpUri operator +(HttpUri baseUrl, HttpUri relativeUrl)
|
|
|
|
{
|
|
|
|
if (relativeUrl.Scheme.IsNotNullOrWhiteSpace())
|
|
|
|
{
|
|
|
|
return relativeUrl;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (relativeUrl.Host.IsNotNullOrWhiteSpace())
|
|
|
|
{
|
|
|
|
return new HttpUri(baseUrl.Scheme, relativeUrl.Host, relativeUrl.Port, relativeUrl.Path, relativeUrl.Query, relativeUrl.Fragment);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (relativeUrl.Path.IsNotNullOrWhiteSpace())
|
|
|
|
{
|
|
|
|
return new HttpUri(baseUrl.Scheme, baseUrl.Host, baseUrl.Port, CombineRelativePath(baseUrl.Path, relativeUrl.Path), relativeUrl.Query, relativeUrl.Fragment);
|
|
|
|
}
|
|
|
|
|
|
|
|
return new HttpUri(baseUrl.Scheme, baseUrl.Host, baseUrl.Port, baseUrl.Path, relativeUrl.Query, relativeUrl.Fragment);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|