//----------------------------------------------------------------------- // // Copyright (c) Andrew Arnott. All rights reserved. // //----------------------------------------------------------------------- namespace DotNetOAuth.ChannelElements { using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; using System.Text; using System.Web; using DotNetOAuth.Messaging; using DotNetOAuth.Messaging.Bindings; using DotNetOAuth.Messaging.Reflection; /// /// An OAuth-specific implementation of the class. /// internal class OAuthChannel : Channel { /// /// The object that will transmit instances /// and return their responses. /// private IWebRequestHandler webRequestHandler; /// /// Initializes a new instance of the class. /// /// The binding element to use for signing. /// The web application store to use for nonces. /// The token manager instance to use. internal OAuthChannel(ITamperProtectionChannelBindingElement signingBindingElement, INonceStore store, ITokenManager tokenManager) : this(signingBindingElement, store, new OAuthMessageTypeProvider(tokenManager), new StandardWebRequestHandler()) { } /// /// Initializes a new instance of the class. /// /// The binding element to use for signing. /// The web application store to use for nonces. /// /// An injected message type provider instance. /// Except for mock testing, this should always be . /// /// /// An instance to a that will be used when submitting HTTP /// requests and waiting for responses. /// /// /// This overload for testing purposes only. /// internal OAuthChannel(ITamperProtectionChannelBindingElement signingBindingElement, INonceStore store, IMessageTypeProvider messageTypeProvider, IWebRequestHandler webRequestHandler) : base(messageTypeProvider, new OAuthHttpMethodBindingElement(), signingBindingElement, new StandardExpirationBindingElement(), new StandardReplayProtectionBindingElement(store)) { if (webRequestHandler == null) { throw new ArgumentNullException("webRequestHandler"); } this.webRequestHandler = webRequestHandler; } /// /// Gets or sets the Consumer web application path. /// internal Uri Realm { get; set; } /// /// Encodes the names and values that are part of the message 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 GetEncodedParameters(IProtocolMessage message) { var encodedDictionary = new Dictionary(); EncodeParameters(new MessageDictionary(message), encodedDictionary); return encodedDictionary; } /// /// Encodes 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. internal static void EncodeParameters(IDictionary source, IDictionary destination) { if (source == null) { throw new ArgumentNullException("source"); } if (destination == null) { throw new ArgumentNullException("destination"); } foreach (var pair in source) { var key = Uri.EscapeDataString(pair.Key); var value = Uri.EscapeDataString(pair.Value); destination.Add(key, value); } } /// /// 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 initialized web request. internal HttpWebRequest InitializeRequest(IDirectedProtocolMessage request) { if (request == null) { throw new ArgumentNullException("request"); } PrepareMessageForSending(request); return this.InitializeRequestInternal(request); } /// /// Searches an incoming HTTP request for data that could be used to assemble /// a protocol request message. /// /// The HTTP request to search. /// A dictionary of data in the request. Should never be null, but may be empty. protected override IProtocolMessage ReadFromRequestInternal(HttpRequestInfo request) { if (request == null) { throw new ArgumentNullException("request"); } // First search the Authorization header. Use it exclusively if it's present. string authorization = request.Headers[HttpRequestHeader.Authorization]; if (authorization != null) { string[] authorizationSections = authorization.Split(';'); // TODO: is this the right delimiter? string oauthPrefix = Protocol.Default.AuthorizationHeaderScheme + " "; // The Authorization header may have multiple uses, and OAuth may be just one of them. // Go through each one looking for an OAuth one. foreach (string auth in authorizationSections) { string trimmedAuth = auth.Trim(); if (trimmedAuth.StartsWith(oauthPrefix, StringComparison.Ordinal)) { // We found an Authorization: OAuth header. // Parse it according to the rules in section 5.4.1 of the V1.0 spec. var fields = new Dictionary(); foreach (string stringPair in trimmedAuth.Substring(oauthPrefix.Length).Split(',')) { string[] keyValueStringPair = stringPair.Trim().Split('='); string key = Uri.UnescapeDataString(keyValueStringPair[0]); string value = Uri.UnescapeDataString(keyValueStringPair[1].Trim('"')); fields.Add(key, value); } return this.Receive(fields, request.GetRecipient()); } } } // We didn't find an OAuth authorization header. Revert to other payload methods. IProtocolMessage message = base.ReadFromRequestInternal(request); // Add receiving HTTP transport information required for signature generation. var signedMessage = message as ITamperResistantOAuthMessage; if (signedMessage != null) { signedMessage.Recipient = request.Url; signedMessage.HttpMethod = request.HttpMethod; } return message; } /// /// Gets the protocol message that may be in the given HTTP response stream. /// /// The response that is anticipated to contain an OAuth message. /// The deserialized message, if one is found. Null otherwise. protected override IProtocolMessage ReadFromResponseInternal(Stream responseStream) { if (responseStream == null) { throw new ArgumentNullException("responseStream"); } using (StreamReader reader = new StreamReader(responseStream)) { string response = reader.ReadToEnd(); var fields = HttpUtility.ParseQueryString(response).ToDictionary(); return Receive(fields, null); } } /// /// Sends a direct message to a remote party and waits for the response. /// /// The message to send. /// The remote party's response. protected override IProtocolMessage RequestInternal(IDirectedProtocolMessage request) { HttpWebRequest httpRequest = this.InitializeRequestInternal(request); Response response = this.webRequestHandler.GetResponse(httpRequest); if (response.ResponseStream == null) { return null; } var responseFields = HttpUtility.ParseQueryString(response.Body).ToDictionary(); Type messageType = this.MessageTypeProvider.GetResponseMessageType(request, responseFields); if (messageType == null) { return null; } var responseSerialize = MessageSerializer.Get(messageType); var responseMessage = responseSerialize.Deserialize(responseFields, null); return responseMessage; } /// /// 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 Response SendDirectMessageResponse(IProtocolMessage response) { MessageSerializer serializer = MessageSerializer.Get(response.GetType()); var fields = serializer.Serialize(response); string responseBody = MessagingUtilities.CreateQueryString(fields); Response encodedResponse = new Response { Body = responseBody, OriginalMessage = response, Status = HttpStatusCode.OK, Headers = new System.Net.WebHeaderCollection(), }; return encodedResponse; } /// /// Prepares to send a request to the Service Provider as the query string in a GET request. /// /// The message to be transmitted to the ServiceProvider. /// The web request ready to send. /// /// This method implements OAuth 1.0 section 5.2, item #3. /// private static HttpWebRequest InitializeRequestAsGet(IDirectedProtocolMessage requestMessage) { var serializer = MessageSerializer.Get(requestMessage.GetType()); var fields = serializer.Serialize(requestMessage); UriBuilder builder = new UriBuilder(requestMessage.Recipient); MessagingUtilities.AppendQueryArgs(builder, fields); HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(builder.Uri); return httpRequest; } /// /// Initializes a web request by attaching a message to it. /// /// The message to attach. /// The initialized web request. private HttpWebRequest InitializeRequestInternal(IDirectedProtocolMessage request) { if (request == null) { throw new ArgumentNullException("request"); } if (request.Recipient == null) { throw new ArgumentException(MessagingStrings.DirectedMessageMissingRecipient, "request"); } IOAuthDirectedMessage oauthRequest = request as IOAuthDirectedMessage; if (oauthRequest == null) { throw new ArgumentException( string.Format( CultureInfo.CurrentCulture, MessagingStrings.UnexpectedType, typeof(IOAuthDirectedMessage), request.GetType())); } HttpWebRequest httpRequest; HttpDeliveryMethod transmissionMethod = oauthRequest.HttpMethods; if ((transmissionMethod & HttpDeliveryMethod.AuthorizationHeaderRequest) != 0) { httpRequest = this.InitializeRequestAsAuthHeader(request); } else if ((transmissionMethod & HttpDeliveryMethod.PostRequest) != 0) { httpRequest = this.InitializeRequestAsPost(request); } else if ((transmissionMethod & HttpDeliveryMethod.GetRequest) != 0) { httpRequest = InitializeRequestAsGet(request); } else { throw new NotSupportedException(); } return httpRequest; } /// /// 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 HttpWebRequest InitializeRequestAsAuthHeader(IDirectedProtocolMessage requestMessage) { var protocol = Protocol.Lookup(requestMessage.ProtocolVersion); var dictionary = new MessageDictionary(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 builder = new UriBuilder(requestMessage.Recipient); MessagingUtilities.AppendQueryArgs(builder, requestMessage.ExtraData); HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(builder.Uri); StringBuilder authorization = new StringBuilder(); authorization.Append(protocol.AuthorizationHeaderScheme); authorization.Append(" "); foreach (var pair in fields) { string key = Uri.EscapeDataString(pair.Key); string value = Uri.EscapeDataString(pair.Value); authorization.Append(key); authorization.Append("=\""); authorization.Append(value); authorization.Append("\","); } authorization.Length--; // remove trailing comma httpRequest.Headers.Add(HttpRequestHeader.Authorization, authorization.ToString()); return httpRequest; } /// /// Prepares to send a request to the Service Provider as the payload of a POST request. /// /// The message to be transmitted to the ServiceProvider. /// The web request ready to send. /// /// This method implements OAuth 1.0 section 5.2, item #2. /// private HttpWebRequest InitializeRequestAsPost(IDirectedProtocolMessage requestMessage) { var serializer = MessageSerializer.Get(requestMessage.GetType()); var fields = serializer.Serialize(requestMessage); HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(requestMessage.Recipient); httpRequest.Method = "POST"; httpRequest.ContentType = "application/x-www-form-urlencoded"; string requestBody = MessagingUtilities.CreateQueryString(fields); httpRequest.ContentLength = requestBody.Length; using (TextWriter writer = this.webRequestHandler.GetRequestStream(httpRequest)) { writer.Write(requestBody); } return httpRequest; } } }