diff --git a/src/RestSharp/Authenticators/AuthenticatorBase.cs b/src/RestSharp/Authenticators/AuthenticatorBase.cs index f0847c1a7..6e3ae7db3 100644 --- a/src/RestSharp/Authenticators/AuthenticatorBase.cs +++ b/src/RestSharp/Authenticators/AuthenticatorBase.cs @@ -17,7 +17,7 @@ namespace RestSharp.Authenticators; public abstract class AuthenticatorBase : IAuthenticator { protected AuthenticatorBase(string token) => Token = token; - protected string Token { get; } + protected string Token { get; set; } protected abstract ValueTask GetAuthenticationParameter(string accessToken); diff --git a/src/RestSharp/Authenticators/HttpBasicAuthenticator.cs b/src/RestSharp/Authenticators/HttpBasicAuthenticator.cs index 964acbba6..f6724cee3 100644 --- a/src/RestSharp/Authenticators/HttpBasicAuthenticator.cs +++ b/src/RestSharp/Authenticators/HttpBasicAuthenticator.cs @@ -35,5 +35,5 @@ static string GetHeader(string username, string password, Encoding encoding) // return ; protected override ValueTask GetAuthenticationParameter(string accessToken) - => new(new Parameter("Authorization", $"Basic {accessToken}", ParameterType.HttpHeader)); + => new(new Parameter(KnownHeaders.Authorization, $"Basic {accessToken}", ParameterType.HttpHeader, false)); } \ No newline at end of file diff --git a/src/RestSharp/Authenticators/JwtAuthenticator.cs b/src/RestSharp/Authenticators/JwtAuthenticator.cs index 815973d9d..4295ba159 100644 --- a/src/RestSharp/Authenticators/JwtAuthenticator.cs +++ b/src/RestSharp/Authenticators/JwtAuthenticator.cs @@ -18,21 +18,18 @@ namespace RestSharp.Authenticators; /// JSON WEB TOKEN (JWT) Authenticator class. /// https://tools.ietf.org/html/draft-ietf-oauth-json-web-token /// -public class JwtAuthenticator : IAuthenticator { - string _authHeader = null!; - - // ReSharper disable once IntroduceOptionalParameters.Global - public JwtAuthenticator(string accessToken) => SetBearerToken(accessToken); +public class JwtAuthenticator : AuthenticatorBase { + public JwtAuthenticator(string accessToken) : base(GetToken(accessToken)) { } /// /// Set the new bearer token so the request gets the new header value /// /// [PublicAPI] - public void SetBearerToken(string accessToken) => _authHeader = $"Bearer {Ensure.NotEmpty(accessToken, nameof(accessToken))}"; + public void SetBearerToken(string accessToken) => Token = GetToken(accessToken); + + static string GetToken(string accessToken) => $"Bearer {Ensure.NotEmpty(accessToken, nameof(accessToken))}"; - public ValueTask Authenticate(RestClient client, RestRequest request) { - request.AddOrUpdateParameter("Authorization", _authHeader, ParameterType.HttpHeader); - return default; - } + protected override ValueTask GetAuthenticationParameter(string accessToken) + => new(new Parameter(KnownHeaders.Authorization, accessToken, ParameterType.HttpHeader, false)); } \ No newline at end of file diff --git a/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs b/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs index 8a6706912..777088262 100644 --- a/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs +++ b/src/RestSharp/Authenticators/OAuth/OAuth1Authenticator.cs @@ -40,9 +40,9 @@ public class OAuth1Authenticator : IAuthenticator { public ValueTask Authenticate(RestClient client, RestRequest request) { var workflow = new OAuthWorkflow { - ConsumerKey = ConsumerKey, - ConsumerSecret = ConsumerSecret, - // ParameterHandling = ParameterHandling, + ConsumerKey = ConsumerKey, + ConsumerSecret = ConsumerSecret, + ParameterHandling = ParameterHandling, SignatureMethod = SignatureMethod, SignatureTreatment = SignatureTreatment, Verifier = Verifier, @@ -191,7 +191,7 @@ public static OAuth1Authenticator ForProtectedResource( }; void AddOAuthData(RestClient client, RestRequest request, OAuthWorkflow workflow) { - var requestUrl = client.BuildUriWithoutQueryParameters(request); + var requestUrl = client.BuildUriWithoutQueryParameters(request).AbsoluteUri; if (requestUrl.Contains('?')) throw new ApplicationException( @@ -233,7 +233,7 @@ void AddOAuthData(RestClient client, RestRequest request, OAuthWorkflow workflow OAuthType.AccessToken => workflow.BuildAccessTokenSignature(method, parameters), OAuthType.ClientAuthentication => workflow.BuildClientAuthAccessTokenSignature(method, parameters), OAuthType.ProtectedResource => workflow.BuildProtectedResourceSignature(method, parameters, url), - _ => throw new ArgumentOutOfRangeException() + _ => throw new ArgumentOutOfRangeException(nameof(Type)) }; oauth.Parameters.Add("oauth_signature", oauth.Signature); @@ -242,13 +242,13 @@ void AddOAuthData(RestClient client, RestRequest request, OAuthWorkflow workflow OAuthParameterHandling.HttpAuthorizationHeader => CreateHeaderParameters(), OAuthParameterHandling.UrlOrPostParameters => CreateUrlParameters(), _ => - throw new ArgumentOutOfRangeException() + throw new ArgumentOutOfRangeException(nameof(ParameterHandling)) }; request.AddOrUpdateParameters(oauthParameters); IEnumerable CreateHeaderParameters() - => new[] { new Parameter("Authorization", GetAuthorizationHeader(), ParameterType.HttpHeader) }; + => new[] { new Parameter(KnownHeaders.Authorization, GetAuthorizationHeader(), ParameterType.HttpHeader) }; IEnumerable CreateUrlParameters() => oauth.Parameters.Select(p => new Parameter(p.Name, HttpUtility.UrlDecode(p.Value), ParameterType.GetOrPost)); diff --git a/src/RestSharp/Authenticators/OAuth/OAuthWorkflow.cs b/src/RestSharp/Authenticators/OAuth/OAuthWorkflow.cs index 97e61e839..f17cb64dc 100644 --- a/src/RestSharp/Authenticators/OAuth/OAuthWorkflow.cs +++ b/src/RestSharp/Authenticators/OAuth/OAuthWorkflow.cs @@ -14,7 +14,6 @@ using System.Web; using RestSharp.Authenticators.OAuth.Extensions; -using RestSharp.Extensions; namespace RestSharp.Authenticators.OAuth; @@ -32,11 +31,11 @@ sealed class OAuthWorkflow { public string? SessionHandle { get; set; } public OAuthSignatureMethod SignatureMethod { get; set; } public OAuthSignatureTreatment SignatureTreatment { get; set; } - // public OAuthParameterHandling ParameterHandling { get; set; } - public string? ClientUsername { get; set; } - public string? ClientPassword { get; set; } - public string? RequestTokenUrl { get; set; } - public string? AccessTokenUrl { get; set; } + public OAuthParameterHandling ParameterHandling { get; set; } + public string? ClientUsername { get; set; } + public string? ClientPassword { get; set; } + public string? RequestTokenUrl { get; set; } + public string? AccessTokenUrl { get; set; } /// /// Generates an OAuth signature to pass to an @@ -169,32 +168,24 @@ void ValidateProtectedResourceState() { Ensure.NotEmpty(ConsumerKey, nameof(ConsumerKey)); } - WebPairCollection GenerateAuthParameters(string timestamp, string nonce) { - var authParameters = new WebPairCollection { - new("oauth_consumer_key", Ensure.NotNull(ConsumerKey, nameof(ConsumerKey))), - new("oauth_nonce", nonce), - new("oauth_signature_method", SignatureMethod.ToRequestValue()), - new("oauth_timestamp", timestamp), - new("oauth_version", Version ?? "1.0") - }; - - if (!Token.IsEmpty()) authParameters.Add(new WebPair("oauth_token", Token!, true)); - - if (!CallbackUrl.IsEmpty()) authParameters.Add(new WebPair("oauth_callback", CallbackUrl!, true)); - - if (!Verifier.IsEmpty()) authParameters.Add(new WebPair("oauth_verifier", Verifier!)); - - if (!SessionHandle.IsEmpty()) authParameters.Add(new WebPair("oauth_session_handle", SessionHandle!)); - - return authParameters; - } + WebPairCollection GenerateAuthParameters(string timestamp, string nonce) + => new WebPairCollection { + new("oauth_consumer_key", Ensure.NotNull(ConsumerKey, nameof(ConsumerKey)), true), + new("oauth_nonce", nonce), + new("oauth_signature_method", SignatureMethod.ToRequestValue()), + new("oauth_timestamp", timestamp), + new("oauth_version", Version ?? "1.0") + }.AddNotEmpty("oauth_token", Token!, true) + .AddNotEmpty("oauth_callback", CallbackUrl!, true) + .AddNotEmpty("oauth_verifier", Verifier!) + .AddNotEmpty("oauth_session_handle", SessionHandle!); WebPairCollection GenerateXAuthParameters(string timestamp, string nonce) - => new() { + => new WebPairCollection { new("x_auth_username", Ensure.NotNull(ClientUsername, nameof(ClientUsername))), new("x_auth_password", Ensure.NotNull(ClientPassword, nameof(ClientPassword))), new("x_auth_mode", "client_auth"), - new("oauth_consumer_key", Ensure.NotNull(ConsumerKey, nameof(ConsumerKey))), + new("oauth_consumer_key", Ensure.NotNull(ConsumerKey, nameof(ConsumerKey)), true), new("oauth_signature_method", SignatureMethod.ToRequestValue()), new("oauth_timestamp", timestamp), new("oauth_nonce", nonce), diff --git a/src/RestSharp/Authenticators/OAuth/WebPairCollection.cs b/src/RestSharp/Authenticators/OAuth/WebPairCollection.cs index 398774efc..c91d00336 100644 --- a/src/RestSharp/Authenticators/OAuth/WebPairCollection.cs +++ b/src/RestSharp/Authenticators/OAuth/WebPairCollection.cs @@ -28,6 +28,12 @@ class WebPairCollection : IList { public void AddRange(IEnumerable collection) => AddCollection(collection); public void Add(string name, string value) => Add(new WebPair(name, value)); + + public WebPairCollection AddNotEmpty(string name, string? value, bool encode = false) { + if (value != null) + Add(new WebPair(name, value, encode)); + return this; + } public void Clear() => _parameters.Clear(); diff --git a/src/RestSharp/Authenticators/OAuth2/OAuth2AuthorizationRequestHeaderAuthenticator.cs b/src/RestSharp/Authenticators/OAuth2/OAuth2AuthorizationRequestHeaderAuthenticator.cs index d05c3269a..af843292c 100644 --- a/src/RestSharp/Authenticators/OAuth2/OAuth2AuthorizationRequestHeaderAuthenticator.cs +++ b/src/RestSharp/Authenticators/OAuth2/OAuth2AuthorizationRequestHeaderAuthenticator.cs @@ -38,5 +38,5 @@ public OAuth2AuthorizationRequestHeaderAuthenticator(string accessToken) public OAuth2AuthorizationRequestHeaderAuthenticator(string accessToken, string tokenType) : base(accessToken) => _tokenType = tokenType; protected override ValueTask GetAuthenticationParameter(string accessToken) - => new(new Parameter("Authorization", $"{_tokenType} {accessToken}", ParameterType.HttpHeader)); + => new(new Parameter(KnownHeaders.Authorization, $"{_tokenType} {accessToken}", ParameterType.HttpHeader)); } \ No newline at end of file diff --git a/src/RestSharp/KnownHeaders.cs b/src/RestSharp/KnownHeaders.cs index 7aa6de8e2..82c765d04 100644 --- a/src/RestSharp/KnownHeaders.cs +++ b/src/RestSharp/KnownHeaders.cs @@ -18,6 +18,7 @@ namespace RestSharp; public static class KnownHeaders { + public const string Authorization = "Authorization"; public const string Accept = "Accept"; public const string Allow = "Allow"; public const string Expires = "Expires"; diff --git a/src/RestSharp/Parameters/Parameter.cs b/src/RestSharp/Parameters/Parameter.cs index 9de5c8941..82276d0a8 100644 --- a/src/RestSharp/Parameters/Parameter.cs +++ b/src/RestSharp/Parameters/Parameter.cs @@ -66,7 +66,6 @@ public Parameter(string name, object value, string contentType, ParameterType ty /// /// String public override string ToString() => $"{Name}={Value}"; - } public record XmlParameter : Parameter { diff --git a/src/RestSharp/Parameters/ParametersCollection.cs b/src/RestSharp/Parameters/ParametersCollection.cs new file mode 100644 index 000000000..d39fafa2b --- /dev/null +++ b/src/RestSharp/Parameters/ParametersCollection.cs @@ -0,0 +1,72 @@ +// Copyright © 2009-2020 John Sheehan, Andrew Young, Alexey Zimarev and RestSharp community +// +// 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. +// + +using System.Collections; +using System.Collections.Concurrent; +using RestSharp.Authenticators.OAuth.Extensions; + +namespace RestSharp; + +public class ParametersCollection : IReadOnlyCollection { + readonly List _parameters = new(); + + public ParametersCollection() { } + + public ParametersCollection(IEnumerable parameters) => _parameters.AddRange(parameters); + + public ParametersCollection AddParameters(IEnumerable parameters) { + _parameters.AddRange(parameters); + return this; + } + + public ParametersCollection AddParameters(ParametersCollection parameters) { + _parameters.AddRange(parameters); + return this; + } + + public void AddParameter(Parameter parameter) => _parameters.Add(parameter); + + public void RemoveParameter(string name) => _parameters.RemoveAll(x => x.Name == name); + + public void RemoveParameter(Parameter parameter) => _parameters.Remove(parameter); + + public bool Exists(Parameter parameter) + => _parameters.Any( + p => p.Name != null && p.Name.Equals(parameter.Name, StringComparison.InvariantCultureIgnoreCase) && p.Type == parameter.Type + ); + + public Parameter? TryFind(string parameterName) => _parameters.FirstOrDefault(x => x.Name != null && x.Name.EqualsIgnoreCase(parameterName)); + + internal ParametersCollection GetParameters(ParameterType parameterType) => new(_parameters.Where(x => x.Type == parameterType)); + + internal ParametersCollection GetQueryParameters(Method method) + => new( + method is not Method.Post and not Method.Put and not Method.Patch + ? _parameters + .Where( + p => p.Type is ParameterType.GetOrPost or ParameterType.QueryString + ) + : _parameters + .Where( + p => p.Type is ParameterType.QueryString + ) + ); + + public IEnumerator GetEnumerator() => _parameters.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public int Count => _parameters.Count; +} \ No newline at end of file diff --git a/src/RestSharp/Request/HttpRequestMessageExtensions.cs b/src/RestSharp/Request/HttpRequestMessageExtensions.cs index 43d9b6b95..d35f5c533 100644 --- a/src/RestSharp/Request/HttpRequestMessageExtensions.cs +++ b/src/RestSharp/Request/HttpRequestMessageExtensions.cs @@ -13,19 +13,24 @@ // limitations under the License. // +using System.Text; using RestSharp.Extensions; -namespace RestSharp; +namespace RestSharp; static class HttpRequestMessageExtensions { - public static void AddHeaders(this HttpRequestMessage message, IEnumerable parameters) { + public static void AddHeaders(this HttpRequestMessage message, ParametersCollection parameters, Func encode) { var headerParameters = parameters - .Where(x => x.Type == ParameterType.HttpHeader && !RequestContent.ContentHeaders.Contains(x.Name)); + .GetParameters(ParameterType.HttpHeader) + .Where(x => !RequestContent.ContentHeaders.Contains(x.Name)); headerParameters.ForEach(AddHeader); - + void AddHeader(Parameter parameter) { var parameterStringValue = parameter.Value!.ToString(); + + if (parameter.Encode) parameterStringValue = encode(parameterStringValue!); + message.Headers.Remove(parameter.Name!); message.Headers.TryAddWithoutValidation(parameter.Name!, parameterStringValue); } diff --git a/src/RestSharp/Request/RequestContent.cs b/src/RestSharp/Request/RequestContent.cs index 4b13fe982..361168497 100644 --- a/src/RestSharp/Request/RequestContent.cs +++ b/src/RestSharp/Request/RequestContent.cs @@ -136,7 +136,7 @@ void AddPostParameters() { var formContent = new FormUrlEncodedContent( _request.Parameters .Where(x => x.Type == ParameterType.GetOrPost) - .Select(x => new KeyValuePair(x.Name!, x.Value!.ToString()!)) + .Select(x => new KeyValuePair(x.Name!, x.Value!.ToString()!))! ); Content = formContent; } diff --git a/src/RestSharp/Request/RequestParameters.cs b/src/RestSharp/Request/RequestParameters.cs index 629d0f746..50505c5c4 100644 --- a/src/RestSharp/Request/RequestParameters.cs +++ b/src/RestSharp/Request/RequestParameters.cs @@ -13,49 +13,36 @@ // limitations under the License. // -using RestSharp.Authenticators.OAuth.Extensions; - namespace RestSharp; class RequestParameters { static readonly ParameterType[] MultiParameterTypes = { ParameterType.QueryString, ParameterType.GetOrPost }; - - readonly List _requestParameters = new(); - public IReadOnlyCollection Parameters => _requestParameters.AsReadOnly(); + public ParametersCollection Parameters { get; } = new(); - public RequestParameters AddRequestParameters(RestRequest request) { - _requestParameters.AddRange(request.Parameters); + public RequestParameters AddParameters(ParametersCollection parameters, bool allowSameName) { + Parameters.AddParameters(GetParameters(parameters, allowSameName)); return this; } - // move RestClient.DefaultParameters into Request.Parameters - public RequestParameters AddDefaultParameters(RestClient client) { - foreach (var defaultParameter in client.DefaultParameters) { - var parameterExists = - _requestParameters.Any( - p => - p.Name != null && - p.Name.Equals(defaultParameter.Name, StringComparison.InvariantCultureIgnoreCase) && - p.Type == defaultParameter.Type - ); - - if (client.Options.AllowMultipleDefaultParametersWithSameName) { - var isMultiParameter = MultiParameterTypes.Any(pt => pt == defaultParameter.Type); + IEnumerable GetParameters(ParametersCollection parametersCollection, bool allowSameName) { + foreach (var parameter in parametersCollection) { + var parameterExists = Parameters.Exists(parameter); + + if (allowSameName) { + var isMultiParameter = MultiParameterTypes.Any(pt => pt == parameter.Type); parameterExists = !isMultiParameter && parameterExists; } - if (!parameterExists) _requestParameters.Add(defaultParameter); + if (!parameterExists) yield return parameter; } - - return this; } // Add Accept header based on registered deserializers if none has been set by the caller. - public RequestParameters AddAcceptHeader(RestClient client) { - if (_requestParameters.All(p => !p.Name!.EqualsIgnoreCase(KnownHeaders.Accept))) { - var accepts = string.Join(", ", client.AcceptedContentTypes); - _requestParameters.Add(new Parameter(KnownHeaders.Accept, accepts, ParameterType.HttpHeader)); + public RequestParameters AddAcceptHeader(string[] acceptedContentTypes) { + if (Parameters.TryFind(KnownHeaders.Accept) == null) { + var accepts = string.Join(", ", acceptedContentTypes); + Parameters.AddParameter(new Parameter(KnownHeaders.Accept, accepts, ParameterType.HttpHeader)); } return this; diff --git a/src/RestSharp/Request/RestRequest.cs b/src/RestSharp/Request/RestRequest.cs index d211353fd..0e01f41c8 100644 --- a/src/RestSharp/Request/RestRequest.cs +++ b/src/RestSharp/Request/RestRequest.cs @@ -31,16 +31,6 @@ public RestRequest() { Method = Method.Get; } - /// - /// Sets Method property to value of method - /// - /// Method to use for this request - public RestRequest(Method method) : this() => Method = method; - - public RestRequest(string resource, Method method) : this(resource, method, DataFormat.Json) { } - - public RestRequest(string resource, DataFormat dataFormat) : this(resource, Method.Get, dataFormat) { } - public RestRequest(string? resource, Method method = Method.Get, DataFormat dataFormat = DataFormat.Json) : this() { Resource = resource ?? ""; Method = method; @@ -80,7 +70,7 @@ public RestRequest(Uri resource, Method method = Method.Get, DataFormat dataForm dataFormat ) { } - readonly List _parameters = new(); + // readonly List _parameters = new(); readonly List _files = new(); /// @@ -92,7 +82,7 @@ public RestRequest(Uri resource, Method method = Method.Get, DataFormat dataForm /// Container of all HTTP parameters to be passed with the request. /// See AddParameter() for explanation of the types of parameters that can be passed /// - public IReadOnlyCollection Parameters => _parameters.AsReadOnly(); + public ParametersCollection Parameters { get; } = new(); /// /// Container of all the files to be uploaded with the request. @@ -192,10 +182,10 @@ public RestRequest AddParameter(Parameter p) { if (p.Type == ParameterType.Cookie) throw new InvalidOperationException("Cookie parameters should be added to the RestClient's cookie container"); - return this.With(x => x._parameters.Add(p)); + return this.With(x => x.Parameters.AddParameter(p)); } - public void RemoveParameter(Parameter p) => _parameters.Remove(p); + public void RemoveParameter(Parameter p) => Parameters.RemoveParameter(p); internal RestRequest AddFile(FileParameter file) => this.With(x => x._files.Add(file)); } \ No newline at end of file diff --git a/src/RestSharp/Request/RestRequestExtensions.cs b/src/RestSharp/Request/RestRequestExtensions.cs index a3c749c1f..104b7884e 100644 --- a/src/RestSharp/Request/RestRequestExtensions.cs +++ b/src/RestSharp/Request/RestRequestExtensions.cs @@ -28,14 +28,14 @@ public static class RestRequestExtensions { /// Name of the parameter /// Value of the parameter /// This request - public static RestRequest AddParameter(this RestRequest request, string name, object value) - => request.AddParameter(new Parameter(name, value, ParameterType.GetOrPost)); + public static RestRequest AddParameter(this RestRequest request, string name, object value, bool encode = true) + => request.AddParameter(new Parameter(name, value, ParameterType.GetOrPost, encode)); - public static RestRequest AddParameter(this RestRequest request, string? name, object value, ParameterType type) - => request.AddParameter(new Parameter(name, value, type)); + public static RestRequest AddParameter(this RestRequest request, string? name, object value, ParameterType type, bool encode = true) + => request.AddParameter(new Parameter(name, value, type, encode)); - public static RestRequest AddParameter(this RestRequest request, string name, object value, string contentType, ParameterType type) - => request.AddParameter(new Parameter(name, value, contentType, type)); + public static RestRequest AddParameter(this RestRequest request, string name, object value, string contentType, ParameterType type, bool encode = true) + => request.AddParameter(new Parameter(name, value, contentType, type, encode)); public static RestRequest AddOrUpdateParameter(this RestRequest request, Parameter parameter) { var p = request.Parameters .FirstOrDefault(x => x.Name == parameter.Name && x.Type == parameter.Type); @@ -56,20 +56,20 @@ public static RestRequest AddOrUpdateParameters(this RestRequest request, IEnume public static RestRequest AddOrUpdateParameter(this RestRequest request, string name, object value) => request.AddOrUpdateParameter(new Parameter(name, value, ParameterType.GetOrPost)); - public static RestRequest AddOrUpdateParameter(this RestRequest request, string name, object value, ParameterType type) + public static RestRequest AddOrUpdateParameter(this RestRequest request, string name, object value, ParameterType type, bool encode = true) => request.AddOrUpdateParameter(new Parameter(name, value, type)); - public static RestRequest AddOrUpdateParameter(this RestRequest request, string name, object value, string contentType, ParameterType type) - => request.AddOrUpdateParameter(new Parameter(name, value, contentType, type)); + public static RestRequest AddOrUpdateParameter(this RestRequest request, string name, object value, string contentType, ParameterType type, bool encode = true) + => request.AddOrUpdateParameter(new Parameter(name, value, contentType, type, encode)); - public static RestRequest AddHeader(this RestRequest request, string name, string value) { + public static RestRequest AddHeader(this RestRequest request, string name, string value, bool encode = true) { CheckAndThrowsForInvalidHost(name, value); - return request.AddParameter(name, value, ParameterType.HttpHeader); + return request.AddParameter(name, value, ParameterType.HttpHeader, encode); } - public static RestRequest AddOrUpdateHeader(this RestRequest request, string name, string value) { + public static RestRequest AddOrUpdateHeader(this RestRequest request, string name, string value, bool encode = true) { CheckAndThrowsForInvalidHost(name, value); - return request.AddOrUpdateParameter(name, value, ParameterType.HttpHeader); + return request.AddOrUpdateParameter(name, value, ParameterType.HttpHeader, encode); } public static RestRequest AddHeaders(this RestRequest request, ICollection> headers) { diff --git a/src/RestSharp/Request/UriExtensions.cs b/src/RestSharp/Request/UriExtensions.cs new file mode 100644 index 000000000..a7a1a8288 --- /dev/null +++ b/src/RestSharp/Request/UriExtensions.cs @@ -0,0 +1,94 @@ +// Copyright © 2009-2020 John Sheehan, Andrew Young, Alexey Zimarev and RestSharp community +// +// 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. +// + +using System.Text; +using RestSharp.Extensions; + +namespace RestSharp; + +static class UriExtensions { + public static Uri MergeBaseUrlAndResource(this Uri? baseUrl, string? resource) { + var assembled = resource; + + if (assembled.IsNotEmpty() && assembled!.StartsWith("/")) assembled = assembled.Substring(1); + + if (baseUrl == null || baseUrl.AbsoluteUri.IsEmpty()) { + return assembled.IsNotEmpty() + ? new Uri(assembled!) + : throw new ArgumentException("Both BaseUrl and Resource are empty", nameof(resource)); + } + + var usingBaseUri = baseUrl.AbsoluteUri.EndsWith("/") || assembled.IsEmpty() ? baseUrl : new Uri(baseUrl.AbsoluteUri + "/"); + + return assembled != null ? new Uri(usingBaseUri, assembled) : baseUrl; + } + + public static Uri ApplyQueryStringParamsValuesToUri( + this Uri mergedUri, + Method method, + Encoding encoding, + Func encodeQuery, + params ParametersCollection[] parametersCollections + ) { + var parameters = parametersCollections.SelectMany(x => x.GetQueryParameters(method)).ToList(); + + if (parameters.Count == 0) return mergedUri; + + var uri = mergedUri.AbsoluteUri; + var separator = uri.Contains("?") ? "&" : "?"; + + return new Uri(string.Concat(uri, separator, EncodeParameters())); + + string EncodeParameters() => string.Join("&", parameters.Select(EncodeParameter).ToArray()); + + string EncodeParameter(Parameter parameter) { + return + !parameter.Encode + ? $"{parameter.Name}={StringOrEmpty(parameter.Value)}" + : $"{encodeQuery(parameter.Name!, encoding)}={encodeQuery(StringOrEmpty(parameter.Value), encoding)}"; + + static string StringOrEmpty(object? value) => value == null ? "" : value.ToString()!; + } + } + + public static UrlSegmentParamsValues GetUrlSegmentParamsValues( + this Uri? baseUri, + string resource, + Func encode, + params ParametersCollection[] parametersCollections + ) { + var assembled = baseUri == null ? "" : resource; + var baseUrl = baseUri ?? new Uri(resource); + + var hasResource = !assembled.IsEmpty(); + + var parameters = parametersCollections.SelectMany(x => x.GetParameters(ParameterType.UrlSegment)); + + var builder = new UriBuilder(baseUrl); + + foreach (var parameter in parameters) { + var paramPlaceHolder = $"{{{parameter.Name}}}"; + var paramValue = parameter.Encode ? encode(parameter.Value!.ToString()!) : parameter.Value!.ToString(); + + if (hasResource) assembled = assembled.Replace(paramPlaceHolder, paramValue); + + builder.Path = builder.Path.UrlDecode().Replace(paramPlaceHolder, paramValue); + } + + return new UrlSegmentParamsValues(builder.Uri, assembled); + } +} + +record UrlSegmentParamsValues(Uri Uri, string Resource); \ No newline at end of file diff --git a/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs index 2e2cd1027..adcd07c5d 100644 --- a/src/RestSharp/RestClient.Async.cs +++ b/src/RestSharp/RestClient.Async.cs @@ -61,10 +61,10 @@ async Task ExecuteInternal(RestRequest request, CancellationTo try { var parameters = new RequestParameters() - .AddRequestParameters(request) - .AddDefaultParameters(this) - .AddAcceptHeader(this); - message.AddHeaders(parameters.Parameters); + .AddParameters(request.Parameters, true) + .AddParameters(DefaultParameters, Options.AllowMultipleDefaultParametersWithSameName) + .AddAcceptHeader(AcceptedContentTypes); + message.AddHeaders(parameters.Parameters, Encode); if (request.OnBeforeRequest != null) await request.OnBeforeRequest(message); @@ -164,12 +164,12 @@ RestResponse ReturnErrorOrThrow(RestResponse response, Exception exception, Canc return Options.ThrowOnAnyError ? ThrowIfError(response) : response; } - internal static RestResponse ThrowIfError(RestResponse response) { + static RestResponse ThrowIfError(RestResponse response) { var exception = response.GetException(); return exception != null ? throw exception : response; } - HttpMethod AsHttpMethod(Method method) + static HttpMethod AsHttpMethod(Method method) => method switch { Method.Get => HttpMethod.Get, Method.Post => HttpMethod.Post, diff --git a/src/RestSharp/RestClient.cs b/src/RestSharp/RestClient.cs index 41ecb678d..9eee56c8d 100644 --- a/src/RestSharp/RestClient.cs +++ b/src/RestSharp/RestClient.cs @@ -31,7 +31,7 @@ namespace RestSharp; /// public partial class RestClient { readonly CookieContainer _cookieContainer; - readonly List _defaultParameters = new(); + // readonly List _defaultParameters = new(); HttpClient HttpClient { get; } @@ -85,7 +85,7 @@ public RestClient(RestClientOptions options) { handler.MaxAutomaticRedirections = Options.MaxRedirects.Value; var finalHandler = Options.ConfigureMessageHandler?.Invoke(handler) ?? handler; - + HttpClient = new HttpClient(finalHandler); if (Options.Timeout > 0) @@ -134,9 +134,10 @@ public RestClient(string baseUrl) : this(new Uri(Ensure.NotEmptyString(baseUrl, /// public IAuthenticator? Authenticator { get; set; } - public IReadOnlyCollection DefaultParameters { - get { lock (_defaultParameters) return _defaultParameters; } - } + // public IReadOnlyCollection DefaultParameters { + // get { lock (_defaultParameters) return _defaultParameters; } + // } + public ParametersCollection DefaultParameters { get; } = new(); /// /// Add a parameter to use on every request made with this client instance @@ -153,12 +154,11 @@ public RestClient AddDefaultParameter(Parameter p) { lock (_cookieContainer) { _cookieContainer.Add(new Cookie(p.Name!, p.Value!.ToString())); } + break; } default: { - lock (_defaultParameters) { - _defaultParameters.Add(p); - } + DefaultParameters.AddParameter(p); break; } } @@ -172,19 +172,22 @@ public RestClient AddDefaultParameter(Parameter p) { public Uri BuildUri(RestRequest request) { DoBuildUriValidations(request); - var applied = GetUrlSegmentParamsValues(request); - var mergedUri = MergeBaseUrlAndResource(applied.Uri, applied.Resource); - var finalUri = ApplyQueryStringParamsValuesToUri(mergedUri, request); - - return new Uri(finalUri!); + var (uri, resource) = Options.BaseUrl.GetUrlSegmentParamsValues(request.Resource, Encode, request.Parameters, DefaultParameters); + var mergedUri = uri.MergeBaseUrlAndResource(resource); + var finalUri = mergedUri.ApplyQueryStringParamsValuesToUri( + request.Method, + Options.Encoding, + EncodeQuery, + request.Parameters, + DefaultParameters + ); + return finalUri; } - internal string BuildUriWithoutQueryParameters(RestRequest request) { + internal Uri BuildUriWithoutQueryParameters(RestRequest request) { DoBuildUriValidations(request); - - var applied = GetUrlSegmentParamsValues(request); - - return MergeBaseUrlAndResource(applied.Uri, applied.Resource); + var (uri, resource) = Options.BaseUrl.GetUrlSegmentParamsValues(request.Resource, Encode, request.Parameters, DefaultParameters); + return uri.MergeBaseUrlAndResource(resource); } public string[] AcceptedContentTypes { get; private set; } = null!; @@ -218,7 +221,8 @@ void DoBuildUriValidations(RestRequest request) { ); var nullValuedParams = request.Parameters - .Where(p => p.Type == ParameterType.UrlSegment && p.Value == null) + .GetParameters(ParameterType.UrlSegment) + .Where(p => p.Value == null) .Select(p => p.Name) .ToArray(); @@ -232,87 +236,6 @@ void DoBuildUriValidations(RestRequest request) { } } - UrlSegmentParamsValues GetUrlSegmentParamsValues(RestRequest request) { - var assembled = Options.BaseUrl == null ? "" : request.Resource; - var baseUrl = Options.BaseUrl ?? new Uri(request.Resource); - - var hasResource = !assembled.IsEmpty(); - var parameters = request.Parameters.Where(p => p.Type == ParameterType.UrlSegment).ToList(); - - lock (_defaultParameters) { - parameters.AddRange(_defaultParameters.Where(p => p.Type == ParameterType.UrlSegment)); - } - var builder = new UriBuilder(baseUrl); - - foreach (var parameter in parameters) { - var paramPlaceHolder = $"{{{parameter.Name}}}"; - var paramValue = parameter.Encode ? Encode(parameter.Value!.ToString()!) : parameter.Value!.ToString(); - - if (hasResource) assembled = assembled.Replace(paramPlaceHolder, paramValue); - - builder.Path = builder.Path.UrlDecode().Replace(paramPlaceHolder, paramValue); - } - - return new UrlSegmentParamsValues(builder.Uri, assembled); - } - - static string MergeBaseUrlAndResource(Uri? baseUrl, string? resource) { - var assembled = resource; - - if (!IsNullOrEmpty(assembled) && assembled!.StartsWith("/")) assembled = assembled.Substring(1); - - if (baseUrl == null || IsNullOrEmpty(baseUrl.AbsoluteUri)) return assembled ?? ""; - - var usingBaseUri = baseUrl.AbsoluteUri.EndsWith("/") || IsNullOrEmpty(assembled) ? baseUrl : new Uri(baseUrl.AbsoluteUri + "/"); - - return assembled != null ? new Uri(usingBaseUri, assembled).AbsoluteUri : baseUrl.AbsoluteUri; - } - - string? ApplyQueryStringParamsValuesToUri(string? mergedUri, RestRequest request) { - var parameters = GetQueryStringParameters(request).ToList(); - parameters.AddRange(GetDefaultQueryStringParameters(request)); - - if (!parameters.Any()) return mergedUri; - - var separator = mergedUri != null && mergedUri.Contains("?") ? "&" : "?"; - - return Concat(mergedUri, separator, EncodeParameters(parameters, Options.Encoding)); - } - - IEnumerable GetDefaultQueryStringParameters(RestRequest request) - => request.Method != Method.Post && request.Method != Method.Put && request.Method != Method.Patch - ? DefaultParameters - .Where( - p => p.Type is ParameterType.GetOrPost or ParameterType.QueryString - ) - : DefaultParameters - .Where( - p => p.Type is ParameterType.QueryString - ); - - static IEnumerable GetQueryStringParameters(RestRequest request) - => request.Method != Method.Post && request.Method != Method.Put && request.Method != Method.Patch - ? request.Parameters - .Where( - p => p.Type is ParameterType.GetOrPost or ParameterType.QueryString - ) - : request.Parameters - .Where( - p => p.Type is ParameterType.QueryString - ); - - string EncodeParameters(IEnumerable parameters, Encoding encoding) - => Join("&", parameters.Select(parameter => EncodeParameter(parameter, encoding)).ToArray()); - - string EncodeParameter(Parameter parameter, Encoding encoding) { - return - !parameter.Encode - ? $"{parameter.Name}={StringOrEmpty(parameter.Value)}" - : $"{EncodeQuery(parameter.Name!, encoding)}={EncodeQuery(StringOrEmpty(parameter.Value), encoding)}"; - - static string StringOrEmpty(object? value) => value == null ? "" : value.ToString()!; - } - internal RestResponse Deserialize(RestRequest request, RestResponse raw) { var response = RestResponse.FromResponse(raw); @@ -329,9 +252,9 @@ internal RestResponse Deserialize(RestRequest request, RestResponse raw) { // This can happen when a request returns for example a 404 page instead of the requested JSON/XML resource if (handler is IXmlDeserializer xml && request is RestXmlRequest xmlRequest) { if (xmlRequest.XmlNamespace.IsNotEmpty()) xml.Namespace = xmlRequest.XmlNamespace!; - + if (xml is IWithDateFormat withDateFormat && xmlRequest.DateFormat.IsNotEmpty()) - withDateFormat.DateFormat = xmlRequest.DateFormat!; + withDateFormat.DateFormat = xmlRequest.DateFormat!; } if (handler is IWithRootElement deserializer && !request.RootElement.IsEmpty()) deserializer.RootElement = request.RootElement; @@ -362,20 +285,10 @@ internal RestResponse Deserialize(RestRequest request, RestResponse raw) { if (contentType.IsEmpty()) return null; var serializer = Serializers.FirstOrDefault(x => x.Value.SupportedContentTypes.Contains(contentType)); - var factory = serializer.Value ?? (Serializers.ContainsKey(requestFormat) ? Serializers[requestFormat] : null); + var factory = serializer.Value ?? (Serializers.ContainsKey(requestFormat) ? Serializers[requestFormat] : null); return factory?.GetSerializer().Deserializer; string? DetectContentType() => response.Content!.StartsWith("<") ? ContentType.Xml : response.Content.StartsWith("{") ? ContentType.Json : null; } - - class UrlSegmentParamsValues { - public UrlSegmentParamsValues(Uri builderUri, string assembled) { - Uri = builderUri; - Resource = assembled; - } - - public Uri Uri { get; } - public string Resource { get; } - } } \ No newline at end of file diff --git a/src/RestSharp/RestClientExtensions.Json.cs b/src/RestSharp/RestClientExtensions.Json.cs index 2b0783853..6857f3a0b 100644 --- a/src/RestSharp/RestClientExtensions.Json.cs +++ b/src/RestSharp/RestClientExtensions.Json.cs @@ -13,24 +13,105 @@ // limitations under the License. // +using System.Net; + namespace RestSharp; public static partial class RestClientExtensions { - public static Task PostAsync( + /// + /// Calls the URL specified in the resource parameter, expecting a JSON response back. Deserializes and returns the response. + /// + /// RestClient instance + /// Resource URL + /// Cancellation token + /// Response object type + /// + public static Task GetJsonAsync(this RestClient client, string resource, CancellationToken cancellationToken = default) { + var request = new RestRequest(resource); + return client.GetAsync(request, cancellationToken); + } + + /// + /// Serializes the request object to JSON and makes a POST call to the resource specified in the resource parameter. + /// Expects a JSON response back, deserializes it to TResponse type and returns it. + /// + /// RestClient instance + /// Resource URL + /// Request object, must be serializable to JSON + /// Cancellation token + /// Request object type + /// Response object type + /// Deserialized response object + public static Task PostJsonAsync( this RestClient client, + string resource, TRequest request, CancellationToken cancellationToken = default ) where TRequest : class { var restRequest = new RestRequest().AddJsonBody(request); return client.PostAsync(restRequest, cancellationToken); } - - public static Task PutAsync( + + /// + /// Serializes the request object to JSON and makes a POST call to the resource specified in the resource parameter. + /// Expects no response back, just the status code. + /// + /// RestClient instance + /// Resource URL + /// Request object, must be serializable to JSON + /// Cancellation token + /// Request object type + /// Response status code + public static async Task PostJsonAsync( this RestClient client, + string resource, + TRequest request, + CancellationToken cancellationToken = default + ) where TRequest : class { + var restRequest = new RestRequest().AddJsonBody(request); + var response = await client.PostAsync(restRequest, cancellationToken); + return response.StatusCode; + } + + /// + /// Serializes the request object to JSON and makes a PUT call to the resource specified in the resource parameter. + /// Expects a JSON response back, deserializes it to TResponse type and returns it. + /// + /// RestClient instance + /// Resource URL + /// Request object, must be serializable to JSON + /// Cancellation token + /// Request object type + /// Response object type + /// Deserialized response object + public static Task PutJsonAsync( + this RestClient client, + string resource, TRequest request, CancellationToken cancellationToken = default ) where TRequest : class { var restRequest = new RestRequest().AddJsonBody(request); return client.PutAsync(restRequest, cancellationToken); } + + /// + /// Serializes the request object to JSON and makes a PUT call to the resource specified in the resource parameter. + /// Expects no response back, just the status code. + /// + /// RestClient instance + /// Resource URL + /// Request object, must be serializable to JSON + /// Cancellation token + /// Request object type + /// Response status code + public static async Task PutJsonAsync( + this RestClient client, + string resource, + TRequest request, + CancellationToken cancellationToken = default + ) where TRequest : class { + var restRequest = new RestRequest().AddJsonBody(request); + var response = await client.PutAsync(restRequest, cancellationToken); + return response.StatusCode; + } } \ No newline at end of file diff --git a/src/RestSharp/RestClientExtensions.cs b/src/RestSharp/RestClientExtensions.cs index 448384cb1..272b57572 100644 --- a/src/RestSharp/RestClientExtensions.cs +++ b/src/RestSharp/RestClientExtensions.cs @@ -131,8 +131,11 @@ public static Task> ExecuteAsync( return response.Data; } - public static Task GetAsync(this RestClient client, RestRequest request, CancellationToken cancellationToken = default) - => client.ExecuteAsync(request, Method.Get, cancellationToken); + public static async Task GetAsync(this RestClient client, RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(request, Method.Get, cancellationToken); + ThrowIfError(response); + return response; + } /// /// Execute the request using POST HTTP method. Exception will be thrown if the request does not succeed. @@ -149,8 +152,11 @@ public static Task GetAsync(this RestClient client, RestRequest request, Cancell return response.Data; } - public static Task PostAsync(this RestClient client, RestRequest request, CancellationToken cancellationToken = default) - => client.ExecuteAsync(request, Method.Post, cancellationToken); + public static async Task PostAsync(this RestClient client, RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(request, Method.Post, cancellationToken); + ThrowIfError(response); + return response; + } /// /// Execute the request using PUT HTTP method. Exception will be thrown if the request does not succeed. @@ -167,8 +173,11 @@ public static Task PostAsync(this RestClient client, RestRequest r return response.Data; } - public static Task PutAsync(this RestClient client, RestRequest request, CancellationToken cancellationToken = default) - => client.ExecuteAsync(request, Method.Put, cancellationToken); + public static async Task PutAsync(this RestClient client, RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(request, Method.Put, cancellationToken); + ThrowIfError(response); + return response; + } /// /// Execute the request using HEAD HTTP method. Exception will be thrown if the request does not succeed. @@ -185,8 +194,11 @@ public static Task PutAsync(this RestClient client, RestRequest request, Cancell return response.Data; } - public static Task HeadAsync(this RestClient client, RestRequest request, CancellationToken cancellationToken = default) - => client.ExecuteAsync(request, Method.Head, cancellationToken); + public static async Task HeadAsync(this RestClient client, RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(request, Method.Head, cancellationToken); + ThrowIfError(response); + return response; + } /// /// Execute the request using OPTIONS HTTP method. Exception will be thrown if the request does not succeed. @@ -203,8 +215,11 @@ public static Task HeadAsync(this RestClient client, RestRequest request, Cancel return response.Data; } - public static Task OptionsAsync(this RestClient client, RestRequest request, CancellationToken cancellationToken = default) - => client.ExecuteAsync(request, Method.Options, cancellationToken); + public static async Task OptionsAsync(this RestClient client, RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(request, Method.Options, cancellationToken); + ThrowIfError(response); + return response; + } /// /// Execute the request using PATCH HTTP method. Exception will be thrown if the request does not succeed. @@ -221,8 +236,11 @@ public static Task OptionsAsync(this RestClient client, RestRequest request, Can return response.Data; } - public static Task PatchAsync(this RestClient client, RestRequest request, CancellationToken cancellationToken = default) - => client.ExecuteAsync(request, Method.Patch, cancellationToken); + public static async Task PatchAsync(this RestClient client, RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(request, Method.Patch, cancellationToken); + ThrowIfError(response); + return response; + } /// /// Execute the request using DELETE HTTP method. Exception will be thrown if the request does not succeed. @@ -239,8 +257,11 @@ public static Task PatchAsync(this RestClient client, RestRequest request, Cance return response.Data; } - public static Task DeleteAsync(this RestClient client, RestRequest request, CancellationToken cancellationToken = default) - => client.ExecuteAsync(request, Method.Delete, cancellationToken); + public static async Task DeleteAsync(this RestClient client, RestRequest request, CancellationToken cancellationToken = default) { + var response = await client.ExecuteAsync(request, Method.Delete, cancellationToken); + ThrowIfError(response); + return response; + } /// /// Sets the to only use JSON diff --git a/src/RestSharp/RestSharp.csproj b/src/RestSharp/RestSharp.csproj index 1034dcb6d..448533095 100644 --- a/src/RestSharp/RestSharp.csproj +++ b/src/RestSharp/RestSharp.csproj @@ -1,5 +1,5 @@  - + diff --git a/test/Directory.Build.props b/test/Directory.Build.props index cdb2a6932..ba3952801 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -7,13 +7,15 @@ disable - + - + + + + - diff --git a/test/RestSharp.IntegrationTests/Authentication/AuthenticationTests.cs b/test/RestSharp.IntegrationTests/Authentication/AuthenticationTests.cs index 40ccfeeed..474dcd3ee 100644 --- a/test/RestSharp.IntegrationTests/Authentication/AuthenticationTests.cs +++ b/test/RestSharp.IntegrationTests/Authentication/AuthenticationTests.cs @@ -1,10 +1,11 @@ using System.Net; using System.Text; +using System.Web; using RestSharp.Authenticators; +using RestSharp.IntegrationTests.Fixtures; using RestSharp.Tests.Shared.Extensions; -using RestSharp.Tests.Shared.Fixtures; -namespace RestSharp.IntegrationTests.Authentication; +namespace RestSharp.IntegrationTests.Authentication; public class AuthenticationTests { readonly ITestOutputHelper _output; @@ -14,7 +15,8 @@ public class AuthenticationTests { static void UsernamePasswordEchoHandler(HttpListenerContext context) { var header = context.Request.Headers["Authorization"]!; - var parts = Encoding.ASCII.GetString(Convert.FromBase64String(header["Basic ".Length..])) + var parts = Encoding.ASCII + .GetString(Convert.FromBase64String(header["Basic ".Length..])) .Split(':'); context.Response.OutputStream.WriteStringUtf8(string.Join("|", parts)); @@ -22,14 +24,26 @@ static void UsernamePasswordEchoHandler(HttpListenerContext context) { [Fact] public async Task Can_Authenticate_With_Basic_Http_Auth() { - using var server = SimpleServer.Create(UsernamePasswordEchoHandler); + const string userName = "testuser"; + const string password = "testpassword"; + + var server = new HttpServer(); + await server.Start(); var client = new RestClient(server.Url) { - Authenticator = new HttpBasicAuthenticator("testuser", "testpassword") + Authenticator = new HttpBasicAuthenticator(userName, password) }; - var request = new RestRequest("test"); - var response = await client.ExecuteAsync(request); - - Assert.Equal("testuser|testpassword", response.Content); + var request = new RestRequest("headers"); + var response = await client.GetAsync(request); + + var header = response!.First(x => x.Name == KnownHeaders.Authorization); + var auth = HttpUtility.UrlDecode(header.Value)["Basic ".Length..]; + var value = Convert.FromBase64String(auth); + var parts = Encoding.UTF8.GetString(value).Split(':'); + + parts[0].Should().Be(userName); + parts[1].Should().Be(password); + + await server.Stop(); } } \ No newline at end of file diff --git a/test/RestSharp.IntegrationTests/Fixtures/TestServer.cs b/test/RestSharp.IntegrationTests/Fixtures/TestServer.cs index 0a4a74063..97f42bf5c 100644 --- a/test/RestSharp.IntegrationTests/Fixtures/TestServer.cs +++ b/test/RestSharp.IntegrationTests/Fixtures/TestServer.cs @@ -15,7 +15,7 @@ public HttpServer(ITestOutputHelper output = null) { if (output != null) builder.WebHost.ConfigureLogging(x => x.SetMinimumLevel(LogLevel.Information).AddXunit(output, LogLevel.Debug)); - + builder.WebHost.UseUrls(Address); _app = builder.Build(); _app.MapGet("success", () => new TestResponse { Message = "Works!" }); @@ -23,6 +23,13 @@ public HttpServer(ITestOutputHelper output = null) { _app.MapGet("timeout", async () => await Task.Delay(2000)); // ReSharper disable once ConvertClosureToMethodGroup _app.MapGet("status", (int code) => Results.StatusCode(code)); + + _app.MapGet("headers", HandleHeaders); + + IResult HandleHeaders(HttpContext ctx) { + var response = ctx.Request.Headers.Select(x => new TestServerResponse(x.Key, x.Value)); + return Results.Ok(response); + } } public Uri Url => new(Address); @@ -30,4 +37,6 @@ public HttpServer(ITestOutputHelper output = null) { public Task Start() => _app.StartAsync(); public Task Stop() => _app.StopAsync(); -} \ No newline at end of file +} + +public record TestServerResponse(string Name, string Value); \ No newline at end of file diff --git a/test/RestSharp.IntegrationTests/OAuth1Tests.cs b/test/RestSharp.IntegrationTests/OAuth1Tests.cs index f27dbeab5..40c531a16 100644 --- a/test/RestSharp.IntegrationTests/OAuth1Tests.cs +++ b/test/RestSharp.IntegrationTests/OAuth1Tests.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using System.Net; -using System.Web; using System.Xml.Serialization; using RestSharp.Authenticators; using RestSharp.Authenticators.OAuth; @@ -189,7 +188,7 @@ public void Can_Authenticate_OAuth1_With_Querystring_Parameters() { }; var client = new RestClient(baseUrl); - var request = new RestRequest(Method.Get); + var request = new RestRequest(); var authenticator = OAuth1Authenticator.ForRequestToken(consumerKey, consumerSecret); authenticator.ParameterHandling = OAuthParameterHandling.UrlOrPostParameters; authenticator.Authenticate(client, request); diff --git a/test/RestSharp.IntegrationTests/RequestHeadTests.cs b/test/RestSharp.IntegrationTests/RequestHeadTests.cs index 0fb746ff2..6398e0798 100644 --- a/test/RestSharp.IntegrationTests/RequestHeadTests.cs +++ b/test/RestSharp.IntegrationTests/RequestHeadTests.cs @@ -22,7 +22,7 @@ public async Task Does_Not_Pass_Default_Credentials_When_Server_Does_Not_Negotia var keys = RequestHeadCapturer.CapturedHeaders.Keys.Cast().ToArray(); Assert.False( - keys.Contains("Authorization"), + keys.Contains(KnownHeaders.Authorization), "Authorization header was present in HTTP request from client, even though server does not use the Negotiate scheme" ); } @@ -58,7 +58,7 @@ public async Task Passes_Default_Credentials_When_UseDefaultCredentials_Is_True( keys.Should() .Contain( - "Authorization", + KnownHeaders.Authorization, "Authorization header not present in HTTP request from client, even though UseDefaultCredentials = true" ); } diff --git a/test/RestSharp.IntegrationTests/StatusCodeTests.cs b/test/RestSharp.IntegrationTests/StatusCodeTests.cs index 22d32fa57..ff035f47f 100644 --- a/test/RestSharp.IntegrationTests/StatusCodeTests.cs +++ b/test/RestSharp.IntegrationTests/StatusCodeTests.cs @@ -24,7 +24,7 @@ public StatusCodeTests() { public async Task ContentType_Additional_Information() { _server.SetHandler(Handlers.Generic()); - var request = new RestRequest(Method.Post) { + var request = new RestRequest("", Method.Post) { RequestFormat = DataFormat.Json, Resource = "contenttype_odata" }; diff --git a/test/RestSharp.InteractiveTests/AuthenticationTests.cs b/test/RestSharp.InteractiveTests/AuthenticationTests.cs index 9987167ea..859c70152 100644 --- a/test/RestSharp.InteractiveTests/AuthenticationTests.cs +++ b/test/RestSharp.InteractiveTests/AuthenticationTests.cs @@ -6,77 +6,8 @@ namespace RestSharp.InteractiveTests; public class AuthenticationTests { public class TwitterKeys { - public string? ConsumerKey { get; set; } - public string? ConsumerSecret { get; set; } - } - - public static async Task Can_Authenticate_With_OAuth(TwitterKeys twitterKeys) { - Console.WriteLine("OAuth test"); - - var baseUrl = new Uri("https://api.twitter.com"); - - Console.WriteLine("Getting request token..."); - - var client = new RestClient(baseUrl) { - Authenticator = OAuth1Authenticator.ForRequestToken(twitterKeys.ConsumerKey, twitterKeys.ConsumerSecret) - }; - var request = new RestRequest("oauth/request_token"); - var response = await client.ExecuteAsync(request); - - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var qs = HttpUtility.ParseQueryString(response.Content); - var oauthToken = qs["oauth_token"]!; - var oauthTokenSecret = qs["oauth_token_secret"]!; - - Assert.NotNull(oauthToken); - Assert.NotNull(oauthTokenSecret); - - request = new RestRequest("oauth/authorize?oauth_token=" + oauthToken); - - var url = client.BuildUri(request) - .ToString(); - - Console.WriteLine($"Open this URL in the browser: {url} and complete the authentication."); - Console.Write("Enter the verifier: "); - var verifier = Console.ReadLine(); - - Console.WriteLine("Getting access token..."); - request = new RestRequest("oauth/access_token"); - - client.Authenticator = OAuth1Authenticator.ForAccessToken( - twitterKeys.ConsumerKey, - twitterKeys.ConsumerSecret, - oauthToken, - oauthTokenSecret, - verifier! - ); - response = await client.ExecuteAsync(request); - - Console.WriteLine($"Code: {response.StatusCode}, response: {response.Content}"); - Assert.NotNull(response); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - qs = HttpUtility.ParseQueryString(response.Content); - oauthToken = qs["oauth_token"]!; - oauthTokenSecret = qs["oauth_token_secret"]!; - - Assert.NotNull(oauthToken); - Assert.NotNull(oauthTokenSecret); - - Console.WriteLine("Verifying credentials..."); - request = new RestRequest("1.1/account/verify_credentials.json", DataFormat.Json); - - client.Authenticator = OAuth1Authenticator.ForProtectedResource( - twitterKeys.ConsumerKey, - twitterKeys.ConsumerSecret, - oauthToken, - oauthTokenSecret - ); - response = await client.ExecuteAsync(request); - - Console.WriteLine($"Code: {response.StatusCode}, response: {response.Content}"); + public string ConsumerKey { get; set; } + public string ConsumerSecret { get; set; } } public static async Task Can_Authenticate_With_OAuth_Async_With_Callback(TwitterKeys twitterKeys) { diff --git a/test/RestSharp.InteractiveTests/Program.cs b/test/RestSharp.InteractiveTests/Program.cs index a6c667dc5..030f0919e 100644 --- a/test/RestSharp.InteractiveTests/Program.cs +++ b/test/RestSharp.InteractiveTests/Program.cs @@ -5,10 +5,9 @@ ConsumerSecret = Prompt("Consumer secret"), }; -AuthenticationTests.Can_Authenticate_With_OAuth(keys); await AuthenticationTests.Can_Authenticate_With_OAuth_Async_With_Callback(keys); -static string? Prompt(string message) { - Console.Write(message + ": "); +static string Prompt(string message) { + Console.Write($"{message}: "); return Console.ReadLine(); } \ No newline at end of file diff --git a/test/RestSharp.Tests/HttpBasicAuthenticatorTests.cs b/test/RestSharp.Tests/HttpBasicAuthenticatorTests.cs index 8836adb93..c5a736574 100644 --- a/test/RestSharp.Tests/HttpBasicAuthenticatorTests.cs +++ b/test/RestSharp.Tests/HttpBasicAuthenticatorTests.cs @@ -31,6 +31,6 @@ public void Authenticate_ShouldAddAuthorizationParameter_IfPreviouslyUnassigned( _authenticator.Authenticate(client, request); // Assert - request.Parameters.Single(x => x.Name == "Authorization").Value.Should().Be(expectedToken); + request.Parameters.Single(x => x.Name == KnownHeaders.Authorization).Value.Should().Be(expectedToken); } } \ No newline at end of file diff --git a/test/RestSharp.Tests/JwtAuthTests.cs b/test/RestSharp.Tests/JwtAuthTests.cs index f7133b353..2d1200e0c 100644 --- a/test/RestSharp.Tests/JwtAuthTests.cs +++ b/test/RestSharp.Tests/JwtAuthTests.cs @@ -22,31 +22,31 @@ public JwtAuthTests() { } [Fact] - public void Can_Set_ValidFormat_Auth_Header() { + public async Task Can_Set_ValidFormat_Auth_Header() { var client = new RestClient { Authenticator = new JwtAuthenticator(_testJwt) }; var request = new RestRequest(); //In real case client.Execute(request) will invoke Authenticate method - client.Authenticator.Authenticate(client, request); + await client.Authenticator.Authenticate(client, request); - var authParam = request.Parameters.Single(p => p.Name.Equals("Authorization", StringComparison.OrdinalIgnoreCase)); + var authParam = request.Parameters.Single(p => p.Name.Equals(KnownHeaders.Authorization, StringComparison.OrdinalIgnoreCase)); Assert.True(authParam.Type == ParameterType.HttpHeader); Assert.Equal(_expectedAuthHeaderContent, authParam.Value); } [Fact] - public void Check_Only_Header_Authorization() { + public async Task Check_Only_Header_Authorization() { var client = new RestClient { Authenticator = new JwtAuthenticator(_testJwt) }; var request = new RestRequest(); // Paranoid server needs "two-factor authentication": jwt header and query param key for example - request.AddParameter("Authorization", "manualAuth", ParameterType.QueryString); + request.AddParameter(KnownHeaders.Authorization, "manualAuth", ParameterType.QueryString); // In real case client.Execute(request) will invoke Authenticate method - client.Authenticator.Authenticate(client, request); + await client.Authenticator.Authenticate(client, request); - var paramList = request.Parameters.Where(p => p.Name.Equals("Authorization")).ToList(); + var paramList = request.Parameters.Where(p => p.Name.Equals(KnownHeaders.Authorization)).ToList(); Assert.Equal(2, paramList.Count); @@ -58,18 +58,18 @@ public void Check_Only_Header_Authorization() { } [Fact] - public void Set_Auth_Header_Only_Once() { + public async Task Set_Auth_Header_Only_Once() { var client = new RestClient(); var request = new RestRequest(); - request.AddHeader("Authorization", "second_header_auth_token"); + request.AddHeader(KnownHeaders.Authorization, "second_header_auth_token"); client.Authenticator = new JwtAuthenticator(_testJwt); //In real case client.Execute(...) will invoke Authenticate method - client.Authenticator.Authenticate(client, request); + await client.Authenticator.Authenticate(client, request); - var paramList = request.Parameters.Where(p => p.Name.Equals("Authorization")).ToList(); + var paramList = request.Parameters.Where(p => p.Name.Equals(KnownHeaders.Authorization)).ToList(); paramList.Should().HaveCount(1); @@ -81,19 +81,19 @@ public void Set_Auth_Header_Only_Once() { } [Fact] - public void Updates_Auth_Header() { + public async Task Updates_Auth_Header() { var client = new RestClient(); var request = new RestRequest(); var authenticator = new JwtAuthenticator(_expectedAuthHeaderContent); client.Authenticator = authenticator; - client.Authenticator.Authenticate(client, request); + await client.Authenticator.Authenticate(client, request); authenticator.SetBearerToken("second_header_auth_token"); - client.Authenticator.Authenticate(client, request); + await client.Authenticator.Authenticate(client, request); - var paramList = request.Parameters.Where(p => p.Name.Equals("Authorization")).ToList(); + var paramList = request.Parameters.Where(p => p.Name.Equals(KnownHeaders.Authorization)).ToList(); Assert.Single(paramList); diff --git a/test/RestSharp.Tests/OAuth1AuthenticatorTests.cs b/test/RestSharp.Tests/OAuth1AuthenticatorTests.cs index a4b6191c9..795257a78 100644 --- a/test/RestSharp.Tests/OAuth1AuthenticatorTests.cs +++ b/test/RestSharp.Tests/OAuth1AuthenticatorTests.cs @@ -38,7 +38,7 @@ public void Authenticate_ShouldAddAuthorizationAsTextValueToRequest_OnHttpAuthor _authenticator.Authenticate(client, request); // Assert - var authParameter = request.Parameters.Single(x => x.Name == "Authorization"); + var authParameter = request.Parameters.Single(x => x.Name == KnownHeaders.Authorization); var value = (string)authParameter.Value; Assert.Contains("OAuth", value); @@ -165,7 +165,7 @@ public void Authenticate_ShouldEncodeOAuthTokenParameter(OAuthType type, string _authenticator.Authenticate(client, request); // Assert - var authParameter = request.Parameters.Single(x => x.Name == "Authorization"); + var authParameter = request.Parameters.Single(x => x.Name == KnownHeaders.Authorization); var authHeader = (string)authParameter.Value; Assert.NotNull(authHeader); @@ -192,12 +192,12 @@ public void Authenticate_ShouldAllowEmptyConsumerSecret_OnHttpAuthorizationHeade _authenticator.Authenticate(client, request); // Assert - var authParameter = request.Parameters.Single(x => x.Name == "Authorization"); + var authParameter = request.Parameters.Single(x => x.Name == KnownHeaders.Authorization); var value = (string)authParameter.Value; Assert.NotNull(value); Assert.NotEmpty(value); Assert.Contains("OAuth", value!); - Assert.Contains("oauth_signature=\"" + OAuthTools.UrlEncodeStrict("&"), value); + Assert.Contains($"oauth_signature=\"{OAuthTools.UrlEncodeStrict("&")}", value); } } \ No newline at end of file diff --git a/test/RestSharp.Tests/RestClientTests.cs b/test/RestSharp.Tests/RestClientTests.cs index b8199d948..035f34e71 100644 --- a/test/RestSharp.Tests/RestClientTests.cs +++ b/test/RestSharp.Tests/RestClientTests.cs @@ -13,7 +13,7 @@ public class RestClientTests { [InlineData(Method.Post, Method.Put)] [InlineData(Method.Get, Method.Delete)] public async Task Execute_with_IRestRequest_and_Method_overrides_previous_request_method(Method reqMethod, Method overrideMethod) { - var req = new RestRequest(reqMethod); + var req = new RestRequest("", reqMethod); var client = new RestClient(BaseUrl); await client.ExecuteAsync(req, overrideMethod); diff --git a/test/RestSharp.Tests/RestSharp.Tests.csproj b/test/RestSharp.Tests/RestSharp.Tests.csproj index 9db982d04..f29aad0a5 100644 --- a/test/RestSharp.Tests/RestSharp.Tests.csproj +++ b/test/RestSharp.Tests/RestSharp.Tests.csproj @@ -25,7 +25,4 @@ - - - \ No newline at end of file