Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make BuildUrl an extension #2039

Merged
merged 10 commits into from
Mar 31, 2023
1 change: 1 addition & 0 deletions RestSharp.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeEditing/SuppressUninitializedWarningFix/Enabled/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeConstructorOrDestructorBody/@EntryIndexedValue">SUGGESTION</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeLocalFunctionBody/@EntryIndexedValue">SUGGESTION</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ArrangeMethodOrOperatorBody/@EntryIndexedValue">SUGGESTION</s:String>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AutoFixture" Version="4.17.0" />
<PackageReference Include="AutoFixture" Version="4.18.0" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
</ItemGroup>
<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion gen/SourceGenerator/SourceGenerator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="All"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" PrivateAssets="All"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" PrivateAssets="All"/>
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
<PackageReference Include="MinVer" Version="4.2.0" PrivateAssets="All"/>
<PackageReference Include="MinVer" Version="4.3.0" PrivateAssets="All"/>
<PackageReference Include="JetBrains.Annotations" Version="2022.3.1" PrivateAssets="All"/>
</ItemGroup>
<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/RestSharp/Authenticators/AuthenticatorBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ public abstract class AuthenticatorBase : IAuthenticator {

protected abstract ValueTask<Parameter> GetAuthenticationParameter(string accessToken);

public async ValueTask Authenticate(RestClient client, RestRequest request)
public async ValueTask Authenticate(IRestClient client, RestRequest request)
=> request.AddOrUpdateParameter(await GetAuthenticationParameter(Token).ConfigureAwait(false));
}
2 changes: 1 addition & 1 deletion src/RestSharp/Authenticators/IAuthenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@
namespace RestSharp.Authenticators;

public interface IAuthenticator {
ValueTask Authenticate(RestClient client, RestRequest request);
ValueTask Authenticate(IRestClient client, RestRequest request);
}
36 changes: 13 additions & 23 deletions src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using RestSharp.Extensions;
using System.Web;

// ReSharper disable NotResolvedInText
// ReSharper disable CheckNamespace

