summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.OpenId
diff options
context:
space:
mode:
Diffstat (limited to 'src/DotNetOpenAuth.OpenId')
-rw-r--r--src/DotNetOpenAuth.OpenId/Configuration/HostMetaDiscoveryElement.cs37
-rw-r--r--src/DotNetOpenAuth.OpenId/Configuration/OpenIdRelyingPartyElement.cs17
-rw-r--r--src/DotNetOpenAuth.OpenId/DotNetOpenAuth.OpenId.csproj1
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Association.cs2
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs7
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionArgumentsManager.cs2
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyResponse.cs8
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsRequest.cs4
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs26
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Constants.cs33
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/HmacShaAssociation.cs21
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/Realm.cs6
-rw-r--r--src/DotNetOpenAuth.OpenId/OpenId/UriIdentifier.cs5
13 files changed, 135 insertions, 34 deletions
diff --git a/src/DotNetOpenAuth.OpenId/Configuration/HostMetaDiscoveryElement.cs b/src/DotNetOpenAuth.OpenId/Configuration/HostMetaDiscoveryElement.cs
new file mode 100644
index 0000000..437b12f
--- /dev/null
+++ b/src/DotNetOpenAuth.OpenId/Configuration/HostMetaDiscoveryElement.cs
@@ -0,0 +1,37 @@
+//-----------------------------------------------------------------------
+// <copyright file="HostMetaDiscoveryElement.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Configuration {
+ using System.Configuration;
+
+ /// <summary>
+ /// The configuration element that can adjust how hostmeta discovery works.
+ /// </summary>
+ internal class HostMetaDiscoveryElement : ConfigurationElement {
+ /// <summary>
+ /// The property name for enableCertificateValidationCache.
+ /// </summary>
+ private const string EnableCertificateValidationCacheConfigName = "enableCertificateValidationCache";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HostMetaDiscoveryElement"/> class.
+ /// </summary>
+ public HostMetaDiscoveryElement() {
+ }
+
+ /// <summary>
+ /// Gets or sets a value indicating whether validated certificates should be cached and not validated again.
+ /// </summary>
+ /// <remarks>
+ /// This helps to avoid unexplained 5-10 second delays in certificate validation for Google Apps for Domains that impact some servers.
+ /// </remarks>
+ [ConfigurationProperty(EnableCertificateValidationCacheConfigName, DefaultValue = false)]
+ public bool EnableCertificateValidationCache {
+ get { return (bool)this[EnableCertificateValidationCacheConfigName]; }
+ set { this[EnableCertificateValidationCacheConfigName] = value; }
+ }
+ }
+}
diff --git a/src/DotNetOpenAuth.OpenId/Configuration/OpenIdRelyingPartyElement.cs b/src/DotNetOpenAuth.OpenId/Configuration/OpenIdRelyingPartyElement.cs
index 749659e..8af1129 100644
--- a/src/DotNetOpenAuth.OpenId/Configuration/OpenIdRelyingPartyElement.cs
+++ b/src/DotNetOpenAuth.OpenId/Configuration/OpenIdRelyingPartyElement.cs
@@ -47,9 +47,15 @@ namespace DotNetOpenAuth.Configuration {
private const string DiscoveryServicesElementName = "discoveryServices";
/// <summary>
+ /// The name of the &lt;hostMetaDiscovery&gt; sub-element.
+ /// </summary>
+ private const string HostMetaDiscoveryElementName = "hostMetaDiscovery";
+
+ /// <summary>
/// The built-in set of identifier discovery services.
/// </summary>
- private static readonly TypeConfigurationCollection<IIdentifierDiscoveryService> defaultDiscoveryServices = new TypeConfigurationCollection<IIdentifierDiscoveryService>(new Type[] { typeof(UriDiscoveryService), typeof(XriDiscoveryProxyService) });
+ private static readonly TypeConfigurationCollection<IIdentifierDiscoveryService> defaultDiscoveryServices =
+ new TypeConfigurationCollection<IIdentifierDiscoveryService>(new Type[] { typeof(UriDiscoveryService), typeof(XriDiscoveryProxyService) });
/// <summary>
/// Initializes a new instance of the <see cref="OpenIdRelyingPartyElement"/> class.
@@ -98,6 +104,15 @@ namespace DotNetOpenAuth.Configuration {
}
/// <summary>
+ /// Gets or sets the host meta discovery configuration element.
+ /// </summary>
+ [ConfigurationProperty(HostMetaDiscoveryElementName)]
+ internal HostMetaDiscoveryElement HostMetaDiscovery {
+ get { return (HostMetaDiscoveryElement)this[HostMetaDiscoveryElementName] ?? new HostMetaDiscoveryElement(); }
+ set { this[HostMetaDiscoveryElementName] = value; }
+ }
+
+ /// <summary>
/// Gets or sets the services to use for discovering service endpoints for identifiers.
/// </summary>
/// <remarks>
diff --git a/src/DotNetOpenAuth.OpenId/DotNetOpenAuth.OpenId.csproj b/src/DotNetOpenAuth.OpenId/DotNetOpenAuth.OpenId.csproj
index 95dccc1..75bd113 100644
--- a/src/DotNetOpenAuth.OpenId/DotNetOpenAuth.OpenId.csproj
+++ b/src/DotNetOpenAuth.OpenId/DotNetOpenAuth.OpenId.csproj
@@ -22,6 +22,7 @@
<ItemGroup>
<Compile Include="Configuration\AssociationTypeCollection.cs" />
<Compile Include="Configuration\AssociationTypeElement.cs" />
+ <Compile Include="Configuration\HostMetaDiscoveryElement.cs" />
<Compile Include="Configuration\OpenIdElement.cs" />
<Compile Include="Configuration\OpenIdProviderElement.cs" />
<Compile Include="Configuration\OpenIdProviderSecuritySettingsElement.cs" />
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Association.cs b/src/DotNetOpenAuth.OpenId/OpenId/Association.cs
index 764f4fa..a0f5bae 100644
--- a/src/DotNetOpenAuth.OpenId/OpenId/Association.cs
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Association.cs
@@ -240,7 +240,7 @@ namespace DotNetOpenAuth.OpenId {
/// A hash code for the current <see cref="T:System.Object"/>.
/// </returns>
public override int GetHashCode() {
- HMACSHA1 hmac = new HMACSHA1(this.SecretKey);
+ var hmac = HmacAlgorithms.Create(HmacAlgorithms.HmacSha1, this.SecretKey);
try {
CryptoStream cs = new CryptoStream(Stream.Null, hmac, CryptoStreamMode.Write);
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs
index ec16fae..912a322 100644
--- a/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs
+++ b/src/DotNetOpenAuth.OpenId/OpenId/ChannelElements/ReturnToSignatureBindingElement.cs
@@ -195,9 +195,14 @@ namespace DotNetOpenAuth.OpenId.ChannelElements {
try {
if (cryptoKey == null) {
cryptoKey = this.cryptoKeyStore.GetKey(SecretUri.AbsoluteUri, returnToParameters[ReturnToSignatureHandleParameterName]);
+ ErrorUtilities.VerifyProtocol(
+ cryptoKey != null,
+ MessagingStrings.MissingDecryptionKeyForHandle,
+ SecretUri.AbsoluteUri,
+ returnToParameters[ReturnToSignatureHandleParameterName]);
}
- using (var signer = new HMACSHA256(cryptoKey.Key)) {
+ using (var signer = HmacAlgorithms.Create(HmacAlgorithms.HmacSha256, cryptoKey.Key)) {
signature = signer.ComputeHash(bytesToSign);
}
} catch (ProtocolException ex) {
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionArgumentsManager.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionArgumentsManager.cs
index 5cd4904..1d795da 100644
--- a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionArgumentsManager.cs
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ExtensionArgumentsManager.cs
@@ -21,7 +21,7 @@ namespace DotNetOpenAuth.OpenId.Extensions {
/// match to namespaces for backward compatibility with other OpenID libraries.
/// </summary>
private static readonly Dictionary<string, string> typeUriToAliasAffinity = new Dictionary<string, string> {
- { Extensions.SimpleRegistration.Constants.sreg_ns, Extensions.SimpleRegistration.Constants.sreg_compatibility_alias },
+ { Extensions.SimpleRegistration.Constants.TypeUris.Standard, Extensions.SimpleRegistration.Constants.sreg_compatibility_alias },
{ Extensions.ProviderAuthenticationPolicy.Constants.TypeUri, Extensions.ProviderAuthenticationPolicy.Constants.CompatibilityAlias },
};
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyResponse.cs
index 373134d..880a25e 100644
--- a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyResponse.cs
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/ProviderAuthenticationPolicy/PolicyResponse.cs
@@ -71,10 +71,10 @@ namespace DotNetOpenAuth.OpenId.Extensions.ProviderAuthenticationPolicy {
/// actively authenticated to the OP in a manner fitting the asserted policies.
/// </summary>
/// <remarks>
- /// If the RP's request included the "openid.max_auth_age" parameter
- /// then the OP MUST include "openid.auth_time" in its response.
- /// If "openid.max_auth_age" was not requested, the OP MAY choose to include
- /// "openid.auth_time" in its response.
+ /// If the RP's request included the "openid.pape.max_auth_age" parameter
+ /// then the OP MUST include "openid.pape.auth_time" in its response.
+ /// If "openid.pape.max_auth_age" was not requested, the OP MAY choose to include
+ /// "openid.pape.auth_time" in its response.
/// </remarks>
[MessagePart("auth_time", Encoder = typeof(DateTimeEncoder))]
public DateTime? AuthenticationTimeUtc {
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsRequest.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsRequest.cs
index 9d418ee..ab08cbb 100644
--- a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsRequest.cs
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsRequest.cs
@@ -24,7 +24,7 @@ namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration {
/// The factory method that may be used in deserialization of this message.
/// </summary>
internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => {
- if (typeUri == Constants.sreg_ns && isProviderRole) {
+ if (typeUri == Constants.TypeUris.Standard && isProviderRole) {
return new ClaimsRequest(typeUri);
}
@@ -41,7 +41,7 @@ namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration {
/// Initializes a new instance of the <see cref="ClaimsRequest"/> class.
/// </summary>
public ClaimsRequest()
- : base(new Version(1, 0), Constants.sreg_ns, Constants.AdditionalTypeUris) {
+ : base(new Version(1, 0), Constants.TypeUris.Standard, Constants.AdditionalTypeUris) {
}
/// <summary>
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs
index 999fe8d..a313519 100644
--- a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/ClaimsResponse.cs
@@ -27,7 +27,7 @@ namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration {
/// The factory method that may be used in deserialization of this message.
/// </summary>
internal static readonly StandardOpenIdExtensionFactory.CreateDelegate Factory = (typeUri, data, baseMessage, isProviderRole) => {
- if ((typeUri == Constants.sreg_ns || Array.IndexOf(Constants.AdditionalTypeUris, typeUri) >= 0) && !isProviderRole) {
+ if ((typeUri == Constants.TypeUris.Standard || Array.IndexOf(Constants.AdditionalTypeUris, typeUri) >= 0) && !isProviderRole) {
return new ClaimsResponse(typeUri);
}
@@ -55,10 +55,11 @@ namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration {
private CultureInfo culture;
/// <summary>
- /// Initializes a new instance of the <see cref="ClaimsResponse"/> class.
+ /// Initializes a new instance of the <see cref="ClaimsResponse"/> class
+ /// using the most common, and spec prescribed type URI.
/// </summary>
- internal ClaimsResponse()
- : this(Constants.sreg_ns) {
+ public ClaimsResponse()
+ : this(Constants.TypeUris.Standard) {
}
/// <summary>
@@ -67,8 +68,10 @@ namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration {
/// <param name="typeUriToUse">
/// The type URI that must be used to identify this extension in the response message.
/// This value should be the same one the relying party used to send the extension request.
+ /// Commonly used type URIs supported by relying parties are defined in the
+ /// <see cref="Constants.TypeUris"/> class.
/// </param>
- internal ClaimsResponse(string typeUriToUse)
+ public ClaimsResponse(string typeUriToUse = Constants.TypeUris.Standard)
: base(new Version(1, 0), typeUriToUse, Constants.AdditionalTypeUris) {
Requires.NotNullOrEmpty(typeUriToUse, "typeUriToUse");
}
@@ -189,7 +192,7 @@ namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration {
}
/// <summary>
- /// Gets or sets a combination o the language and country of the user.
+ /// Gets or sets a combination of the language and country of the user.
/// </summary>
[XmlIgnore]
public CultureInfo Culture {
@@ -200,7 +203,16 @@ namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration {
if (!string.IsNullOrEmpty(this.Country)) {
cultureString += "-" + this.Country;
}
- this.culture = CultureInfo.GetCultureInfo(cultureString);
+
+ // language-country may not always form a recongized valid culture.
+ // For instance, a Google OpenID Provider can return a random combination
+ // of language and country based on user settings.
+ try {
+ this.culture = CultureInfo.GetCultureInfo(cultureString);
+ } catch (ArgumentException) { // CultureNotFoundException derives from this, and .NET 3.5 throws the base type
+ // Fallback to just reporting a culture based on language.
+ this.culture = CultureInfo.GetCultureInfo(this.Language);
+ }
}
return this.culture;
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Constants.cs b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Constants.cs
index 8325b0c..30cd748 100644
--- a/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Constants.cs
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Extensions/SimpleRegistration/Constants.cs
@@ -13,10 +13,31 @@ namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration {
/// <summary>
/// Simple Registration constants
/// </summary>
- internal static class Constants {
- internal const string sreg_ns = "http://openid.net/extensions/sreg/1.1";
- internal const string sreg_ns10 = "http://openid.net/sreg/1.0";
- internal const string sreg_ns11other = "http://openid.net/sreg/1.1";
+ public static class Constants {
+ /// <summary>
+ /// Commonly used type URIs to represent the Simple Registration extension.
+ /// </summary>
+ public static class TypeUris {
+ /// <summary>
+ /// The URI "http://openid.net/extensions/sreg/1.1".
+ /// </summary>
+ /// <remarks>
+ /// This is the type URI prescribed by the Simple Registration 1.1 spec.
+ /// http://openid.net/specs/openid-simple-registration-extension-1_1-01.html#anchor3
+ /// </remarks>
+ public const string Standard = "http://openid.net/extensions/sreg/1.1";
+
+ /// <summary>
+ /// The URI "http://openid.net/sreg/1.0"
+ /// </summary>
+ public const string Variant10 = "http://openid.net/sreg/1.0";
+
+ /// <summary>
+ /// The URI "http://openid.net/sreg/1.1"
+ /// </summary>
+ public const string Variant11 = "http://openid.net/sreg/1.1";
+ }
+
internal const string sreg_compatibility_alias = "sreg";
internal const string policy_url = "policy_url";
internal const string optional = "optional";
@@ -39,8 +60,8 @@ namespace DotNetOpenAuth.OpenId.Extensions.SimpleRegistration {
/// Additional type URIs that this extension is sometimes known by remote parties.
/// </summary>
internal static readonly string[] AdditionalTypeUris = new string[] {
- Constants.sreg_ns10,
- Constants.sreg_ns11other,
+ Constants.TypeUris.Variant10,
+ Constants.TypeUris.Variant11,
};
}
}
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/HmacShaAssociation.cs b/src/DotNetOpenAuth.OpenId/OpenId/HmacShaAssociation.cs
index 5e3553d..bf0111d 100644
--- a/src/DotNetOpenAuth.OpenId/OpenId/HmacShaAssociation.cs
+++ b/src/DotNetOpenAuth.OpenId/OpenId/HmacShaAssociation.cs
@@ -226,22 +226,22 @@ namespace DotNetOpenAuth.OpenId {
private static HmacSha[] CreateAssociationTypes() {
return new[] {
new HmacSha {
- CreateHasher = secretKey => new HMACSHA512(secretKey),
+ HmacAlgorithmName = HmacAlgorithms.HmacSha384,
GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA512,
BaseHashAlgorithm = SHA512.Create(),
},
new HmacSha {
- CreateHasher = secretKey => new HMACSHA384(secretKey),
+ HmacAlgorithmName = HmacAlgorithms.HmacSha384,
GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA384,
BaseHashAlgorithm = SHA384.Create(),
},
new HmacSha {
- CreateHasher = secretKey => new HMACSHA256(secretKey),
+ HmacAlgorithmName = HmacAlgorithms.HmacSha256,
GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA256,
BaseHashAlgorithm = SHA256.Create(),
},
new HmacSha {
- CreateHasher = secretKey => new HMACSHA1(secretKey),
+ HmacAlgorithmName = HmacAlgorithms.HmacSha1,
GetAssociationType = protocol => protocol.Args.SignatureAlgorithm.HMAC_SHA1,
BaseHashAlgorithm = SHA1.Create(),
},
@@ -258,9 +258,9 @@ namespace DotNetOpenAuth.OpenId {
internal Func<Protocol, string> GetAssociationType { get; set; }
/// <summary>
- /// Gets or sets a function that will create the <see cref="HashAlgorithm"/> using a given shared secret for the mac.
+ /// Gets or sets the name of the HMAC-SHA algorithm. (e.g. "HMAC-SHA256")
/// </summary>
- internal Func<byte[], HashAlgorithm> CreateHasher { get; set; }
+ internal string HmacAlgorithmName { get; set; }
/// <summary>
/// Gets or sets the base hash algorithm.
@@ -271,6 +271,15 @@ namespace DotNetOpenAuth.OpenId {
/// Gets the size of the hash (in bytes).
/// </summary>
internal int SecretLength { get { return this.BaseHashAlgorithm.HashSize / 8; } }
+
+ /// <summary>
+ /// Creates the <see cref="HashAlgorithm"/> using a given shared secret for the mac.
+ /// </summary>
+ /// <param name="secret">The HMAC secret.</param>
+ /// <returns>The algorithm.</returns>
+ internal HashAlgorithm CreateHasher(byte[] secret) {
+ return HmacAlgorithms.Create(this.HmacAlgorithmName, secret);
+ }
}
}
} \ No newline at end of file
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/Realm.cs b/src/DotNetOpenAuth.OpenId/OpenId/Realm.cs
index 28e4df0..8f1baed 100644
--- a/src/DotNetOpenAuth.OpenId/OpenId/Realm.cs
+++ b/src/DotNetOpenAuth.OpenId/OpenId/Realm.cs
@@ -116,11 +116,7 @@ namespace DotNetOpenAuth.OpenId {
Requires.ValidState(HttpContext.Current != null && HttpContext.Current.Request != null, MessagingStrings.HttpContextRequired);
Contract.Ensures(Contract.Result<Realm>() != null);
- HttpRequestBase requestInfo = new HttpRequestWrapper(HttpContext.Current.Request);
- UriBuilder realmUrl = new UriBuilder(requestInfo.GetPublicFacingUrl());
- realmUrl.Path = HttpContext.Current.Request.ApplicationPath;
- realmUrl.Query = null;
- realmUrl.Fragment = null;
+ var realmUrl = new UriBuilder(MessagingUtilities.GetWebRoot());
// For RP discovery, the realm url MUST NOT redirect. To prevent this for
// virtual directory hosted apps, we need to make sure that the realm path ends
diff --git a/src/DotNetOpenAuth.OpenId/OpenId/UriIdentifier.cs b/src/DotNetOpenAuth.OpenId/OpenId/UriIdentifier.cs
index 631eab6..41417de 100644
--- a/src/DotNetOpenAuth.OpenId/OpenId/UriIdentifier.cs
+++ b/src/DotNetOpenAuth.OpenId/OpenId/UriIdentifier.cs
@@ -69,6 +69,11 @@ namespace DotNetOpenAuth.OpenId {
/// </remarks>
[SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "Some things just can't be done in a field initializer.")]
static UriIdentifier() {
+ if (Type.GetType("Mono.Runtime") != null) {
+ // Uri scheme registration doesn't work on mono.
+ return;
+ }
+
// Our first attempt to handle trailing periods in path segments is to leverage
// full trust if it's available to rewrite the rules.
// In fact this is the ONLY way in .NET 3.5 (and arguably in .NET 4.0) to send