//-----------------------------------------------------------------------
//
// Copyright (c) Outercurve Foundation. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.OAuth.ChannelElements {
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using DotNetOpenAuth.Logging;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.Messaging.Bindings;
using DotNetOpenAuth.Messaging.Reflection;
using DotNetOpenAuth.OAuth.Messages;
using Validation;
using HttpRequestHeaders = DotNetOpenAuth.Messaging.HttpRequestHeaders;
///
/// An OAuth-specific implementation of the class.
///
internal abstract class OAuthChannel : Channel {
///
/// Initializes a new instance of the class.
///
/// The binding element to use for signing.
/// The ITokenManager instance to use.
/// The security settings.
/// An injected message type provider instance.
/// Except for mock testing, this should always be one of
/// OAuthConsumerMessageFactory or OAuthServiceProviderMessageFactory.
/// The binding elements.
/// The host factories.
[SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Diagnostics.Contracts.__ContractsRuntime.Requires(System.Boolean,System.String,System.String)", Justification = "Code contracts"), SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "securitySettings", Justification = "Code contracts")]
protected OAuthChannel(ITamperProtectionChannelBindingElement signingBindingElement, ITokenManager tokenManager, SecuritySettings securitySettings, IMessageFactory messageTypeProvider, IChannelBindingElement[] bindingElements, IHostFactories hostFactories = null)
: base(messageTypeProvider, bindingElements, hostFactories ?? new DefaultOAuthHostFactories()) {
Requires.NotNull(tokenManager, "tokenManager");
Requires.NotNull(securitySettings, "securitySettings");
Requires.NotNull(signingBindingElement, "signingBindingElement");
Requires.That(signingBindingElement.SignatureCallback == null, "signingBindingElement", OAuthStrings.SigningElementAlreadyAssociatedWithChannel);
Requires.NotNull(bindingElements, "bindingElements");
this.TokenManager = tokenManager;
signingBindingElement.SignatureCallback = this.SignatureCallback;
}
///
/// Gets or sets the Consumer web application path.
///
internal Uri Realm { get; set; }
///
/// Gets the token manager being used.
///
protected internal ITokenManager TokenManager { get; private set; }
///
/// Uri-escapes the names and values in a dictionary per OAuth 1.0 section 5.1.
///
/// The message with data to encode.
/// A dictionary of name-value pairs with their strings encoded.
internal static IDictionary GetUriEscapedParameters(IEnumerable> message) {
var encodedDictionary = new Dictionary();
UriEscapeParameters(message, encodedDictionary);
return encodedDictionary;
}
///
/// Initializes a web request for sending by attaching a message to it.
/// Use this method to prepare a protected resource request that you do NOT
/// expect an OAuth message response to.
///
/// The message to attach.
/// The cancellation token.
/// The initialized web request.
internal async Task InitializeRequestAsync(IDirectedProtocolMessage request, CancellationToken cancellationToken) {
Requires.NotNull(request, "request");
await this.ProcessOutgoingMessageAsync(request, cancellationToken);
return this.CreateHttpRequest(request);
}
///
/// Initializes the binding elements for the OAuth channel.
///
/// The signing binding element.
/// The nonce store.
///
/// An array of binding elements used to initialize the channel.
///
protected static List InitializeBindingElements(ITamperProtectionChannelBindingElement signingBindingElement, INonceStore store) {
var bindingElements = new List {
new OAuthHttpMethodBindingElement(),
signingBindingElement,
new StandardExpirationBindingElement(),
new StandardReplayProtectionBindingElement(store),
};
return bindingElements;
}
///
/// Searches an incoming HTTP request for data that could be used to assemble
/// a protocol request message.
///
/// The HTTP request to search.
/// The cancellation token.
/// The deserialized message, if one is found. Null otherwise.
protected override async Task ReadFromRequestCoreAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
// First search the Authorization header.
var authorization = request.Headers.Authorization;
var fields = MessagingUtilities.ParseAuthorizationHeader(Protocol.AuthorizationHeaderScheme, authorization).ToDictionary();
fields.Remove("realm"); // ignore the realm parameter, since we don't use it, and it must be omitted from signature base string.
// Scrape the entity
foreach (var pair in await ParseUrlEncodedFormContentAsync(request, cancellationToken)) {
if (pair.Key != null) {
fields.Add(pair.Key, pair.Value);
} else {
Logger.OAuth.WarnFormat("Ignoring query string parameter '{0}' since it isn't a standard name=value parameter.", pair.Value);
}
}
// Scrape the query string
var qs = HttpUtility.ParseQueryString(request.RequestUri.Query);
foreach (string key in qs) {
if (key != null) {
fields.Add(key, qs[key]);
} else {
Logger.OAuth.WarnFormat("Ignoring query string parameter '{0}' since it isn't a standard name=value parameter.", qs[key]);
}
}
MessageReceivingEndpoint recipient;
try {
recipient = request.GetRecipient();
} catch (ArgumentException ex) {
Logger.OAuth.WarnFormat("Unrecognized HTTP request: " + ex.ToString());
return null;
}
// Deserialize the message using all the data we've collected.
var message = (IDirectedProtocolMessage)this.Receive(fields, recipient);
// Add receiving HTTP transport information required for signature generation.
var signedMessage = message as ITamperResistantOAuthMessage;
if (signedMessage != null) {
signedMessage.Recipient = request.RequestUri;
signedMessage.HttpMethod = request.Method;
}
return message;
}
///
/// Gets the protocol message that may be in the given HTTP response.
///
/// The response that is anticipated to contain an protocol message.
/// The cancellation token.
///
/// The deserialized message parts, if found. Null otherwise.
///
protected override async Task> ReadFromResponseCoreAsync(HttpResponseMessage response, CancellationToken cancellationToken) {
string body = await response.Content.ReadAsStringAsync();
return HttpUtility.ParseQueryString(body).ToDictionary();
}
///
/// Prepares an HTTP request that carries a given message.
///
/// The message to send.
///
/// The prepared to send the request.
///
protected override HttpRequestMessage CreateHttpRequest(IDirectedProtocolMessage request) {
HttpRequestMessage httpRequest;
HttpDeliveryMethods transmissionMethod = request.HttpMethods;
if ((transmissionMethod & HttpDeliveryMethods.AuthorizationHeaderRequest) != 0) {
httpRequest = this.InitializeRequestAsAuthHeader(request);
} else if ((transmissionMethod & HttpDeliveryMethods.PostRequest) != 0) {
var requestMessageWithBinaryData = request as IMessageWithBinaryData;
ErrorUtilities.VerifyProtocol(requestMessageWithBinaryData == null || !requestMessageWithBinaryData.SendAsMultipart, OAuthStrings.MultipartPostMustBeUsedWithAuthHeader);
httpRequest = this.InitializeRequestAsPost(request);
} else if ((transmissionMethod & HttpDeliveryMethods.GetRequest) != 0) {
httpRequest = InitializeRequestAsGet(request);
} else if ((transmissionMethod & HttpDeliveryMethods.HeadRequest) != 0) {
httpRequest = InitializeRequestAsHead(request);
} else if ((transmissionMethod & HttpDeliveryMethods.PutRequest) != 0) {
httpRequest = this.InitializeRequestAsPut(request);
} else if ((transmissionMethod & HttpDeliveryMethods.DeleteRequest) != 0) {
httpRequest = InitializeRequestAsDelete(request);
} else {
throw new NotSupportedException();
}
return httpRequest;
}
///
/// Queues a message for sending in the response stream where the fields
/// are sent in the response stream in querystring style.
///
/// The message to send as a response.
/// The pending user agent redirect based message to be sent as an HttpResponse.
///
/// This method implements spec V1.0 section 5.3.
///
protected override HttpResponseMessage PrepareDirectResponse(IProtocolMessage response) {
var messageAccessor = this.MessageDescriptions.GetAccessor(response);
var fields = messageAccessor.Serialize();
var encodedResponse = new HttpResponseMessage {
Content = new FormUrlEncodedContent(fields),
};
ApplyMessageTemplate(response, encodedResponse);
return encodedResponse;
}
///
/// Gets the consumer secret for a given consumer key.
///
/// The consumer key.
/// A consumer secret.
protected abstract string GetConsumerSecret(string consumerKey);
///
/// Uri-escapes the names and values in a dictionary per OAuth 1.0 section 5.1.
///
/// The dictionary with names and values to encode.
/// The dictionary to add the encoded pairs to.
private static void UriEscapeParameters(IEnumerable> source, IDictionary destination) {
Requires.NotNull(source, "source");
Requires.NotNull(destination, "destination");
foreach (var pair in source) {
var key = MessagingUtilities.EscapeUriDataStringRfc3986(pair.Key);
var value = MessagingUtilities.EscapeUriDataStringRfc3986(pair.Value);
destination.Add(key, value);
}
}
///
/// Gets the HTTP method to use for a message.
///
/// The message.
/// "POST", "GET" or some other similar http verb.
private static HttpMethod GetHttpMethod(IDirectedProtocolMessage message) {
Requires.NotNull(message, "message");
var signedMessage = message as ITamperResistantOAuthMessage;
if (signedMessage != null) {
return signedMessage.HttpMethod;
} else {
return MessagingUtilities.GetHttpVerb(message.HttpMethods);
}
}
///
/// Prepares to send a request to the Service Provider via the Authorization header.
///
/// The message to be transmitted to the ServiceProvider.
/// The web request ready to send.
///
/// This method implements OAuth 1.0 section 5.2, item #1 (described in section 5.4).
///
private HttpRequestMessage InitializeRequestAsAuthHeader(IDirectedProtocolMessage requestMessage) {
var dictionary = this.MessageDescriptions.GetAccessor(requestMessage);
// copy so as to not modify original
var fields = new Dictionary();
foreach (string key in dictionary.DeclaredKeys) {
fields.Add(key, dictionary[key]);
}
if (this.Realm != null) {
fields.Add("realm", this.Realm.AbsoluteUri);
}
UriBuilder recipientBuilder = new UriBuilder(requestMessage.Recipient);
bool hasEntity = HttpMethodHasEntity(GetHttpMethod(requestMessage));
if (!hasEntity) {
MessagingUtilities.AppendQueryArgs(recipientBuilder, requestMessage.ExtraData);
}
var httpRequest = new HttpRequestMessage(GetHttpMethod(requestMessage), recipientBuilder.Uri);
this.PrepareHttpWebRequest(httpRequest);
httpRequest.Headers.Authorization = new AuthenticationHeaderValue(Protocol.AuthorizationHeaderScheme, MessagingUtilities.AssembleAuthorizationHeader(fields));
if (hasEntity) {
var requestMessageWithBinaryData = requestMessage as IMessageWithBinaryData;
if (requestMessageWithBinaryData != null && requestMessageWithBinaryData.SendAsMultipart) {
// Include the binary data in the multipart entity, and any standard text extra message data.
// The standard declared message parts are included in the authorization header.
var content = InitializeMultipartFormDataContent(requestMessageWithBinaryData);
httpRequest.Content = content;
foreach (var extraData in requestMessage.ExtraData) {
content.Add(new StringContent(extraData.Value), extraData.Key);
}
} else {
ErrorUtilities.VerifyProtocol(requestMessageWithBinaryData == null || requestMessageWithBinaryData.BinaryData.Count == 0, MessagingStrings.BinaryDataRequiresMultipart);
if (requestMessage.ExtraData.Count > 0) {
httpRequest.Content = new FormUrlEncodedContent(requestMessage.ExtraData);
}
}
}
return httpRequest;
}
///
/// Fills out the secrets in a message so that signing/verification can be performed.
///
/// The message about to be signed or whose signature is about to be verified.
private void SignatureCallback(ITamperResistantProtocolMessage message) {
var oauthMessage = message as ITamperResistantOAuthMessage;
try {
Logger.Channel.Debug("Applying secrets to message to prepare for signing or signature verification.");
oauthMessage.ConsumerSecret = this.GetConsumerSecret(oauthMessage.ConsumerKey);
var tokenMessage = message as ITokenContainingMessage;
if (tokenMessage != null) {
oauthMessage.TokenSecret = this.TokenManager.GetTokenSecret(tokenMessage.Token);
}
} catch (KeyNotFoundException ex) {
throw new ProtocolException(OAuthStrings.ConsumerOrTokenSecretNotFound, ex);
}
}
}
}