namespace RestSharp.Authenticators;
Expand All @@ -38,7 +39,7 @@ public class OAuth1Authenticator : IAuthenticator {
public virtual string? ClientUsername { get; set; }
public virtual string? ClientPassword { get; set; }

public ValueTask Authenticate(RestClient client, RestRequest request) {
public ValueTask Authenticate(IRestClient client, RestRequest request) {
var workflow = new OAuthWorkflow {
ConsumerKey = ConsumerKey,
ConsumerSecret = ConsumerSecret,
Expand All @@ -64,8 +65,8 @@ public static OAuth1Authenticator ForRequestToken(
string consumerKey,
string? consumerSecret,
OAuthSignatureMethod signatureMethod = OAuthSignatureMethod.HmacSha1
) {
var authenticator = new OAuth1Authenticator {
)
=> new() {
ParameterHandling = OAuthParameterHandling.HttpAuthorizationHeader,
SignatureMethod = signatureMethod,
SignatureTreatment = OAuthSignatureTreatment.Escaped,
Expand All @@ -74,10 +75,6 @@ public static OAuth1Authenticator ForRequestToken(
Type = OAuthType.RequestToken
};

return authenticator;
}

[PublicAPI]
public static OAuth1Authenticator ForRequestToken(string consumerKey, string? consumerSecret, string callbackUrl) {
var authenticator = ForRequestToken(consumerKey, consumerSecret);

Expand Down Expand Up @@ -105,7 +102,6 @@ public static OAuth1Authenticator ForAccessToken(
Type = OAuthType.AccessToken
};

[PublicAPI]
public static OAuth1Authenticator ForAccessToken(
string consumerKey,
string? consumerSecret,
Expand Down Expand Up @@ -171,7 +167,6 @@ public static OAuth1Authenticator ForClientAuthentication(
Type = OAuthType.ClientAuthentication
};

[PublicAPI]
public static OAuth1Authenticator ForProtectedResource(
string consumerKey,
string? consumerSecret,
Expand All @@ -190,7 +185,7 @@ public static OAuth1Authenticator ForProtectedResource(
TokenSecret = accessTokenSecret
};

void AddOAuthData(RestClient client, RestRequest request, OAuthWorkflow workflow) {
void AddOAuthData(IRestClient client, RestRequest request, OAuthWorkflow workflow) {
var requestUrl = client.BuildUriWithoutQueryParameters(request).AbsoluteUri;

if (requestUrl.Contains('?'))
Expand All @@ -201,11 +196,9 @@ void AddOAuthData(RestClient client, RestRequest request, OAuthWorkflow workflow
var url = client.BuildUri(request).ToString();
var queryStringStart = url.IndexOf('?');

if (queryStringStart != -1)
url = url.Substring(0, queryStringStart);

var method = request.Method.ToString().ToUpperInvariant();
if (queryStringStart != -1) url = url.Substring(0, queryStringStart);

var method = request.Method.ToString().ToUpperInvariant();
var parameters = new WebPairCollection();

// include all GET and POST parameters before generating the signature
Expand Down Expand Up @@ -247,21 +240,18 @@ void AddOAuthData(RestClient client, RestRequest request, OAuthWorkflow workflow

request.AddOrUpdateParameters(oauthParameters);

IEnumerable<Parameter> CreateHeaderParameters()
=> new[] { new HeaderParameter(KnownHeaders.Authorization, GetAuthorizationHeader()) };
IEnumerable<Parameter> CreateHeaderParameters() => new[] { new HeaderParameter(KnownHeaders.Authorization, GetAuthorizationHeader()) };

IEnumerable<Parameter> CreateUrlParameters()
=> oauth.Parameters.Select(p => new GetOrPostParameter(p.Name, HttpUtility.UrlDecode(p.Value)));
IEnumerable<Parameter> CreateUrlParameters() => oauth.Parameters.Select(p => new GetOrPostParameter(p.Name, HttpUtility.UrlDecode(p.Value)));

string GetAuthorizationHeader() {
var oathParameters =
oauth.Parameters
.OrderBy(x => x, WebPair.Comparer)
.Select(x => $"{x.Name}=\"{x.WebValue}\"")
.Select(x => x.GetQueryParameter(true))
.ToList();

if (!Realm.IsEmpty())
oathParameters.Insert(0, $"realm=\"{OAuthTools.UrlEncodeRelaxed(Realm!)}\"");
if (!Realm.IsEmpty()) oathParameters.Insert(0, $"realm=\"{OAuthTools.UrlEncodeRelaxed(Realm)}\"");

return $"OAuth {string.Join(",", oathParameters)}";
}
Expand All @@ -270,5 +260,5 @@ string GetAuthorizationHeader() {

static class ParametersExtensions {
internal static IEnumerable<WebPair> ToWebParameters(this IEnumerable<Parameter> p)
=> p.Select(x => new WebPair(Ensure.NotNull(x.Name, "Parameter name"), Ensure.NotNull(x.Value, "Parameter value").ToString()!));
}
=> p.Select(x => new WebPair(Ensure.NotNull(x.Name, "Parameter name"), x.Value?.ToString()));
}
19 changes: 12 additions & 7 deletions src/RestSharp/Authenticators/OAuth/OAuthTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System.Diagnostics.CodeAnalysis;
using System.Security.Cryptography;
using System.Text;
using RestSharp.Authenticators.OAuth.Extensions;
Expand Down Expand Up @@ -88,7 +89,10 @@ public static string GetNonce() {
/// actually worked (which in my experiments it <i>doesn't</i>), we can't rely on every
/// host actually having this configuration element present.
/// </remarks>
public static string UrlEncodeRelaxed(string value) {
[return: NotNullIfNotNull(nameof(value))]
public static string? UrlEncodeRelaxed(string? value) {
if (value == null) return null;

// Escape RFC 3986 chars first.
var escapedRfc3986 = new StringBuilder(value);

Expand Down Expand Up @@ -117,8 +121,9 @@ public static string UrlEncodeRelaxed(string value) {
// Generic Syntax," .) section 2.3) MUST be encoded.
// ...
// unreserved = ALPHA, DIGIT, '-', '.', '_', '~'
public static string UrlEncodeStrict(string value)
=> string.Join("", value.Select(x => Unreserved.Contains(x) ? x.ToString() : $"%{(byte)x:X2}"));
[return: NotNullIfNotNull(nameof(value))]
public static string? UrlEncodeStrict(string? value)
=> value == null ? null : string.Join("", value.Select(x => Unreserved.Contains(x) ? x.ToString() : $"%{(byte)x:X2}"));

/// <summary>
/// Sorts a collection of key-value pairs by name, and then value if equal,
Expand All @@ -137,9 +142,9 @@ public static string UrlEncodeStrict(string value)
public static IEnumerable<string> SortParametersExcludingSignature(WebPairCollection parameters)
=> parameters
.Where(x => !x.Name.EqualsIgnoreCase("oauth_signature"))
.Select(x => new WebPair(UrlEncodeStrict(x.Name), UrlEncodeStrict(x.Value), x.Encode))
.Select(x => new WebPair(UrlEncodeStrict(x.Name), UrlEncodeStrict(x.Value)))
.OrderBy(x => x, WebPair.Comparer)
.Select(x => $"{x.Name}={x.Value}");
.Select(x => x.GetQueryParameter(false));

/// <summary>
/// Creates a request URL suitable for making OAuth requests.
Expand All @@ -151,8 +156,8 @@ public static IEnumerable<string> SortParametersExcludingSignature(WebPairCollec
static string ConstructRequestUrl(Uri url) {
Ensure.NotNull(url, nameof(url));

var basic = url.Scheme == "http" && url.Port == 80;
var secure = url.Scheme == "https" && url.Port == 443;
var basic = url is { Scheme: "http", Port : 80 };
var secure = url is { Scheme: "https", Port: 443 };
var port = basic || secure ? "" : $":{url.Port}";

return $"{url.Scheme}://{url.Host}{port}{url.AbsolutePath}";
Expand Down
17 changes: 10 additions & 7 deletions src/RestSharp/Authenticators/OAuth/WebPair.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,20 @@
namespace RestSharp.Authenticators.OAuth;

class WebPair {
public WebPair(string name, string value, bool encode = false) {
public WebPair(string name, string? value, bool encode = false) {
Name = name;
Value = value;
WebValue = encode ? OAuthTools.UrlEncodeRelaxed(value) : value;
Encode = encode;
}

public string Name { get; }
public string Value { get; }
public string WebValue { get; }
public bool Encode { get; }
public string Name { get; }
public string? Value { get; }
string? WebValue { get; }

public string GetQueryParameter(bool web) {
var value = web ? $"\"{WebValue}\"" : Value;
return value == null ? Name : $"{Name}={value}";
}

internal static WebPairComparer Comparer { get; } = new();

Expand All @@ -36,4 +39,4 @@ public int Compare(WebPair? x, WebPair? y) {
return compareName != 0 ? compareName : string.CompareOrdinal(x?.Value, y?.Value);
}
}
}
}
93 changes: 93 additions & 0 deletions src/RestSharp/BuildUriExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) .NET Foundation and Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

namespace RestSharp;

public static class BuildUriExtensions {
/// <summary>
/// Builds the URI for the request
/// </summary>
/// <param name="client">Client instance</param>
/// <param name="request">Request instance</param>
/// <returns></returns>
public static Uri BuildUri(this IRestClient client, RestRequest request) {
DoBuildUriValidations(client, request);

var (uri, resource) = client.Options.BaseUrl.GetUrlSegmentParamsValues(
request.Resource,
client.Options.Encode,
request.Parameters,
client.DefaultParameters
);
var mergedUri = uri.MergeBaseUrlAndResource(resource);
var query = client.GetRequestQuery(request);
return mergedUri.AddQueryString(query);
}

/// <summary>
/// Builds the URI for the request without query parameters.
/// </summary>
/// <param name="client">Client instance</param>
/// <param name="request">Request instance</param>
/// <returns></returns>
public static Uri BuildUriWithoutQueryParameters(this IRestClient client, RestRequest request) {
DoBuildUriValidations(client, request);

var (uri, resource) = client.Options.BaseUrl.GetUrlSegmentParamsValues(
request.Resource,
client.Options.Encode,
request.Parameters,
client.DefaultParameters
);
return uri.MergeBaseUrlAndResource(resource);
}

/// <summary>
/// Gets the query string for the request.
/// </summary>
/// <param name="client">Client instance</param>
/// <param name="request">Request instance</param>
/// <returns></returns>
[PublicAPI]
public static string? GetRequestQuery(this IRestClient client, RestRequest request) {
var parametersCollections = new ParametersCollection[] { request.Parameters, client.DefaultParameters };

var parameters = parametersCollections.SelectMany(x => x.GetQueryParameters(request.Method)).ToList();

return parameters.Count == 0 ? null : string.Join("&", parameters.Select(EncodeParameter).ToArray());

string GetString(string name, string? value, Func<string, string>? encode) {
var val = encode != null && value != null ? encode(value) : value;
return val == null ? name : $"{name}={val}";
}

string EncodeParameter(Parameter parameter)
=> !parameter.Encode
? GetString(parameter.Name!, parameter.Value?.ToString(), null)
: GetString(
client.Options.EncodeQuery(parameter.Name!, client.Options.Encoding),
parameter.Value?.ToString(),
x => client.Options.EncodeQuery(x, client.Options.Encoding)
);
}

static void DoBuildUriValidations(IRestClient client, RestRequest request) {
if (client.Options.BaseUrl == null && !request.Resource.ToLowerInvariant().StartsWith("http"))
throw new ArgumentOutOfRangeException(
nameof(request),
"Request resource doesn't contain a valid scheme for an empty base URL of the client"
);
}
}
5 changes: 5 additions & 0 deletions src/RestSharp/IRestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public interface IRestClient : IDisposable {
/// </summary>
RestSerializers Serializers { get; }

/// <summary>
/// Default parameters to use on every request made with this client instance.
/// </summary>
DefaultParameters DefaultParameters { get; }

/// <summary>
/// Executes the request asynchronously, authenticating if needed
/// </summary>
Expand Down
Loading