Skip to content

Commit

Permalink
Make BuildUrl an extension (#2039)
Browse files Browse the repository at this point in the history
* Make BuildUrl an extension, add DefaultParameters to the interface, change the type to guard against concurrent updates.

* Null value allowed for OAuth1Authenticator
Serializers cache (fixes #1988)

* Updated dependencies, added System.Text.Json to all targets (fixes #2033)

* Update package version

* Add sealed to Parameter.ToString() and also exposed GetRequestQuery extension (fixes #2035)

* Quote OAuth parameter value

* Fix the boundary quotation, was using the wrong option

* One more place missed

* Fix the accepted header null value
  • Loading branch information
alexeyzimarev committed Mar 31, 2023
1 parent d184961 commit 159c8a7
Show file tree
Hide file tree
Showing 42 changed files with 381 additions and 254 deletions.
1 change: 1 addition & 0 deletions RestSharp.sln.DotSettings
@@ -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
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
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
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
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
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
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 class OAuth1Authenticator : IAuthenticator {
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 class OAuth1Authenticator : IAuthenticator {
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 class OAuth1Authenticator : IAuthenticator {
Type = OAuthType.AccessToken
};

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

[PublicAPI]
public static OAuth1Authenticator ForProtectedResource(
string consumerKey,
string? consumerSecret,
Expand All @@ -190,7 +185,7 @@ string sessionHandle
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 @@ string sessionHandle
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 @@ string sessionHandle

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 @@ IEnumerable<Parameter> CreateUrlParameters()

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
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 @@ static class OAuthTools {
/// 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 @@ static class OAuthTools {
// 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
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 @@ internal class WebPairComparer : IComparer<WebPair> {
return compareName != 0 ? compareName : string.CompareOrdinal(x?.Value, y?.Value);
}
}
}
}
93 changes: 93 additions & 0 deletions src/RestSharp/BuildUriExtensions.cs
@@ -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
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

0 comments on commit 159c8a7

Please sign in to comment.