diff options
Diffstat (limited to 'src')
49 files changed, 710 insertions, 176 deletions
diff --git a/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.csproj b/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.csproj index daf6327..3f76f78 100644 --- a/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.csproj +++ b/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.csproj @@ -80,7 +80,9 @@ <Reference Include="Microsoft.Build.Utilities.v3.5"> <RequiredTargetFramework>3.5</RequiredTargetFramework> </Reference> - <Reference Include="Microsoft.Contracts, Version=1.0.0.0, Culture=neutral, PublicKeyToken=736440c9b414ea16, processorArchitecture=MSIL" /> + <Reference Include="Microsoft.Contracts, Version=1.0.0.0, Culture=neutral, PublicKeyToken=736440c9b414ea16, processorArchitecture=MSIL"> + <Private>False</Private> + </Reference> <Reference Include="Microsoft.Web.Administration, Version=7.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"> <SpecificVersion>False</SpecificVersion> <HintPath>$(SystemRoot)\System32\inetsrv\Microsoft.Web.Administration.dll</HintPath> diff --git a/src/DotNetOpenAuth.BuildTasks/GetBuildVersion.cs b/src/DotNetOpenAuth.BuildTasks/GetBuildVersion.cs index 48b5d5c..23db9a6 100644 --- a/src/DotNetOpenAuth.BuildTasks/GetBuildVersion.cs +++ b/src/DotNetOpenAuth.BuildTasks/GetBuildVersion.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; using System.Linq; using System.Text; -using Microsoft.Build.Utilities; using Microsoft.Build.Framework; -using System.IO; +using Microsoft.Build.Utilities; namespace DotNetOpenAuth.BuildTasks { public class GetBuildVersion : Task { @@ -52,18 +54,54 @@ namespace DotNetOpenAuth.BuildTasks { return string.Empty; } - string headContent = string.Empty; + string commitId = string.Empty; + + // First try asking Git for the HEAD commit id + try { + string cmdPath = Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe"); + ProcessStartInfo psi = new ProcessStartInfo(cmdPath, "/c git rev-parse HEAD"); + psi.WindowStyle = ProcessWindowStyle.Hidden; + psi.RedirectStandardOutput = true; + psi.UseShellExecute = false; + Process git = Process.Start(psi); + commitId = git.StandardOutput.ReadLine(); + git.WaitForExit(); + if (git.ExitCode != 0) { + commitId = string.Empty; + } + if (commitId != null) { + commitId = commitId.Trim(); + if (commitId.Length == 40) { + return commitId; + } + } + } catch (InvalidOperationException) { + } catch (Win32Exception) { + } + + // Failing being able to use the git command to figure out the HEAD commit ID, try the filesystem directly. try { - headContent = File.ReadAllText(Path.Combine(this.GitRepoRoot, @".git/HEAD")).Trim(); + string headContent = File.ReadAllText(Path.Combine(this.GitRepoRoot, @".git/HEAD")).Trim(); if (headContent.StartsWith("ref:", StringComparison.Ordinal)) { string refName = headContent.Substring(5).Trim(); - headContent = File.ReadAllText(Path.Combine(this.GitRepoRoot, @".git/" + refName)).Trim(); + string refPath = Path.Combine(this.GitRepoRoot, ".git/" + refName); + if (File.Exists(refPath)) { + commitId = File.ReadAllText(refPath).Trim(); + } else { + string packedRefPath = Path.Combine(this.GitRepoRoot, ".git/packed-refs"); + string matchingLine = File.ReadAllLines(packedRefPath).FirstOrDefault(line => line.EndsWith(refName)); + if (matchingLine != null) { + commitId = matchingLine.Substring(0, matchingLine.IndexOf(' ')); + } + } + } else { + commitId = headContent; } } catch (FileNotFoundException) { } catch (DirectoryNotFoundException) { } - return headContent.Trim(); + return commitId.Trim(); } private Version ReadVersionFromFile() { diff --git a/src/DotNetOpenAuth.Test/Messaging/MessagingUtilitiesTests.cs b/src/DotNetOpenAuth.Test/Messaging/MessagingUtilitiesTests.cs index ce0928e..8a8ae25 100644 --- a/src/DotNetOpenAuth.Test/Messaging/MessagingUtilitiesTests.cs +++ b/src/DotNetOpenAuth.Test/Messaging/MessagingUtilitiesTests.cs @@ -11,8 +11,10 @@ namespace DotNetOpenAuth.Test.Messaging using System.Collections.Specialized; using System.IO; using System.Net; + using System.Text.RegularExpressions; using System.Web; using DotNetOpenAuth.Messaging; + using DotNetOpenAuth.Test.Mocks; using NUnit.Framework; [TestFixture] @@ -139,5 +141,79 @@ namespace DotNetOpenAuth.Test.Messaging Assert.AreEqual("%C2%80", MessagingUtilities.EscapeUriDataStringRfc3986("\u0080")); Assert.AreEqual("%E3%80%81", MessagingUtilities.EscapeUriDataStringRfc3986("\u3001")); } + + /// <summary> + /// Verifies the overall format of the multipart POST is correct. + /// </summary> + [TestCase] + public void PostMultipart() { + var httpHandler = new TestWebRequestHandler(); + bool callbackTriggered = false; + httpHandler.Callback = req => { + Match m = Regex.Match(req.ContentType, "multipart/form-data; boundary=(.+)"); + Assert.IsTrue(m.Success, "Content-Type HTTP header not set correctly."); + string boundary = m.Groups[1].Value; + string expectedEntity = "--{0}\r\nContent-Disposition: form-data; name=\"a\"\r\n\r\nb\r\n--{0}--\r\n"; + expectedEntity = string.Format(expectedEntity, boundary); + string actualEntity = httpHandler.RequestEntityAsString; + Assert.AreEqual(expectedEntity, actualEntity); + callbackTriggered = true; + Assert.AreEqual(req.ContentLength, actualEntity.Length); + IncomingWebResponse resp = new CachedDirectWebResponse(); + return resp; + }; + var request = (HttpWebRequest)WebRequest.Create("http://someserver"); + var parts = new[] { + MultipartPostPart.CreateFormPart("a", "b"), + }; + request.PostMultipart(httpHandler, parts); + Assert.IsTrue(callbackTriggered); + } + + /// <summary> + /// Verifies proper behavior of GetHttpVerb + /// </summary> + [TestCase] + public void GetHttpVerbTest() { + Assert.AreEqual("GET", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.GetRequest)); + Assert.AreEqual("POST", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PostRequest)); + Assert.AreEqual("HEAD", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.HeadRequest)); + Assert.AreEqual("DELETE", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.DeleteRequest)); + Assert.AreEqual("PUT", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PutRequest)); + + Assert.AreEqual("GET", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.AuthorizationHeaderRequest)); + Assert.AreEqual("POST", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest)); + Assert.AreEqual("HEAD", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.HeadRequest | HttpDeliveryMethods.AuthorizationHeaderRequest)); + Assert.AreEqual("DELETE", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.DeleteRequest | HttpDeliveryMethods.AuthorizationHeaderRequest)); + Assert.AreEqual("PUT", MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PutRequest | HttpDeliveryMethods.AuthorizationHeaderRequest)); + } + + /// <summary> + /// Verifies proper behavior of GetHttpVerb on invalid input. + /// </summary> + [TestCase, ExpectedException(typeof(ArgumentException))] + public void GetHttpVerbOutOfRangeTest() { + MessagingUtilities.GetHttpVerb(HttpDeliveryMethods.PutRequest | HttpDeliveryMethods.PostRequest); + } + + /// <summary> + /// Verifies proper behavior of GetHttpDeliveryMethod + /// </summary> + [TestCase] + public void GetHttpDeliveryMethodTest() { + Assert.AreEqual(HttpDeliveryMethods.GetRequest, MessagingUtilities.GetHttpDeliveryMethod("GET")); + Assert.AreEqual(HttpDeliveryMethods.PostRequest, MessagingUtilities.GetHttpDeliveryMethod("POST")); + Assert.AreEqual(HttpDeliveryMethods.HeadRequest, MessagingUtilities.GetHttpDeliveryMethod("HEAD")); + Assert.AreEqual(HttpDeliveryMethods.PutRequest, MessagingUtilities.GetHttpDeliveryMethod("PUT")); + Assert.AreEqual(HttpDeliveryMethods.DeleteRequest, MessagingUtilities.GetHttpDeliveryMethod("DELETE")); + } + + /// <summary> + /// Verifies proper behavior of GetHttpDeliveryMethod for an unexpected input + /// </summary> + [TestCase, ExpectedException(typeof(ArgumentException))] + public void GetHttpDeliveryMethodOutOfRangeTest() { + MessagingUtilities.GetHttpDeliveryMethod("UNRECOGNIZED"); + } } } diff --git a/src/DotNetOpenAuth.Test/Messaging/MultiPartPostPartTests.cs b/src/DotNetOpenAuth.Test/Messaging/MultiPartPostPartTests.cs deleted file mode 100644 index a293895..0000000 --- a/src/DotNetOpenAuth.Test/Messaging/MultiPartPostPartTests.cs +++ /dev/null @@ -1,99 +0,0 @@ -//----------------------------------------------------------------------- -// <copyright file="MultipartPostPartTests.cs" company="Andrew Arnott"> -// Copyright (c) Andrew Arnott. All rights reserved. -// </copyright> -//----------------------------------------------------------------------- - -namespace DotNetOpenAuth.Test.Messaging { - using System.CodeDom.Compiler; - using System.Collections.Generic; - using System.Diagnostics.Contracts; - using System.IO; - using System.Net; - using DotNetOpenAuth.Messaging; - using NUnit.Framework; - - [TestFixture] - public class MultipartPostPartTests : TestBase { - /// <summary> - /// Verifies that the Length property matches the length actually serialized. - /// </summary> - [TestCase] - public void FormDataSerializeMatchesLength() { - var part = MultipartPostPart.CreateFormPart("a", "b"); - VerifyLength(part); - } - - /// <summary> - /// Verifies that the length property matches the length actually serialized. - /// </summary> - [TestCase] - public void FileSerializeMatchesLength() { - using (TempFileCollection tfc = new TempFileCollection()) { - string file = tfc.AddExtension(".txt"); - File.WriteAllText(file, "sometext"); - var part = MultipartPostPart.CreateFormFilePart("someformname", file, "text/plain"); - VerifyLength(part); - } - } - - /// <summary> - /// Verifies MultiPartPost sends the right number of bytes. - /// </summary> - [TestCase] - public void MultiPartPostAscii() { - using (TempFileCollection tfc = new TempFileCollection()) { - string file = tfc.AddExtension("txt"); - File.WriteAllText(file, "sometext"); - this.VerifyFullPost(new List<MultipartPostPart> { - MultipartPostPart.CreateFormPart("a", "b"), - MultipartPostPart.CreateFormFilePart("SomeFormField", file, "text/plain"), - }); - } - } - - /// <summary> - /// Verifies MultiPartPost sends the right number of bytes. - /// </summary> - [TestCase] - public void MultiPartPostMultiByteCharacters() { - using (TempFileCollection tfc = new TempFileCollection()) { - string file = tfc.AddExtension("txt"); - File.WriteAllText(file, "\x1020\x818"); - this.VerifyFullPost(new List<MultipartPostPart> { - MultipartPostPart.CreateFormPart("a", "\x987"), - MultipartPostPart.CreateFormFilePart("SomeFormField", file, "text/plain"), - }); - } - } - - private static void VerifyLength(MultipartPostPart part) { - Contract.Requires(part != null); - - var expectedLength = part.Length; - var ms = new MemoryStream(); - var sw = new StreamWriter(ms); - part.Serialize(sw); - sw.Flush(); - var actualLength = ms.Length; - Assert.AreEqual(expectedLength, actualLength); - } - - private void VerifyFullPost(List<MultipartPostPart> parts) { - var request = (HttpWebRequest)WebRequest.Create("http://localhost"); - var handler = new Mocks.TestWebRequestHandler(); - bool posted = false; - handler.Callback = req => { - foreach (string header in req.Headers) { - TestUtilities.TestLogger.InfoFormat("{0}: {1}", header, req.Headers[header]); - } - TestUtilities.TestLogger.InfoFormat(handler.RequestEntityAsString); - Assert.AreEqual(req.ContentLength, handler.RequestEntityStream.Length); - posted = true; - return null; - }; - request.PostMultipart(handler, parts); - Assert.IsTrue(posted, "HTTP POST never sent."); - } - } -} diff --git a/src/DotNetOpenAuth.Test/Messaging/MultipartPostPartTests.cs b/src/DotNetOpenAuth.Test/Messaging/MultipartPostPartTests.cs index a293895..08524b2 100644 --- a/src/DotNetOpenAuth.Test/Messaging/MultipartPostPartTests.cs +++ b/src/DotNetOpenAuth.Test/Messaging/MultipartPostPartTests.cs @@ -38,6 +38,15 @@ namespace DotNetOpenAuth.Test.Messaging { } /// <summary> + /// Verifies file multiparts identify themselves as files and not merely form-data. + /// </summary> + [TestCase] + public void FilePartAsFile() { + var part = MultipartPostPart.CreateFormFilePart("somename", "somefile", "plain/text", new MemoryStream()); + Assert.AreEqual("file", part.ContentDisposition); + } + + /// <summary> /// Verifies MultiPartPost sends the right number of bytes. /// </summary> [TestCase] diff --git a/src/DotNetOpenAuth.Test/Mocks/TestDirectedMessage.cs b/src/DotNetOpenAuth.Test/Mocks/TestDirectedMessage.cs index 342a303..1b3c2b2 100644 --- a/src/DotNetOpenAuth.Test/Mocks/TestDirectedMessage.cs +++ b/src/DotNetOpenAuth.Test/Mocks/TestDirectedMessage.cs @@ -11,11 +11,11 @@ namespace DotNetOpenAuth.Test.Mocks { internal class TestDirectedMessage : TestMessage, IDirectedProtocolMessage { internal TestDirectedMessage() { - this.HttpMethods = HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.PostRequest; + this.HttpMethods = HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.HeadRequest; } internal TestDirectedMessage(MessageTransport transport) : base(transport) { - this.HttpMethods = HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.PostRequest; + this.HttpMethods = HttpDeliveryMethods.GetRequest | HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.HeadRequest; } #region IDirectedProtocolMessage Members diff --git a/src/DotNetOpenAuth.Test/OAuth/ChannelElements/OAuthChannelTests.cs b/src/DotNetOpenAuth.Test/OAuth/ChannelElements/OAuthChannelTests.cs index 5f068d1..c7b9f89 100644 --- a/src/DotNetOpenAuth.Test/OAuth/ChannelElements/OAuthChannelTests.cs +++ b/src/DotNetOpenAuth.Test/OAuth/ChannelElements/OAuthChannelTests.cs @@ -227,6 +227,11 @@ namespace DotNetOpenAuth.Test.ChannelElements { this.ParameterizedRequestTest(HttpDeliveryMethods.PostRequest); } + [TestCase] + public void RequestUsingHead() { + this.ParameterizedRequestTest(HttpDeliveryMethods.HeadRequest); + } + /// <summary> /// Verifies that messages asking for special HTTP status codes get them. /// </summary> @@ -323,7 +328,7 @@ namespace DotNetOpenAuth.Test.ChannelElements { this.webRequestHandler.Callback = (req) => { Assert.IsNotNull(req); HttpRequestInfo reqInfo = ConvertToRequestInfo(req, this.webRequestHandler.RequestEntityStream); - Assert.AreEqual(scheme == HttpDeliveryMethods.PostRequest ? "POST" : "GET", reqInfo.HttpMethod); + Assert.AreEqual(MessagingUtilities.GetHttpVerb(scheme), reqInfo.HttpMethod); var incomingMessage = this.channel.ReadFromRequest(reqInfo) as TestMessage; Assert.IsNotNull(incomingMessage); Assert.AreEqual(request.Age, incomingMessage.Age); diff --git a/src/DotNetOpenAuth.sln b/src/DotNetOpenAuth.sln index d55996c..55d43e8 100644 --- a/src/DotNetOpenAuth.sln +++ b/src/DotNetOpenAuth.sln @@ -185,6 +185,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIdWebRingSsoRelyingPart EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIdWebRingSsoProvider", "..\samples\OpenIdWebRingSsoProvider\OpenIdWebRingSsoProvider.csproj", "{0B4EB2A8-283D-48FB-BCD0-85B8DFFE05E4}" EndProject +Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "OpenIdRelyingPartyWebFormsVB", "..\samples\OpenIdRelyingPartyWebFormsVB\OpenIdRelyingPartyWebFormsVB.vbproj", "{F289B925-4307-4BEC-B411-885CE70E3379}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution CodeAnalysis|Any CPU = CodeAnalysis|Any CPU @@ -315,6 +317,12 @@ Global {0B4EB2A8-283D-48FB-BCD0-85B8DFFE05E4}.Debug|Any CPU.Build.0 = Debug|Any CPU {0B4EB2A8-283D-48FB-BCD0-85B8DFFE05E4}.Release|Any CPU.ActiveCfg = Release|Any CPU {0B4EB2A8-283D-48FB-BCD0-85B8DFFE05E4}.Release|Any CPU.Build.0 = Release|Any CPU + {F289B925-4307-4BEC-B411-885CE70E3379}.CodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU + {F289B925-4307-4BEC-B411-885CE70E3379}.CodeAnalysis|Any CPU.Build.0 = Release|Any CPU + {F289B925-4307-4BEC-B411-885CE70E3379}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F289B925-4307-4BEC-B411-885CE70E3379}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F289B925-4307-4BEC-B411-885CE70E3379}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F289B925-4307-4BEC-B411-885CE70E3379}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -332,6 +340,8 @@ Global {BBACD972-014D-478F-9B07-56B9E1D4CC73} = {034D5B5B-7D00-4A9D-8AFE-4A476E0575B1} {B64A1E7E-6A15-4B91-AF13-7D48F7DA5942} = {034D5B5B-7D00-4A9D-8AFE-4A476E0575B1} {0B4EB2A8-283D-48FB-BCD0-85B8DFFE05E4} = {034D5B5B-7D00-4A9D-8AFE-4A476E0575B1} + {6EB90284-BD15-461C-BBF2-131CF55F7C8B} = {8A5CEDB9-7F8A-4BE2-A1B9-97130F453277} + {F289B925-4307-4BEC-B411-885CE70E3379} = {034D5B5B-7D00-4A9D-8AFE-4A476E0575B1} {6EC36418-DBC5-4AD1-A402-413604AA7A08} = {1E2CBAA5-60A3-4AED-912E-541F5753CDC6} {9ADBE36D-9960-48F6-82E9-B4AC559E9AC3} = {1E2CBAA5-60A3-4AED-912E-541F5753CDC6} {7ADCCD5C-AC2B-4340-9410-FE3A31A48191} = {1E2CBAA5-60A3-4AED-912E-541F5753CDC6} diff --git a/src/DotNetOpenAuth/Configuration/TypeConfigurationElement.cs b/src/DotNetOpenAuth/Configuration/TypeConfigurationElement.cs index 5ac88da..0335af5 100644 --- a/src/DotNetOpenAuth/Configuration/TypeConfigurationElement.cs +++ b/src/DotNetOpenAuth/Configuration/TypeConfigurationElement.cs @@ -126,6 +126,7 @@ namespace DotNetOpenAuth.Configuration { /// be present. /// </remarks> private static T CreateInstanceFromXaml(Stream xaml) { + Contract.Ensures(Contract.Result<T>() != null); return (T)XamlReader.Load(xaml); } } diff --git a/src/DotNetOpenAuth/DotNetOpenAuth.csproj b/src/DotNetOpenAuth/DotNetOpenAuth.csproj index ee271f3..3bfecae 100644 --- a/src/DotNetOpenAuth/DotNetOpenAuth.csproj +++ b/src/DotNetOpenAuth/DotNetOpenAuth.csproj @@ -327,6 +327,7 @@ http://opensource.org/licenses/ms-pl.html <Compile Include="OAuth\ConsumerSecuritySettings.cs" /> <Compile Include="OAuth\DesktopConsumer.cs" /> <Compile Include="GlobalSuppressions.cs" /> + <Compile Include="Messaging\IMessageWithBinaryData.cs" /> <Compile Include="OAuth\Messages\ITokenSecretContainingMessage.cs" /> <Compile Include="Messaging\ChannelEventArgs.cs" /> <Compile Include="Messaging\ITamperProtectionChannelBindingElement.cs" /> diff --git a/src/DotNetOpenAuth/GlobalSuppressions.cs b/src/DotNetOpenAuth/GlobalSuppressions.cs index a3343ba..e436846 100644 --- a/src/DotNetOpenAuth/GlobalSuppressions.cs +++ b/src/DotNetOpenAuth/GlobalSuppressions.cs @@ -56,3 +56,4 @@ [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "DotNetOpenAuth.OpenId.Behaviors.AXFetchAsSregTransform.#DotNetOpenAuth.OpenId.Provider.IProviderBehavior.OnOutgoingResponse(DotNetOpenAuth.OpenId.Provider.IAuthenticationRequest)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "DotNetOpenAuth.OpenId.Behaviors.AXFetchAsSregTransform.#DotNetOpenAuth.OpenId.Provider.IProviderBehavior.OnIncomingRequest(DotNetOpenAuth.OpenId.Provider.IRequest)")] [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Scope = "member", Target = "DotNetOpenAuth.OpenId.Behaviors.AXFetchAsSregTransform.#DotNetOpenAuth.OpenId.Provider.IProviderBehavior.ApplySecuritySettings(DotNetOpenAuth.OpenId.Provider.ProviderSecuritySettings)")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2243:AttributeStringLiteralsShouldParseCorrectly")] diff --git a/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs b/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs index b37c93a..dd34d90 100644 --- a/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs +++ b/src/DotNetOpenAuth/Messaging/CachedDirectWebResponse.cs @@ -153,10 +153,13 @@ namespace DotNetOpenAuth.Messaging { /// <returns>The seekable Stream instance that contains a copy of what was returned in the HTTP response.</returns> private static MemoryStream CacheNetworkStreamAndClose(HttpWebResponse response, int maximumBytesToRead) { Contract.Requires<ArgumentNullException>(response != null); + Contract.Ensures(Contract.Result<MemoryStream>() != null); // Now read and cache the network stream Stream networkStream = response.GetResponseStream(); MemoryStream cachedStream = new MemoryStream(response.ContentLength < 0 ? 4 * 1024 : Math.Min((int)response.ContentLength, maximumBytesToRead)); + Contract.Assume(networkStream.CanRead, "HttpWebResponse.GetResponseStream() always returns a readable stream."); // CC missing + Contract.Assume(cachedStream.CanWrite, "This is a MemoryStream -- it's always writable."); // CC missing networkStream.CopyTo(cachedStream); cachedStream.Seek(0, SeekOrigin.Begin); diff --git a/src/DotNetOpenAuth/Messaging/Channel.cs b/src/DotNetOpenAuth/Messaging/Channel.cs index ef6486a..1bddf02 100644 --- a/src/DotNetOpenAuth/Messaging/Channel.cs +++ b/src/DotNetOpenAuth/Messaging/Channel.cs @@ -580,7 +580,15 @@ namespace DotNetOpenAuth.Messaging { fields = request.QueryStringBeforeRewriting.ToDictionary(); } - return (IDirectedProtocolMessage)this.Receive(fields, request.GetRecipient()); + MessageReceivingEndpoint recipient; + try { + recipient = request.GetRecipient(); + } catch (ArgumentException ex) { + Logger.Messaging.WarnFormat("Unrecognized HTTP request: " + ex.ToString()); + return null; + } + + return (IDirectedProtocolMessage)this.Receive(fields, recipient); } /// <summary> @@ -819,6 +827,24 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Prepares to send a request to the Service Provider as the query string in a HEAD request. + /// </summary> + /// <param name="requestMessage">The message to be transmitted to the ServiceProvider.</param> + /// <returns>The web request ready to send.</returns> + /// <remarks> + /// This method is simply a standard HTTP HEAD request with the message parts serialized to the query string. + /// This method satisfies OAuth 1.0 section 5.2, item #3. + /// </remarks> + protected virtual HttpWebRequest InitializeRequestAsHead(IDirectedProtocolMessage requestMessage) { + Contract.Requires<ArgumentNullException>(requestMessage != null); + Contract.Requires<ArgumentException>(requestMessage.Recipient != null, MessagingStrings.DirectedMessageMissingRecipient); + + HttpWebRequest request = this.InitializeRequestAsGet(requestMessage); + request.Method = "HEAD"; + return request; + } + + /// <summary> /// Prepares to send a request to the Service Provider as the payload of a POST request. /// </summary> /// <param name="requestMessage">The message to be transmitted to the ServiceProvider.</param> @@ -838,7 +864,18 @@ namespace DotNetOpenAuth.Messaging { HttpWebRequest httpRequest = (HttpWebRequest)WebRequest.Create(requestMessage.Recipient); httpRequest.CachePolicy = this.CachePolicy; httpRequest.Method = "POST"; - this.SendParametersInEntity(httpRequest, fields); + + var requestMessageWithBinaryData = requestMessage as IMessageWithBinaryData; + if (requestMessageWithBinaryData != null && requestMessageWithBinaryData.SendAsMultipart) { + var multiPartFields = new List<MultipartPostPart>(requestMessageWithBinaryData.BinaryData); + + // When sending multi-part, all data gets send as multi-part -- even the non-binary data. + multiPartFields.AddRange(fields.Select(field => MultipartPostPart.CreateFormPart(field.Key, field.Value))); + this.SendParametersInEntityAsMultipart(httpRequest, multiPartFields); + } else { + ErrorUtilities.VerifyProtocol(requestMessageWithBinaryData == null || requestMessageWithBinaryData.BinaryData.Count == 0, MessagingStrings.BinaryDataRequiresMultipart); + this.SendParametersInEntity(httpRequest, fields); + } return httpRequest; } @@ -914,6 +951,19 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Sends the given parameters in the entity stream of an HTTP request in multi-part format. + /// </summary> + /// <param name="httpRequest">The HTTP request.</param> + /// <param name="fields">The parameters to send.</param> + /// <remarks> + /// This method calls <see cref="HttpWebRequest.GetRequestStream()"/> and closes + /// the request stream, but does not call <see cref="HttpWebRequest.GetResponse"/>. + /// </remarks> + protected void SendParametersInEntityAsMultipart(HttpWebRequest httpRequest, IEnumerable<MultipartPostPart> fields) { + httpRequest.PostMultipartNoGetResponse(this.WebRequestHandler, fields); + } + + /// <summary> /// Verifies the integrity and applicability of an incoming message. /// </summary> /// <param name="message">The message just received.</param> diff --git a/src/DotNetOpenAuth/Messaging/EmptyDictionary.cs b/src/DotNetOpenAuth/Messaging/EmptyDictionary.cs index caf292a..9db5169 100644 --- a/src/DotNetOpenAuth/Messaging/EmptyDictionary.cs +++ b/src/DotNetOpenAuth/Messaging/EmptyDictionary.cs @@ -154,7 +154,7 @@ namespace DotNetOpenAuth.Messaging { /// <exception cref="T:System.NotSupportedException"> /// The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only. /// </exception> - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Code Contracts generates the code FxCop is complaining about.")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Code Contracts ccrewrite does this.")] public void Add(KeyValuePair<TKey, TValue> item) { throw new NotSupportedException(); } diff --git a/src/DotNetOpenAuth/Messaging/EmptyList.cs b/src/DotNetOpenAuth/Messaging/EmptyList.cs index ba918a4..68cdabd 100644 --- a/src/DotNetOpenAuth/Messaging/EmptyList.cs +++ b/src/DotNetOpenAuth/Messaging/EmptyList.cs @@ -99,7 +99,7 @@ namespace DotNetOpenAuth.Messaging { /// <exception cref="T:System.NotSupportedException"> /// The <see cref="T:System.Collections.Generic.IList`1"/> is read-only. /// </exception> - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Code Contracts generates the code FxCop is complaining about.")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Code Contracts ccrewrite does this.")] public void RemoveAt(int index) { throw new ArgumentOutOfRangeException("index"); } @@ -115,7 +115,7 @@ namespace DotNetOpenAuth.Messaging { /// <exception cref="T:System.NotSupportedException"> /// The <see cref="T:System.Collections.Generic.ICollection`1"/> is read-only. /// </exception> - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Code Contracts generates the code FxCop is complaining about.")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Code Contracts ccrewrite does this.")] public void Add(T item) { throw new NotSupportedException(); } diff --git a/src/DotNetOpenAuth/Messaging/EnumerableCache.cs b/src/DotNetOpenAuth/Messaging/EnumerableCache.cs index 71825bc..29ec533 100644 --- a/src/DotNetOpenAuth/Messaging/EnumerableCache.cs +++ b/src/DotNetOpenAuth/Messaging/EnumerableCache.cs @@ -35,6 +35,8 @@ namespace DotNetOpenAuth.Messaging { /// to avoid double-caching.</para> /// </remarks> public static IEnumerable<T> CacheGeneratedResults<T>(this IEnumerable<T> sequence) { + Contract.Requires<ArgumentNullException>(sequence != null); + // Don't create a cache for types that don't need it. if (sequence is IList<T> || sequence is ICollection<T> || diff --git a/src/DotNetOpenAuth/Messaging/HttpDeliveryMethods.cs b/src/DotNetOpenAuth/Messaging/HttpDeliveryMethods.cs index 17c8f7a..1443fff 100644 --- a/src/DotNetOpenAuth/Messaging/HttpDeliveryMethods.cs +++ b/src/DotNetOpenAuth/Messaging/HttpDeliveryMethods.cs @@ -46,8 +46,13 @@ namespace DotNetOpenAuth.Messaging { DeleteRequest = 0x10, /// <summary> + /// Added to the URLs in the query part (as defined by [RFC3986] (Berners-Lee, T., “Uniform Resource Identifiers (URI): Generic Syntax,” .) section 3). + /// </summary> + HeadRequest = 0x20, + + /// <summary> /// The flags that control HTTP verbs. /// </summary> - HttpVerbMask = PostRequest | GetRequest | PutRequest | DeleteRequest, + HttpVerbMask = PostRequest | GetRequest | PutRequest | DeleteRequest | HeadRequest, } } diff --git a/src/DotNetOpenAuth/Messaging/IDirectWebRequestHandler.cs b/src/DotNetOpenAuth/Messaging/IDirectWebRequestHandler.cs index af03ba9..3420de9 100644 --- a/src/DotNetOpenAuth/Messaging/IDirectWebRequestHandler.cs +++ b/src/DotNetOpenAuth/Messaging/IDirectWebRequestHandler.cs @@ -187,6 +187,8 @@ namespace DotNetOpenAuth.Messaging { /// </remarks> IncomingWebResponse IDirectWebRequestHandler.GetResponse(HttpWebRequest request) { Contract.Requires<ArgumentNullException>(request != null); + Contract.Ensures(Contract.Result<IncomingWebResponse>() != null); + Contract.Ensures(Contract.Result<IncomingWebResponse>().ResponseStream != null); throw new System.NotImplementedException(); } @@ -209,6 +211,9 @@ namespace DotNetOpenAuth.Messaging { IncomingWebResponse IDirectWebRequestHandler.GetResponse(HttpWebRequest request, DirectWebRequestOptions options) { Contract.Requires<ArgumentNullException>(request != null); Contract.Requires<NotSupportedException>(((IDirectWebRequestHandler)this).CanSupport(options), MessagingStrings.DirectWebRequestOptionsNotSupported); + Contract.Ensures(Contract.Result<IncomingWebResponse>() != null); + Contract.Ensures(Contract.Result<IncomingWebResponse>().ResponseStream != null); + ////ErrorUtilities.VerifySupported(((IDirectWebRequestHandler)this).CanSupport(options), string.Format(MessagingStrings.DirectWebRequestOptionsNotSupported, options, this.GetType().Name)); throw new System.NotImplementedException(); } diff --git a/src/DotNetOpenAuth/Messaging/IMessageWithBinaryData.cs b/src/DotNetOpenAuth/Messaging/IMessageWithBinaryData.cs new file mode 100644 index 0000000..f411cf5 --- /dev/null +++ b/src/DotNetOpenAuth/Messaging/IMessageWithBinaryData.cs @@ -0,0 +1,152 @@ +//----------------------------------------------------------------------- +// <copyright file="IMessageWithBinaryData.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.Messaging { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + + /// <summary> + /// The interface that classes must implement to be serialized/deserialized + /// as protocol or extension messages that uses POST multi-part data for binary content. + /// </summary> + [ContractClass(typeof(IMessageWithBinaryDataContract))] + public interface IMessageWithBinaryData : IDirectedProtocolMessage { + /// <summary> + /// Gets the parts of the message that carry binary data. + /// </summary> + /// <value>A list of parts. Never null.</value> + IList<MultipartPostPart> BinaryData { get; } + + /// <summary> + /// Gets a value indicating whether this message should be sent as multi-part POST. + /// </summary> + bool SendAsMultipart { get; } + } + + /// <summary> + /// The contract class for the <see cref="IMessageWithBinaryData"/> interface. + /// </summary> + [ContractClassFor(typeof(IMessageWithBinaryData))] + internal sealed class IMessageWithBinaryDataContract : IMessageWithBinaryData { + #region IMessageWithBinaryData Members + + /// <summary> + /// Gets the parts of the message that carry binary data. + /// </summary> + /// <value>A list of parts. Never null.</value> + IList<MultipartPostPart> IMessageWithBinaryData.BinaryData { + get { + Contract.Ensures(Contract.Result<IList<MultipartPostPart>>() != null); + throw new NotImplementedException(); + } + } + + /// <summary> + /// Gets a value indicating whether this message should be sent as multi-part POST. + /// </summary> + bool IMessageWithBinaryData.SendAsMultipart { + get { throw new NotImplementedException(); } + } + + #endregion + + #region IMessage Properties + + /// <summary> + /// Gets the version of the protocol or extension this message is prepared to implement. + /// </summary> + /// <value></value> + /// <remarks> + /// Implementations of this interface should ensure that this property never returns null. + /// </remarks> + Version IMessage.Version { + get { + Contract.Ensures(Contract.Result<Version>() != null); + return default(Version); // dummy return + } + } + + /// <summary> + /// Gets the extra, non-standard Protocol parameters included in the message. + /// </summary> + /// <value></value> + /// <remarks> + /// Implementations of this interface should ensure that this property never returns null. + /// </remarks> + IDictionary<string, string> IMessage.ExtraData { + get { + Contract.Ensures(Contract.Result<IDictionary<string, string>>() != null); + return default(IDictionary<string, string>); + } + } + + #endregion + + #region IDirectedProtocolMessage Members + + /// <summary> + /// Gets the preferred method of transport for the message. + /// </summary> + /// <remarks> + /// For indirect messages this will likely be GET+POST, which both can be simulated in the user agent: + /// the GET with a simple 301 Redirect, and the POST with an HTML form in the response with javascript + /// to automate submission. + /// </remarks> + HttpDeliveryMethods IDirectedProtocolMessage.HttpMethods { + get { throw new NotImplementedException(); } + } + + /// <summary> + /// Gets the URL of the intended receiver of this message. + /// </summary> + Uri IDirectedProtocolMessage.Recipient { + get { throw new NotImplementedException(); } + } + + #endregion + + #region IProtocolMessage Members + + /// <summary> + /// Gets the level of protection this message requires. + /// </summary> + MessageProtections IProtocolMessage.RequiredProtection { + get { throw new NotImplementedException(); } + } + + /// <summary> + /// Gets a value indicating whether this is a direct or indirect message. + /// </summary> + MessageTransport IProtocolMessage.Transport { + get { throw new NotImplementedException(); } + } + + #endregion + + #region IMessage methods + + /// <summary> + /// Checks the message state for conformity to the protocol specification + /// and throws an exception if the message is invalid. + /// </summary> + /// <remarks> + /// <para>Some messages have required fields, or combinations of fields that must relate to each other + /// in specialized ways. After deserializing a message, this method checks the state of the + /// message to see if it conforms to the protocol.</para> + /// <para>Note that this property should <i>not</i> check signatures or perform any state checks + /// outside this scope of this particular message.</para> + /// </remarks> + /// <exception cref="ProtocolException">Thrown if the message is invalid.</exception> + void IMessage.EnsureValidMessage() { + throw new NotImplementedException(); + } + + #endregion + } +} diff --git a/src/DotNetOpenAuth/Messaging/ITamperProtectionChannelBindingElement.cs b/src/DotNetOpenAuth/Messaging/ITamperProtectionChannelBindingElement.cs index ba25c12..87ea553 100644 --- a/src/DotNetOpenAuth/Messaging/ITamperProtectionChannelBindingElement.cs +++ b/src/DotNetOpenAuth/Messaging/ITamperProtectionChannelBindingElement.cs @@ -6,12 +6,14 @@ namespace DotNetOpenAuth.Messaging { using System; + using System.Diagnostics.Contracts; using DotNetOpenAuth.OAuth.ChannelElements; /// <summary> /// An interface that must be implemented by message transforms/validators in order /// to be included in the channel stack. /// </summary> + [ContractClass(typeof(ITamperProtectionChannelBindingElementContract))] public interface ITamperProtectionChannelBindingElement : IChannelBindingElement { /// <summary> /// Gets or sets the delegate that will initialize the non-serialized properties necessary on a @@ -25,4 +27,102 @@ namespace DotNetOpenAuth.Messaging { /// <returns>The cloned instance.</returns> ITamperProtectionChannelBindingElement Clone(); } + + /// <summary> + /// Contract class for the <see cref="ITamperProtectionChannelBindingElement"/> interface. + /// </summary> + [ContractClassFor(typeof(ITamperProtectionChannelBindingElement))] + internal abstract class ITamperProtectionChannelBindingElementContract : ITamperProtectionChannelBindingElement { + #region ITamperProtectionChannelBindingElement Properties + + /// <summary> + /// Gets or sets the delegate that will initialize the non-serialized properties necessary on a + /// signable message so that its signature can be correctly calculated or verified. + /// </summary> + Action<ITamperResistantOAuthMessage> ITamperProtectionChannelBindingElement.SignatureCallback { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + + #endregion + + #region IChannelBindingElement Members + + /// <summary> + /// Gets or sets the channel that this binding element belongs to. + /// </summary> + /// <remarks> + /// This property is set by the channel when it is first constructed. + /// </remarks> + Channel IChannelBindingElement.Channel { + get { throw new NotImplementedException(); } + set { throw new NotImplementedException(); } + } + + /// <summary> + /// Gets the protection commonly offered (if any) by this binding element. + /// </summary> + /// <remarks> + /// This value is used to assist in sorting binding elements in the channel stack. + /// </remarks> + MessageProtections IChannelBindingElement.Protection { + get { throw new NotImplementedException(); } + } + + /// <summary> + /// Prepares a message for sending based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The message to prepare for sending.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + MessageProtections? IChannelBindingElement.ProcessOutgoingMessage(IProtocolMessage message) { + Contract.Requires<ArgumentNullException>(message != null); + Contract.Requires<InvalidOperationException>(((IChannelBindingElement)this).Channel != null); + throw new NotImplementedException(); + } + + /// <summary> + /// Performs any transformation on an incoming message that may be necessary and/or + /// validates an incoming message based on the rules of this channel binding element. + /// </summary> + /// <param name="message">The incoming message to process.</param> + /// <returns> + /// The protections (if any) that this binding element applied to the message. + /// Null if this binding element did not even apply to this binding element. + /// </returns> + /// <exception cref="ProtocolException"> + /// Thrown when the binding element rules indicate that this message is invalid and should + /// NOT be processed. + /// </exception> + /// <remarks> + /// Implementations that provide message protection must honor the + /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. + /// </remarks> + MessageProtections? IChannelBindingElement.ProcessIncomingMessage(IProtocolMessage message) { + Contract.Requires<ArgumentNullException>(message != null); + Contract.Requires<InvalidOperationException>(((IChannelBindingElement)this).Channel != null); + throw new NotImplementedException(); + } + + #endregion + + #region ITamperProtectionChannelBindingElement Methods + + /// <summary> + /// Clones this instance. + /// </summary> + /// <returns>The cloned instance.</returns> + ITamperProtectionChannelBindingElement ITamperProtectionChannelBindingElement.Clone() { + Contract.Ensures(Contract.Result<ITamperProtectionChannelBindingElement>() != null); + throw new NotImplementedException(); + } + + #endregion + } } diff --git a/src/DotNetOpenAuth/Messaging/MessageReceivingEndpoint.cs b/src/DotNetOpenAuth/Messaging/MessageReceivingEndpoint.cs index e26a672..e39a047 100644 --- a/src/DotNetOpenAuth/Messaging/MessageReceivingEndpoint.cs +++ b/src/DotNetOpenAuth/Messaging/MessageReceivingEndpoint.cs @@ -24,6 +24,7 @@ namespace DotNetOpenAuth.Messaging { : this(new Uri(locationUri), method) { Contract.Requires<ArgumentNullException>(locationUri != null); Contract.Requires<ArgumentOutOfRangeException>(method != HttpDeliveryMethods.None); + Contract.Requires<ArgumentOutOfRangeException>((method & HttpDeliveryMethods.HttpVerbMask) != 0, MessagingStrings.GetOrPostFlagsRequired); } /// <summary> diff --git a/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs b/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs index e5b725b..0bbac42 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingStrings.Designer.cs @@ -70,6 +70,15 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> + /// Looks up a localized string similar to Unable to send all message data because some of it requires multi-part POST, but IMessageWithBinaryData.SendAsMultipart was false.. + /// </summary> + internal static string BinaryDataRequiresMultipart { + get { + return ResourceManager.GetString("BinaryDataRequiresMultipart", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to HttpContext.Current is null. There must be an ASP.NET request in process for this operation to succeed.. /// </summary> internal static string CurrentHttpContextRequired { diff --git a/src/DotNetOpenAuth/Messaging/MessagingStrings.resx b/src/DotNetOpenAuth/Messaging/MessagingStrings.resx index 3d4e317..34385d4 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingStrings.resx +++ b/src/DotNetOpenAuth/Messaging/MessagingStrings.resx @@ -297,4 +297,7 @@ <data name="StreamMustHaveKnownLength" xml:space="preserve"> <value>The stream must have a known length.</value> </data> + <data name="BinaryDataRequiresMultipart" xml:space="preserve"> + <value>Unable to send all message data because some of it requires multi-part POST, but IMessageWithBinaryData.SendAsMultipart was false.</value> + </data> </root>
\ No newline at end of file diff --git a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs index be5927f..c29ec8c 100644 --- a/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs +++ b/src/DotNetOpenAuth/Messaging/MessagingUtilities.cs @@ -141,7 +141,7 @@ namespace DotNetOpenAuth.Messaging { } /// <summary> - /// Sends an multipart HTTP POST request (useful for posting files). + /// Sends a multipart HTTP POST request (useful for posting files). /// </summary> /// <param name="request">The HTTP request.</param> /// <param name="requestHandler">The request handler.</param> @@ -152,14 +152,60 @@ namespace DotNetOpenAuth.Messaging { Contract.Requires<ArgumentNullException>(requestHandler != null); Contract.Requires<ArgumentNullException>(parts != null); + PostMultipartNoGetResponse(request, requestHandler, parts); + return requestHandler.GetResponse(request); + } + + /// <summary> + /// Assembles a message comprised of the message on a given exception and all inner exceptions. + /// </summary> + /// <param name="exception">The exception.</param> + /// <returns>The assembled message.</returns> + public static string ToStringDescriptive(this Exception exception) { + // The input being null is probably bad, but since this method is called + // from a catch block, we don't really want to throw a new exception and + // hide the details of this one. + if (exception == null) { + Logger.Messaging.Error("MessagingUtilities.GetAllMessages called with null input."); + } + + StringBuilder message = new StringBuilder(); + while (exception != null) { + message.Append(exception.Message); + exception = exception.InnerException; + if (exception != null) { + message.Append(" "); + } + } + + return message.ToString(); + } + + /// <summary> + /// Sends a multipart HTTP POST request (useful for posting files) but doesn't call GetResponse on it. + /// </summary> + /// <param name="request">The HTTP request.</param> + /// <param name="requestHandler">The request handler.</param> + /// <param name="parts">The parts to include in the POST entity.</param> + internal static void PostMultipartNoGetResponse(this HttpWebRequest request, IDirectWebRequestHandler requestHandler, IEnumerable<MultipartPostPart> parts) { + Contract.Requires<ArgumentNullException>(request != null); + Contract.Requires<ArgumentNullException>(requestHandler != null); + Contract.Requires<ArgumentNullException>(parts != null); + Reporting.RecordFeatureUse("MessagingUtilities.PostMultipart"); + parts = parts.CacheGeneratedResults(); string boundary = Guid.NewGuid().ToString(); + string initialPartLeadingBoundary = string.Format(CultureInfo.InvariantCulture, "--{0}\r\n", boundary); string partLeadingBoundary = string.Format(CultureInfo.InvariantCulture, "\r\n--{0}\r\n", boundary); string finalTrailingBoundary = string.Format(CultureInfo.InvariantCulture, "\r\n--{0}--\r\n", boundary); request.Method = "POST"; request.ContentType = "multipart/form-data; boundary=" + boundary; - request.ContentLength = parts.Sum(p => partLeadingBoundary.Length + p.Length) + finalTrailingBoundary.Length; + long contentLength = parts.Sum(p => partLeadingBoundary.Length + p.Length) + finalTrailingBoundary.Length; + if (parts.Any()) { + contentLength -= 2; // the initial part leading boundary has no leading \r\n + } + request.ContentLength = contentLength; // Setting the content-encoding to "utf-8" causes Google to reply // with a 415 UnsupportedMediaType. But adding it doesn't buy us @@ -169,8 +215,10 @@ namespace DotNetOpenAuth.Messaging { var requestStream = requestHandler.GetRequestStream(request); try { StreamWriter writer = new StreamWriter(requestStream, Channel.PostEntityEncoding); + bool firstPart = true; foreach (var part in parts) { - writer.Write(partLeadingBoundary); + writer.Write(firstPart ? initialPartLeadingBoundary : partLeadingBoundary); + firstPart = false; part.Serialize(writer); part.Dispose(); } @@ -185,33 +233,6 @@ namespace DotNetOpenAuth.Messaging { requestStream.Dispose(); } } - - return requestHandler.GetResponse(request); - } - - /// <summary> - /// Assembles a message comprised of the message on a given exception and all inner exceptions. - /// </summary> - /// <param name="exception">The exception.</param> - /// <returns>The assembled message.</returns> - public static string ToStringDescriptive(this Exception exception) { - // The input being null is probably bad, but since this method is called - // from a catch block, we don't really want to throw a new exception and - // hide the details of this one. - if (exception == null) { - Logger.Messaging.Error("MessagingUtilities.GetAllMessages called with null input."); - } - - StringBuilder message = new StringBuilder(); - while (exception != null) { - message.Append(exception.Message); - exception = exception.InnerException; - if (exception != null) { - message.Append(" "); - } - } - - return message.ToString(); } /// <summary> @@ -646,6 +667,7 @@ namespace DotNetOpenAuth.Messaging { /// </summary> /// <param name="request">The request to get recipient information from.</param> /// <returns>The recipient.</returns> + /// <exception cref="ArgumentException">Thrown if the HTTP request is something we can't handle.</exception> internal static MessageReceivingEndpoint GetRecipient(this HttpRequestInfo request) { return new MessageReceivingEndpoint(request.UrlBeforeRewriting, GetHttpDeliveryMethod(request.HttpMethod)); } @@ -655,6 +677,7 @@ namespace DotNetOpenAuth.Messaging { /// </summary> /// <param name="httpVerb">The HTTP verb.</param> /// <returns>A <see cref="HttpDeliveryMethods"/> enum value that is within the <see cref="HttpDeliveryMethods.HttpVerbMask"/>.</returns> + /// <exception cref="ArgumentException">Thrown if the HTTP request is something we can't handle.</exception> internal static HttpDeliveryMethods GetHttpDeliveryMethod(string httpVerb) { if (httpVerb == "GET") { return HttpDeliveryMethods.GetRequest; @@ -664,6 +687,8 @@ namespace DotNetOpenAuth.Messaging { return HttpDeliveryMethods.PutRequest; } else if (httpVerb == "DELETE") { return HttpDeliveryMethods.DeleteRequest; + } else if (httpVerb == "HEAD") { + return HttpDeliveryMethods.HeadRequest; } else { throw ErrorUtilities.ThrowArgumentNamed("httpVerb", MessagingStrings.UnsupportedHttpVerb, httpVerb); } @@ -675,14 +700,16 @@ namespace DotNetOpenAuth.Messaging { /// <param name="httpMethod">The HTTP method.</param> /// <returns>An HTTP verb, such as GET, POST, PUT, or DELETE.</returns> internal static string GetHttpVerb(HttpDeliveryMethods httpMethod) { - if ((httpMethod & HttpDeliveryMethods.GetRequest) != 0) { + if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.GetRequest) { return "GET"; - } else if ((httpMethod & HttpDeliveryMethods.PostRequest) != 0) { + } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.PostRequest) { return "POST"; - } else if ((httpMethod & HttpDeliveryMethods.PutRequest) != 0) { + } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.PutRequest) { return "PUT"; - } else if ((httpMethod & HttpDeliveryMethods.DeleteRequest) != 0) { + } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.DeleteRequest) { return "DELETE"; + } else if ((httpMethod & HttpDeliveryMethods.HttpVerbMask) == HttpDeliveryMethods.HeadRequest) { + return "HEAD"; } else if ((httpMethod & HttpDeliveryMethods.AuthorizationHeaderRequest) != 0) { return "GET"; // if AuthorizationHeaderRequest is specified without an explicit HTTP verb, assume GET. } else { diff --git a/src/DotNetOpenAuth/Messaging/MultipartPostPart.cs b/src/DotNetOpenAuth/Messaging/MultipartPostPart.cs index 7ef89a4..f9a5988 100644 --- a/src/DotNetOpenAuth/Messaging/MultipartPostPart.cs +++ b/src/DotNetOpenAuth/Messaging/MultipartPostPart.cs @@ -116,6 +116,10 @@ namespace DotNetOpenAuth.Messaging { /// <param name="contentType">Type of the content in HTTP Content-Type format.</param> /// <returns>The constructed part.</returns> public static MultipartPostPart CreateFormFilePart(string name, string filePath, string contentType) { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(name)); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(filePath)); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(contentType)); + string fileName = Path.GetFileName(filePath); return CreateFormFilePart(name, fileName, contentType, File.OpenRead(filePath)); } @@ -130,11 +134,11 @@ namespace DotNetOpenAuth.Messaging { /// <returns>The constructed part.</returns> public static MultipartPostPart CreateFormFilePart(string name, string fileName, string contentType, Stream content) { Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(name)); - Contract.Requires<ArgumentException>(fileName != null); - Contract.Requires<ArgumentException>(contentType != null); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(fileName)); + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(contentType)); Contract.Requires<ArgumentException>(content != null); - var part = new MultipartPostPart("form-data"); + var part = new MultipartPostPart("file"); part.ContentAttributes["name"] = name; part.ContentAttributes["filename"] = fileName; part.PartHeaders[HttpRequestHeader.ContentType] = contentType; diff --git a/src/DotNetOpenAuth/Messaging/Reflection/MessageDictionary.cs b/src/DotNetOpenAuth/Messaging/Reflection/MessageDictionary.cs index 49e30cb..f6eed24 100644 --- a/src/DotNetOpenAuth/Messaging/Reflection/MessageDictionary.cs +++ b/src/DotNetOpenAuth/Messaging/Reflection/MessageDictionary.cs @@ -269,7 +269,7 @@ namespace DotNetOpenAuth.Messaging.Reflection { /// Sets a named value in the message. /// </summary> /// <param name="item">The name-value pair to add. The name is the serialized form of the key.</param> - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Code Contracts generates the code FxCop is complaining about.")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Code Contracts ccrewrite does this.")] public void Add(KeyValuePair<string, string> item) { this.Add(item.Key, item.Value); } diff --git a/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs b/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs index 32d2fec..08e2411 100644 --- a/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs +++ b/src/DotNetOpenAuth/Messaging/Reflection/MessagePart.cs @@ -286,6 +286,9 @@ namespace DotNetOpenAuth.Messaging.Reflection { /// <param name="messagePartEncoder">The message part encoder type.</param> /// <returns>An instance of the desired encoder.</returns> private static IMessagePartEncoder GetEncoder(Type messagePartEncoder) { + Contract.Requires<ArgumentNullException>(messagePartEncoder != null); + Contract.Ensures(Contract.Result<IMessagePartEncoder>() != null); + IMessagePartEncoder encoder; if (!encoders.TryGetValue(messagePartEncoder, out encoder)) { encoder = encoders[messagePartEncoder] = (IMessagePartEncoder)Activator.CreateInstance(messagePartEncoder); diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs index 8c5980f..b0e938f 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/OAuthChannel.cs @@ -11,6 +11,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { using System.Diagnostics.Contracts; using System.Globalization; using System.IO; + using System.Linq; using System.Net; using System.Text; using System.Web; @@ -87,7 +88,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// </summary> /// <param name="message">The message with data to encode.</param> /// <returns>A dictionary of name-value pairs with their strings encoded.</returns> - internal static IDictionary<string, string> GetUriEscapedParameters(MessageDictionary message) { + internal static IDictionary<string, string> GetUriEscapedParameters(IEnumerable<KeyValuePair<string, string>> message) { var encodedDictionary = new Dictionary<string, string>(); UriEscapeParameters(message, encodedDictionary); return encodedDictionary; @@ -155,8 +156,16 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { } } + 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, request.GetRecipient()); + var message = (IDirectedProtocolMessage)this.Receive(fields, recipient); // Add receiving HTTP transport information required for signature generation. var signedMessage = message as ITamperResistantOAuthMessage; @@ -194,9 +203,13 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { 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) { @@ -264,7 +277,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// </summary> /// <param name="source">The dictionary with names and values to encode.</param> /// <param name="destination">The dictionary to add the encoded pairs to.</param> - private static void UriEscapeParameters(IDictionary<string, string> source, IDictionary<string, string> destination) { + private static void UriEscapeParameters(IEnumerable<KeyValuePair<string, string>> source, IDictionary<string, string> destination) { Contract.Requires<ArgumentNullException>(source != null); Contract.Requires<ArgumentNullException>(destination != null); @@ -341,12 +354,22 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { if (hasEntity) { // WARNING: We only set up the request stream for the caller if there is // extra data. If there isn't any extra data, the caller must do this themselves. - if (requestMessage.ExtraData.Count > 0) { - SendParametersInEntity(httpRequest, requestMessage.ExtraData); + 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 multiPartFields = new List<MultipartPostPart>(requestMessageWithBinaryData.BinaryData); + multiPartFields.AddRange(requestMessage.ExtraData.Select(field => MultipartPostPart.CreateFormPart(field.Key, field.Value))); + this.SendParametersInEntityAsMultipart(httpRequest, multiPartFields); } else { - // We'll assume the content length is zero since the caller may not have - // anything. They're responsible to change it when the add the payload if they have one. - httpRequest.ContentLength = 0; + ErrorUtilities.VerifyProtocol(requestMessageWithBinaryData == null || requestMessageWithBinaryData.BinaryData.Count == 0, MessagingStrings.BinaryDataRequiresMultipart); + if (requestMessage.ExtraData.Count > 0) { + this.SendParametersInEntity(httpRequest, requestMessage.ExtraData); + } else { + // We'll assume the content length is zero since the caller may not have + // anything. They're responsible to change it when the add the payload if they have one. + httpRequest.ContentLength = 0; + } } } diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs index 004e7d5..b45da66 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementBase.cs @@ -10,6 +10,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { using System.Collections.Specialized; using System.Diagnostics.Contracts; using System.Globalization; + using System.Linq; using System.Text; using System.Web; using DotNetOpenAuth.Messaging; @@ -157,7 +158,26 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { signatureBaseStringElements.Add(message.HttpMethod.ToUpperInvariant()); - var encodedDictionary = OAuthChannel.GetUriEscapedParameters(messageDictionary); + // For multipart POST messages, only include the message parts that are NOT + // in the POST entity (those parts that may appear in an OAuth authorization header). + var encodedDictionary = new Dictionary<string, string>(); + IEnumerable<KeyValuePair<string, string>> partsToInclude = Enumerable.Empty<KeyValuePair<string, string>>(); + var binaryMessage = message as IMessageWithBinaryData; + if (binaryMessage != null && binaryMessage.SendAsMultipart) { + HttpDeliveryMethods authHeaderInUseFlags = HttpDeliveryMethods.PostRequest | HttpDeliveryMethods.AuthorizationHeaderRequest; + ErrorUtilities.VerifyProtocol((binaryMessage.HttpMethods & authHeaderInUseFlags) == authHeaderInUseFlags, OAuthStrings.MultipartPostMustBeUsedWithAuthHeader); + + // Include the declared keys in the signature as those will be signable. + // Cache in local variable to avoid recalculating DeclaredKeys in the delegate. + ICollection<string> declaredKeys = messageDictionary.DeclaredKeys; + partsToInclude = messageDictionary.Where(pair => declaredKeys.Contains(pair.Key)); + } else { + partsToInclude = messageDictionary; + } + + foreach (var pair in OAuthChannel.GetUriEscapedParameters(partsToInclude)) { + encodedDictionary[pair.Key] = pair.Value; + } // An incoming message will already have included the query and form parameters // in the message dictionary, but an outgoing message COULD have SOME parameters diff --git a/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementChain.cs b/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementChain.cs index bdb0219..67c5205 100644 --- a/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementChain.cs +++ b/src/DotNetOpenAuth/OAuth/ChannelElements/SigningBindingElementChain.cs @@ -20,7 +20,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// <summary> /// The various signing binding elements that may be applicable to a message in preferred use order. /// </summary> - private ITamperProtectionChannelBindingElement[] signers; + private readonly ITamperProtectionChannelBindingElement[] signers; /// <summary> /// Initializes a new instance of the <see cref="SigningBindingElementChain"/> class. @@ -93,6 +93,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// </returns> public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { foreach (IChannelBindingElement signer in this.signers) { + ErrorUtilities.VerifyInternal(signer.Channel != null, "A binding element's Channel property is unexpectedly null."); MessageProtections? result = signer.ProcessOutgoingMessage(message); if (result.HasValue) { return result; @@ -113,6 +114,7 @@ namespace DotNetOpenAuth.OAuth.ChannelElements { /// </returns> public MessageProtections? ProcessIncomingMessage(IProtocolMessage message) { foreach (IChannelBindingElement signer in this.signers) { + ErrorUtilities.VerifyInternal(signer.Channel != null, "A binding element's Channel property is unexpectedly null."); MessageProtections? result = signer.ProcessIncomingMessage(message); if (result.HasValue) { return result; diff --git a/src/DotNetOpenAuth/OAuth/ConsumerBase.cs b/src/DotNetOpenAuth/OAuth/ConsumerBase.cs index 6c0ce42..48f54d7 100644 --- a/src/DotNetOpenAuth/OAuth/ConsumerBase.cs +++ b/src/DotNetOpenAuth/OAuth/ConsumerBase.cs @@ -9,6 +9,7 @@ namespace DotNetOpenAuth.OAuth { using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; + using System.Linq; using System.Net; using DotNetOpenAuth.Configuration; using DotNetOpenAuth.Messaging; @@ -111,6 +112,27 @@ namespace DotNetOpenAuth.OAuth { } /// <summary> + /// Prepares an authorized request that carries an HTTP multi-part POST, allowing for binary data. + /// </summary> + /// <param name="endpoint">The URL and method on the Service Provider to send the request to.</param> + /// <param name="accessToken">The access token that permits access to the protected resource.</param> + /// <param name="binaryData">Extra parameters to include in the message. Must not be null, but may be empty.</param> + /// <returns>The initialized WebRequest object.</returns> + public HttpWebRequest PrepareAuthorizedRequest(MessageReceivingEndpoint endpoint, string accessToken, IEnumerable<MultipartPostPart> binaryData) { + Contract.Requires<ArgumentNullException>(endpoint != null); + Contract.Requires<ArgumentException>(!String.IsNullOrEmpty(accessToken)); + Contract.Requires<ArgumentNullException>(binaryData != null); + + AccessProtectedResourceRequest message = this.CreateAuthorizingMessage(endpoint, accessToken); + foreach (MultipartPostPart part in binaryData) { + message.BinaryData.Add(part); + } + + HttpWebRequest wr = this.OAuthChannel.InitializeRequest(message); + return wr; + } + + /// <summary> /// Prepares an HTTP request that has OAuth authorization already attached to it. /// </summary> /// <param name="message">The OAuth authorization message to attach to the HTTP request.</param> diff --git a/src/DotNetOpenAuth/OAuth/Messages/AccessProtectedResourceRequest.cs b/src/DotNetOpenAuth/OAuth/Messages/AccessProtectedResourceRequest.cs index b60fda4..f3231f0 100644 --- a/src/DotNetOpenAuth/OAuth/Messages/AccessProtectedResourceRequest.cs +++ b/src/DotNetOpenAuth/OAuth/Messages/AccessProtectedResourceRequest.cs @@ -6,6 +6,7 @@ namespace DotNetOpenAuth.OAuth.Messages { using System; + using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using DotNetOpenAuth.Messaging; @@ -13,7 +14,12 @@ namespace DotNetOpenAuth.OAuth.Messages { /// A message attached to a request for protected resources that provides the necessary /// credentials to be granted access to those resources. /// </summary> - public class AccessProtectedResourceRequest : SignedMessageBase, ITokenContainingMessage { + public class AccessProtectedResourceRequest : SignedMessageBase, ITokenContainingMessage, IMessageWithBinaryData { + /// <summary> + /// A store for the binary data that is carried in the message. + /// </summary> + private List<MultipartPostPart> binaryData = new List<MultipartPostPart>(); + /// <summary> /// Initializes a new instance of the <see cref="AccessProtectedResourceRequest"/> class. /// </summary> @@ -43,5 +49,24 @@ namespace DotNetOpenAuth.OAuth.Messages { /// </remarks> [MessagePart("oauth_token", IsRequired = true)] public string AccessToken { get; set; } + + #region IMessageWithBinaryData Members + + /// <summary> + /// Gets the parts of the message that carry binary data. + /// </summary> + /// <value>A list of parts. Never null.</value> + public IList<MultipartPostPart> BinaryData { + get { return this.binaryData; } + } + + /// <summary> + /// Gets a value indicating whether this message should be sent as multi-part POST. + /// </summary> + public bool SendAsMultipart { + get { return this.HttpMethod == "POST" && this.BinaryData.Count > 0; } + } + + #endregion } } diff --git a/src/DotNetOpenAuth/OAuth/OAuthStrings.Designer.cs b/src/DotNetOpenAuth/OAuth/OAuthStrings.Designer.cs index bb4d5d9..fb4c9a1 100644 --- a/src/DotNetOpenAuth/OAuth/OAuthStrings.Designer.cs +++ b/src/DotNetOpenAuth/OAuth/OAuthStrings.Designer.cs @@ -124,6 +124,15 @@ namespace DotNetOpenAuth.OAuth { } /// <summary> + /// Looks up a localized string similar to Cannot send OAuth message as multipart POST without an authorization HTTP header because sensitive data would not be signed.. + /// </summary> + internal static string MultipartPostMustBeUsedWithAuthHeader { + get { + return ResourceManager.GetString("MultipartPostMustBeUsedWithAuthHeader", resourceCulture); + } + } + + /// <summary> /// Looks up a localized string similar to Use of the OpenID+OAuth extension requires that the token manager in use implement the {0} interface.. /// </summary> internal static string OpenIdOAuthExtensionRequiresSpecialTokenManagerInterface { diff --git a/src/DotNetOpenAuth/OAuth/OAuthStrings.resx b/src/DotNetOpenAuth/OAuth/OAuthStrings.resx index bbeeda9..34b314b 100644 --- a/src/DotNetOpenAuth/OAuth/OAuthStrings.resx +++ b/src/DotNetOpenAuth/OAuth/OAuthStrings.resx @@ -138,6 +138,9 @@ <data name="MinimumConsumerVersionRequirementNotMet" xml:space="preserve"> <value>This OAuth service provider requires OAuth consumers to implement OAuth {0}, but this consumer appears to only support {1}.</value> </data> + <data name="MultipartPostMustBeUsedWithAuthHeader" xml:space="preserve"> + <value>Cannot send OAuth message as multipart POST without an authorization HTTP header because sensitive data would not be signed.</value> + </data> <data name="OpenIdOAuthExtensionRequiresSpecialTokenManagerInterface" xml:space="preserve"> <value>Use of the OpenID+OAuth extension requires that the token manager in use implement the {0} interface.</value> </data> diff --git a/src/DotNetOpenAuth/OAuth/ServiceProviderDescription.cs b/src/DotNetOpenAuth/OAuth/ServiceProviderDescription.cs index 9014762..6205f55 100644 --- a/src/DotNetOpenAuth/OAuth/ServiceProviderDescription.cs +++ b/src/DotNetOpenAuth/OAuth/ServiceProviderDescription.cs @@ -8,6 +8,7 @@ namespace DotNetOpenAuth.OAuth { using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; + using System.Diagnostics.Contracts; using System.Linq; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OAuth.ChannelElements; @@ -94,6 +95,7 @@ namespace DotNetOpenAuth.OAuth { /// </summary> /// <returns>The created signing element.</returns> internal ITamperProtectionChannelBindingElement CreateTamperProtectionElement() { + Contract.Requires(this.TamperProtectionElements != null); return new SigningBindingElementChain(this.TamperProtectionElements.Select(el => (ITamperProtectionChannelBindingElement)el.Clone()).ToArray()); } } diff --git a/src/DotNetOpenAuth/OpenId/Behaviors/AXFetchAsSregTransform.cs b/src/DotNetOpenAuth/OpenId/Behaviors/AXFetchAsSregTransform.cs index d7dca9a..580bdfd 100644 --- a/src/DotNetOpenAuth/OpenId/Behaviors/AXFetchAsSregTransform.cs +++ b/src/DotNetOpenAuth/OpenId/Behaviors/AXFetchAsSregTransform.cs @@ -23,7 +23,7 @@ namespace DotNetOpenAuth.OpenId.Behaviors { /// to the originally requested extension and format. /// </summary> [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Sreg", Justification = "Abbreviation")] - public class AXFetchAsSregTransform : IRelyingPartyBehavior, IProviderBehavior { + public sealed class AXFetchAsSregTransform : IRelyingPartyBehavior, IProviderBehavior { /// <summary> /// Initializes static members of the <see cref="AXFetchAsSregTransform"/> class. /// </summary> diff --git a/src/DotNetOpenAuth/OpenId/ChannelElements/ExtensionsBindingElement.cs b/src/DotNetOpenAuth/OpenId/ChannelElements/ExtensionsBindingElement.cs index 4fa70a0..c516e8f 100644 --- a/src/DotNetOpenAuth/OpenId/ChannelElements/ExtensionsBindingElement.cs +++ b/src/DotNetOpenAuth/OpenId/ChannelElements/ExtensionsBindingElement.cs @@ -7,6 +7,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; using System.Text; @@ -76,6 +77,7 @@ namespace DotNetOpenAuth.OpenId.ChannelElements { /// Implementations that provide message protection must honor the /// <see cref="MessagePartAttribute.RequiredProtection"/> properties where applicable. /// </remarks> + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "It doesn't look too bad to me. :)")] public MessageProtections? ProcessOutgoingMessage(IProtocolMessage message) { var extendableMessage = message as IProtocolMessageWithExtensions; if (extendableMessage != null) { diff --git a/src/DotNetOpenAuth/OpenId/Extensions/UI/UIRequest.cs b/src/DotNetOpenAuth/OpenId/Extensions/UI/UIRequest.cs index 67efe8e..76a1c9b 100644 --- a/src/DotNetOpenAuth/OpenId/Extensions/UI/UIRequest.cs +++ b/src/DotNetOpenAuth/OpenId/Extensions/UI/UIRequest.cs @@ -70,7 +70,7 @@ namespace DotNetOpenAuth.OpenId.Extensions.UI { /// <remarks> /// The user's preferred languages as a [BCP 47] language priority list, represented as a comma-separated list of BCP 47 basic language ranges in descending priority order. For instance, the value "fr-CA,fr-FR,en-CA" represents the preference for French spoken in Canada, French spoken in France, followed by English spoken in Canada. /// </remarks> - [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "By design")] + [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", Justification = "By design.")] [MessagePart("lang", AllowEmpty = false)] public CultureInfo[] LanguagePreference { get; set; } diff --git a/src/DotNetOpenAuth/OpenId/HostMetaDiscoveryService.cs b/src/DotNetOpenAuth/OpenId/HostMetaDiscoveryService.cs index b6ed2f7..e96f362 100644 --- a/src/DotNetOpenAuth/OpenId/HostMetaDiscoveryService.cs +++ b/src/DotNetOpenAuth/OpenId/HostMetaDiscoveryService.cs @@ -7,6 +7,7 @@ namespace DotNetOpenAuth.OpenId { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Globalization; using System.IO; @@ -266,6 +267,7 @@ namespace DotNetOpenAuth.OpenId { /// an alternative plan. /// </remarks> /// <exception cref="ProtocolException">Thrown if the certificate chain is invalid or unverifiable.</exception> + [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Justification = "By design")] private static void VerifyCertChain(List<X509Certificate2> certs) { var chain = new X509Chain(); foreach (var cert in certs) { @@ -418,6 +420,7 @@ namespace DotNetOpenAuth.OpenId { /// <summary> /// A description of a web server that hosts host-meta documents. /// </summary> + [SuppressMessage("Microsoft.Design", "CA1034:NestedTypesShouldNotBeVisible", Justification = "By design")] public class HostMetaProxy { /// <summary> /// Initializes a new instance of the <see cref="HostMetaProxy"/> class. diff --git a/src/DotNetOpenAuth/OpenId/IIdentifierDiscoveryService.cs b/src/DotNetOpenAuth/OpenId/IIdentifierDiscoveryService.cs index f637b37..eb2bf98 100644 --- a/src/DotNetOpenAuth/OpenId/IIdentifierDiscoveryService.cs +++ b/src/DotNetOpenAuth/OpenId/IIdentifierDiscoveryService.cs @@ -7,6 +7,7 @@ namespace DotNetOpenAuth.OpenId { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; using System.Text; @@ -27,6 +28,7 @@ namespace DotNetOpenAuth.OpenId { /// <returns> /// A sequence of service endpoints yielded by discovery. Must not be null, but may be empty. /// </returns> + [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By design")] [Pure] IEnumerable<IdentifierDiscoveryResult> Discover(Identifier identifier, IDirectWebRequestHandler requestHandler, out bool abortDiscoveryChain); } diff --git a/src/DotNetOpenAuth/OpenId/Messages/RequestBase.cs b/src/DotNetOpenAuth/OpenId/Messages/RequestBase.cs index 6b89e92..8e4cb9d 100644 --- a/src/DotNetOpenAuth/OpenId/Messages/RequestBase.cs +++ b/src/DotNetOpenAuth/OpenId/Messages/RequestBase.cs @@ -32,14 +32,14 @@ namespace DotNetOpenAuth.OpenId.Messages { #pragma warning restore 0414 /// <summary> - /// Backing store for the <see cref="Incoming"/> property. + /// Backing store for the <see cref="ExtraData"/> property. /// </summary> - private bool incoming; + private readonly Dictionary<string, string> extraData = new Dictionary<string, string>(); /// <summary> - /// Backing store for the <see cref="ExtraData"/> property. + /// Backing store for the <see cref="Incoming"/> property. /// </summary> - private Dictionary<string, string> extraData = new Dictionary<string, string>(); + private bool incoming; /// <summary> /// Initializes a new instance of the <see cref="RequestBase"/> class. diff --git a/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs b/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs index 611101d..10c69a3 100644 --- a/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs +++ b/src/DotNetOpenAuth/OpenId/Provider/OpenIdProvider.cs @@ -25,6 +25,7 @@ namespace DotNetOpenAuth.OpenId.Provider { /// <summary> /// Offers services for a web page that is acting as an OpenID identity server. /// </summary> + [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "By design")] [ContractVerification(true)] public sealed class OpenIdProvider : IDisposable { /// <summary> diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs index 6e141ad..b6a1b76 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/PositiveAuthenticationResponse.cs @@ -38,7 +38,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { this.VerifyDiscoveryMatchesAssertion(relyingParty); - Logger.OpenId.InfoFormat("Received identity assertion for {0} via {1}.", this.ClaimedIdentifier, this.Provider.Uri); + Logger.OpenId.InfoFormat("Received identity assertion for {0} via {1}.", this.Response.ClaimedIdentifier, this.Provider.Uri); } #region IAuthenticationResponse Properties diff --git a/src/DotNetOpenAuth/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs b/src/DotNetOpenAuth/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs index 8499178..f0a1574 100644 --- a/src/DotNetOpenAuth/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs +++ b/src/DotNetOpenAuth/OpenId/RelyingParty/StandardRelyingPartyApplicationStore.cs @@ -106,7 +106,7 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// </summary> /// <param name="context">The context, or namespace, within which the <paramref name="nonce"/> must be unique.</param> /// <param name="nonce">A series of random characters.</param> - /// <param name="timestamp">The timestamp that together with the nonce string make it unique. + /// <param name="timestampUtc">The timestamp that together with the nonce string make it unique. /// The timestamp may also be used by the data store to clear out old nonces.</param> /// <returns> /// True if the nonce+timestamp (combination) was not previously in the database. @@ -119,8 +119,8 @@ namespace DotNetOpenAuth.OpenId.RelyingParty { /// is retrieved or set using the /// <see cref="StandardExpirationBindingElement.MaximumMessageAge"/> property. /// </remarks> - public bool StoreNonce(string context, string nonce, DateTime timestamp) { - return this.nonceStore.StoreNonce(context, nonce, timestamp); + public bool StoreNonce(string context, string nonce, DateTime timestampUtc) { + return this.nonceStore.StoreNonce(context, nonce, timestampUtc); } #endregion diff --git a/src/DotNetOpenAuth/OpenId/XriDiscoveryProxyService.cs b/src/DotNetOpenAuth/OpenId/XriDiscoveryProxyService.cs index e338542..b1a3430 100644 --- a/src/DotNetOpenAuth/OpenId/XriDiscoveryProxyService.cs +++ b/src/DotNetOpenAuth/OpenId/XriDiscoveryProxyService.cs @@ -7,6 +7,7 @@ namespace DotNetOpenAuth.OpenId { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Globalization; using System.Linq; @@ -21,6 +22,7 @@ namespace DotNetOpenAuth.OpenId { /// <summary> /// The discovery service for XRI identifiers that uses an XRI proxy resolver for discovery. /// </summary> + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Xri", Justification = "Acronym")] public class XriDiscoveryProxyService : IIdentifierDiscoveryService { /// <summary> /// The magic URL that will provide us an XRDS document for a given XRI identifier. diff --git a/src/DotNetOpenAuth/OpenId/XriIdentifier.cs b/src/DotNetOpenAuth/OpenId/XriIdentifier.cs index 6af41fa..729f603 100644 --- a/src/DotNetOpenAuth/OpenId/XriIdentifier.cs +++ b/src/DotNetOpenAuth/OpenId/XriIdentifier.cs @@ -94,7 +94,9 @@ namespace DotNetOpenAuth.OpenId { public override bool Equals(object obj) { XriIdentifier other = obj as XriIdentifier; if (obj != null && other == null && Identifier.EqualityOnStrings) { // test hook to enable MockIdentifier comparison - other = Identifier.Parse(obj.ToString()) as XriIdentifier; + string objString = obj.ToString(); + ErrorUtilities.VerifyInternal(!string.IsNullOrEmpty(objString), "Identifier.ToString() returned a null or empty string."); + other = Identifier.Parse(objString) as XriIdentifier; } if (other == null) { return false; diff --git a/src/DotNetOpenAuth/Reporting.cs b/src/DotNetOpenAuth/Reporting.cs index 4e4bbf5..2235986 100644 --- a/src/DotNetOpenAuth/Reporting.cs +++ b/src/DotNetOpenAuth/Reporting.cs @@ -8,6 +8,7 @@ namespace DotNetOpenAuth { using System; using System.Collections.Generic; using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Globalization; using System.IO; @@ -88,6 +89,8 @@ namespace DotNetOpenAuth { /// <summary> /// Initializes static members of the <see cref="Reporting"/> class. /// </summary> + [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "We do more than field initialization here.")] + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Reporting MUST NOT cause unhandled exceptions.")] static Reporting() { Enabled = DotNetOpenAuthSection.Configuration.Reporting.Enabled; if (Enabled) { @@ -446,6 +449,7 @@ namespace DotNetOpenAuth { /// <summary> /// Sends the stats report asynchronously, and careful to not throw any unhandled exceptions. /// </summary> + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Unhandled exceptions MUST NOT be thrown from here.")] private static void SendStatsAsync() { // Do it on a background thread since it could take a while and we // don't want to slow down this request we're borrowing. @@ -635,6 +639,7 @@ namespace DotNetOpenAuth { /// </summary> public void Dispose() { this.Dispose(true); + GC.SuppressFinalize(this); } #endregion @@ -775,6 +780,7 @@ namespace DotNetOpenAuth { /// </summary> public void Dispose() { this.Dispose(true); + GC.SuppressFinalize(this); } #endregion diff --git a/src/DotNetOpenAuth/Yadis/Yadis.cs b/src/DotNetOpenAuth/Yadis/Yadis.cs index 589a155..f1c8be3 100644 --- a/src/DotNetOpenAuth/Yadis/Yadis.cs +++ b/src/DotNetOpenAuth/Yadis/Yadis.cs @@ -138,6 +138,8 @@ namespace DotNetOpenAuth.Yadis { internal static IncomingWebResponse Request(IDirectWebRequestHandler requestHandler, Uri uri, bool requireSsl, params string[] acceptTypes) { Contract.Requires<ArgumentNullException>(requestHandler != null); Contract.Requires<ArgumentNullException>(uri != null); + Contract.Ensures(Contract.Result<IncomingWebResponse>() != null); + Contract.Ensures(Contract.Result<IncomingWebResponse>().ResponseStream != null); HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri); request.CachePolicy = IdentifierDiscoveryCachePolicy; diff --git a/src/version.txt b/src/version.txt index 1809198..1545d96 100644 --- a/src/version.txt +++ b/src/version.txt @@ -1 +1 @@ -3.4.0 +3.5.0 |