Find WCF service certificate, using several search criteria in config file - c#

I'm wondering, is there any way to find WCF service certificate, using several search criteria in .config file?
E.g., if I want to find certificate by subject name, then my config will contain these lines:
<serviceCertificate findValue="host.domain.com"
storeLocation="LocalMachine"
storeName="My"
x509FindType="FindBySubjectName"/>
And what should I put in the config file, if I want to find a certificate by subject name and by application policy?
I know, that X509Certificate2Collection class allows this:
return store
.Certificates
.Find(X509FindType.FindByApplicationPolicy, "1.3.6.1.5.5.7.3.1", false)
.Find(X509FindType.FindBySubjectName, "host.domain.com", false)
.Cast<X509Certificate2>()
.SingleOrDefault();
And what about .config-files?

Write a custom service behavior and you will be able to provide your application policy AND subject name from configuration, executing two consecutive find on the certificate store.
public class ServerCertificateServiceBehavior : IServiceBehavior
{
private X509Certificate2 certificate;
public ServerCertificateServiceBehavior(StoreName storeName, StoreLocation storeLocation, string subjectName, string applicationPolicy)
{
X509Store store = new X509Store(storeName, storeLocation);
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
certificate = store
.Certificates
.Find(X509FindType.FindByApplicationPolicy, applicationPolicy, false)
.Find(X509FindType.FindBySubjectName, subjectName, false)
.Cast<X509Certificate2>()
.SingleOrDefault();
}
public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
{
serviceHostBase.Credentials.ServiceCertificate.Certificate = this.certificate;
}
public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { }
public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { }
}
The ExtensionElement :
public class ServerCertificateServiceBehaviorExtensionElement : BehaviorExtensionElement
{
[ConfigurationProperty("applicationPolicy", IsRequired = true)]
public string ApplicationPolicy
{
get
{
return (string)base["applicationPolicy"];
}
set
{
base["applicationPolicy"] = value;
}
}
[ConfigurationProperty("subjectName", IsRequired = true)]
public string SubjectName
{
get
{
return (string)base["subjectName"];
}
set
{
base["subjectName"] = value;
}
}
[ConfigurationProperty("storeLocation", DefaultValue = 2)]
public StoreLocation StoreLocation
{
get
{
return (StoreLocation)base["storeLocation"];
}
set
{
base["storeLocation"] = value;
}
}
[ConfigurationProperty("storeName", DefaultValue = 5)]
public StoreName StoreName
{
get
{
return (StoreName)base["storeName"];
}
set
{
base["storeName"] = value;
}
}
public override Type BehaviorType
{
get { return typeof(ServerCertificateServiceBehavior); }
}
protected override object CreateBehavior()
{
return new ServerCertificateServiceBehavior(
this.StoreName,
this.StoreLocation,
this.SubjectName,
this.ApplicationPolicy);
}
}
The configuration becomes :
<behaviors>
<serviceBehaviors>
<behavior name="YourServiceBehaviorConfiguration">
<!-- ... -->
<serverCertificate storeLocation="LocalMachine"
storeName="My"
subjectName="host.domain.com"
applicationPolicy="1.3.6.1.5.5.7.3.1" />
</behavior>
</serviceBehaviors>
</behaviors>
<extensions>
<behaviorExtensions>
<add name="serverCertificate" type="Extensions.ServerCertificateServiceBehaviorExtensionElement, Extensions, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</behaviorExtensions>
</extensions>

Related

Is it possible to use both a certificate and UserName credential type with a service? C#

I am working on a project that makes requests to an API (which I think is WCF). The API requires an X509 certificate as a credential to be able to make requests. After referencing the service for the client and trying to make some requests, I was told that my requests were also missing a UserName header credential in order to authenticate with the API. Is it possible to use both types of credentials in a request and if so would anyone know how to set it up? Sorry if this post is poorly made, this is my first post on this website.
Edit: The username header that I need is at the SOAP message level. A header in the actual SOAP xml request.
This is my program code (with personal info taken out):
using System;
using System.Security.Cryptography.X509Certificates;
using EFM_User_Service_Test.EfmUserService;
namespace EFM_User_Service_Test
{
class Program
{
static void Main(string[] args)
{
X509Certificate2 cert = new X509Certificate2("path to pfx file", "password for file");
EfmUserServiceClient client = new EfmUserServiceClient();
client.ClientCredentials.ClientCertificate.Certificate = cert;
client.ClientCredentials.UserName.UserName = "test#email.com";
client.ClientCredentials.UserName.Password = "test password";
client.Open();
AuthenticateRequestType req = new AuthenticateRequestType();
req.Email = "test#email.com";
req.Password = "test password";
AuthenticateResponseType response = client.AuthenticateUser(req);
string user_id = "";
if (response.Error != null && response.Error.ErrorCode != "0")
{
Console.WriteLine(response.Error.ErrorCode);
Console.WriteLine(response.Error.ErrorText);
} else
{
Console.WriteLine("{0} {1} is now signed in",response.FirstName,response.LastName);
Console.WriteLine("authenticated user id: "+response.UserID);
user_id = response.UserID;
}
GetUserRequestType user_req = new GetUserRequestType();
user_req.UserID = user_id;
var user_response = client.GetUser(user_req);
if (user_response.Error != null)
{
Console.WriteLine("Error Code: "+user_response.Error.ErrorCode);
Console.WriteLine("Error Text: "+user_response.Error.ErrorText);
} else
{
Console.WriteLine(user_response.User.Email);
Console.WriteLine(user_response.User.FirstName);
Console.WriteLine(user_response.User.LastName);
Console.WriteLine(user_response.User.Email);
}
client.Close();
}
}
}
and this is my App.config file for the program which seemed to be automatically generated when I added the service reference
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_IEfmUserService" messageEncoding="Mtom">
<security mode="TransportWithMessageCredential">
<message clientCredentialType="Certificate" />
</security>
</binding>
<binding name="BasicHttpBinding_IEfmUserService1" messageEncoding="Mtom">
<security mode="Transport" />
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="in place of actual address"
binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IEfmUserService"
contract="EfmUserService.IEfmUserService" name="BasicHttpBinding_IEfmUserService" />
</client>
</system.serviceModel>
</configuration>
In time I solved this by adding a ServiceBehavior to my app.config file. Note that in my case it's a WPF application.
<configuration>
<system.serviceModel>
<extensions>
<behaviorExtensions>
<!-- Add this -->
<add name="basicAuthenticationBehavior" type="ProjectNamespace.Behaviors.BasicAuthenticationBehaviorExtensionElement, ProjectAssemblyName, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>
<behaviors>
<!-- Add this -->
<endpointBehaviors>
<behavior name="basicAuthenticationEndpointBehavior">
<basicAuthenticationBehavior />
</behavior>
</endpointBehaviors>
</behaviors>
<bindings>
<basicHttpBinding>
<!-- After adding your service reference, your bindings appear here -->
<binding name="DMSinterfaceSoap">
<security mode="Transport" />
</binding>
</basicHttpBinding>
</bindings>
<client>
<!-- After adding your service reference, your endpoints appear here -->
<endpoint address="https://soa.example.com/dmsinterface/DMS_service.asmx" binding="basicHttpBinding" behaviorConfiguration="basicAuthenticationEndpointBehavior" ... />
</client>
</system.serviceModel>
<configuration>
The BasicAuthenticationBehaviorExtensionElement looks like this
// Add System.Configuration reference to your ExecutingAssembly
internal class BasicAuthenticationBehaviorExtensionElement : BehaviorExtensionElement
{
/// <summary>Set this property if you want to send specific headers along with each request</summary>
public static TCredentials Credentials { internal get; set; }
public override Type BehaviorType
{
get { return typeof(BasicAuthenticationEndpointBehavior); }
}
protected override object CreateBehavior()
{
return new BasicAuthenticationEndpointBehavior();
}
}
class BasicAuthenticationEndpointBehavior : IEndpointBehavior
{
public BasicAuthenticationEndpointBehavior()
{
}
public void Validate(ServiceEndpoint endpoint) { }
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { }
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { }
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
clientRuntime.MessageInspectors.Add(new BasicAuthenticationClientMessageInspector());
}
}
class BasicAuthenticationClientMessageInspector : IClientMessageInspector
{
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
if (BasicAuthenticationBehaviorExtensionElement.Credentials != null)
{
SetRequestHeader(ref request, "USERID", BasicAuthenticationBehaviorExtensionElement.Credentials.Credential.UserName);
SetRequestHeader(ref request, "PSWD", BasicAuthenticationBehaviorExtensionElement.Credentials.Credential.Password);
}
return null;
}
private void SetRequestHeader(ref Message request, string key, string value)
{
object httpRequestMessageObject;
if (request.Properties.TryGetValue(HttpRequestMessageProperty.Name, out httpRequestMessageObject))
{
var httpRequestMessage = httpRequestMessageObject as HttpRequestMessageProperty;
if (httpRequestMessage != null)
{
httpRequestMessage.Headers[key] = (value ?? string.Empty);
}
else
{
httpRequestMessage = new HttpRequestMessageProperty();
httpRequestMessage.Headers.Add(key, (value ?? string.Empty));
request.Properties[HttpRequestMessageProperty.Name] = httpRequestMessage;
}
}
else
{
var httpRequestMessage = new HttpRequestMessageProperty();
httpRequestMessage.Headers.Add(key, (value ?? string.Empty));
request.Properties.Add(HttpRequestMessageProperty.Name, httpRequestMessage);
}
}
public void AfterReceiveReply(ref Message reply, object correlationState) { }
}
This adds the authentication header to each request, based on the object stored in static variable.
Then you can send the soap call with the authentication certificate:
#region Get Certificate
var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
var foundCerts = store.Certificates.Find(X509FindType.FindBySerialNumber, credential.CertificateSerial, false);
if (foundCerts.Count == 0)
throw new Exceptions.NoValidCertificateException();
#endregion
#region Setup client
// Check the App.config for available endpoint configuration names
var client = new MySoapService.DMSinterfaceSoapClient("DMSinterfaceSoap.0");
if (client.Endpoint.Binding is System.ServiceModel.BasicHttpBinding basicBinding)
{
// DMSinterfaceSoap and DMSinterfaceSoap.0 are BasicHttpBindings
// The ClientCredentialType is by default set to None. You should be able to combine HttpClientCredentialTypes but sadly that's not possible.
basicBinding.Security.Transport.ClientCredentialType = System.ServiceModel.HttpClientCredentialType.Certificate;
}
else if (client.Endpoint.Binding is System.ServiceModel.Channels.CustomBinding customBinding)
{
// DMSinterfaceSoap12 and DMSinterfaceSoap12.0 are CustomBindings
// Whatever additional configuration needs to be performed
}
#endregion
// Set the USERID and PSWD header
BasicAuthenticationBehaviorExtensionElement.Credentials.Credential = credential;
// Send certificate along with request
client.ClientCredentials.ClientCertificate.Certificate = foundCerts[0];
var error = client.GetResult(req, out res);
// Clear the USERID and PSWD header
BasicAuthenticationBehaviorExtensionElement.Credentials.Credential = null;

Error control/handler (capture) with Behavior in wcf. Business layer

I have a project (backend) where it receives a call in the service layer, but it also makes calls to another API in the business layer.
I have added a behaviour to those calls so I do not have to make changes to several places in my code.
My Behaviour.config (the importatn is ExternalDependencyBehavior):
<?xml version="1.0" encoding="utf-8"?>
<behaviors>
<endpointBehaviors>
<!-- SPECIFIC BEHAVIOR EXTENSION FOR JSON URL DESERIALIZER-->
<behavior name="ServiceRESTBehavior">
<ConverterWebHttpBehaviourExtension />
</behavior>
<behavior name="ServiceSOAPBehavior" />
<behavior name="ExternalDependencyBehavior">
<ExternalClientBehavior/>
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior name="DefaultBehaviour">
<serviceMetadata httpsGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
</behavior>
</serviceBehaviors>
</behaviors>
My code: (one method in business layer that call a other API external)
protected virtual Vendor.Sinacofi.SinacofiService.RespuestaSNPV1201 RequestToVendor(VendorWebService vendor, string clientFiscalId, BasicSearchCriterion criteria)
{
Vendor.Sinacofi.SinacofiService.RespuestaSNPV1201 result = new Vendor.Sinacofi.SinacofiService.RespuestaSNPV1201();
string errorLogProvider = null;
try
{
using (Vendor.Sinacofi.SinacofiService.SNPV1201SoapClient client = new Vendor.Sinacofi.SinacofiService.SNPV1201SoapClient())
{
/*Credentials*/
result = client.ConsultaRutificacion(
client.ClientCredentials.HttpDigest.ClientCredential.UserName,
client.ClientCredentials.HttpDigest.ClientCredential.Password,
clientFiscalId,
criteria.Value
);
}
return result;
}
catch (AxrException)
{
throw;
}
catch (FaultException fex)
{
this.logProvider.WriteInErrorMode(GetType().Name, MethodBase.GetCurrentMethod().Name, fex);
throw this.exceptionProvider.Create(ErrorCode.---------, ExceptionType.BUSINESS, fex);
}
catch (Exception ex)
{
this.logProvider.WriteInErrorMode(GetType().Name, MethodBase.GetCurrentMethod().Name, ex);
throw this.exceptionProvider.Create(ErrorCode.-------------, ExceptionType.BUSINESS, ex);
}
}
In summary:
When I arrive at the call:
"client.ConsultaRutification"
My behaviour is executed, I add the code (2 files):
using System;
using System.ServiceModel.Channels;
using System.ServiceModel.Configuration;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
namespace SIPE.Search.Helpers
{
/// <summary>
/// Implements methods that can be used to extend run-time behavior for an endpoint in either a client application.
/// </summary>
public class ExternalClientBehavior : BehaviorExtensionElement
{
protected override object CreateBehavior()
{
return new ExternalClientBehaviorClass();
}
public override Type BehaviorType
{
get
{
return typeof(ExternalClientBehaviorClass);
}
}
/// <summary>
/// JSON REST[GET] Converter Behavior
/// </summary>
private class ExternalClientBehaviorClass : IEndpointBehavior
{
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
ExternalClientMessageInspector clientInspector = new ExternalClientMessageInspector(endpoint);
clientRuntime.MessageInspectors.Add(clientInspector);
foreach (ClientOperation op in clientRuntime.Operations)
{
op.ParameterInspectors.Add(clientInspector);
}
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
endpointDispatcher.ChannelDispatcher.ErrorHandlers.Add(new ExternalClientMessageInspector(endpoint));
}
public void Validate(ServiceEndpoint endpoint)
{
}
}
}
}
And file two:
namespace SIPE.Search.Helpers
{
/// <summary>
/// Intercepts send requests.
/// </summary>
public class ExternalClientMessageInspector : IClientMessageInspector, IParameterInspector, IErrorHandler
{
private ServiceEndpoint Endpoint { get; set; }
Dictionary<string, object> inputsParam;
public ExternalClientMessageInspector(ServiceEndpoint endpoint)
{
//empty
}
public void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState)
{
((Dictionary<string, string>)(System.Web.HttpContext.Current.Items[operationName])).Add("OutputParam", JsonConvert.SerializeObject(returnValue));
}
public void AfterReceiveReply(ref Message reply, object correlationState)
{
//code not necessary
}
public object BeforeCall(string operationName, object[] inputs)
{
// code not neccesary
}
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
MessageBuffer buffer = request.CreateBufferedCopy(Int32.MaxValue);
Message copy = buffer.CreateMessage(); // Create a copy to work with
request = buffer.CreateMessage(); // Restore the original message
return copy.GetReaderAtBodyContents().Name;
}
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
// no implement
}
public bool HandleError(Exception error)
{
// no implement
return true;
}
public void NotifyError()
{
}
}
}
I need to be able to capture the errors, (for example 404 and others) of my calls within my behaviour.
I have tried to add several interfaces, but without success
The error is captured by the catch of the RequestToVendor method that is in the business layer.
But I need to be able to capture it in my behaviour.
thank you

The remote endpoint requested an address for acknowledgements that is not the same as the address for application messages

I am in the process of writing a WCF Duplex service for a chat application with a WPF client. The service code is below
IChatCallback
public interface IChatCallback
{
#region Public Methods and Operators
[OperationContract(IsOneWay = true)]
void Receive(Person sender, string message);
[OperationContract(IsOneWay = true)]
void ReceiveWhisper(Person sender, string message);
[OperationContract(IsOneWay = true)]
void UserEnter(Person person);
[OperationContract(IsOneWay = true)]
void UserLeave(Person person);
#endregion
}
IChatService
[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IChatCallback))]
public interface IChatService
{
#region Public Methods and Operators
[OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = false)]
void Say(string message);
[OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = false)]
void Whisper(string to, string message);
[OperationContract(IsOneWay = false, IsInitiating = true, IsTerminating = false)]
Person[] Join(Person person);
[OperationContract(IsOneWay = true, IsInitiating = false, IsTerminating = true)]
void Leave();
#endregion
}
ChatService
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Multiple)]
public class ChatService : IChatService
{
#region Static Fields
private static object _syncObj = new object();
private static Dictionary<Person, ChatEventHandler> _chatters = new Dictionary<Person, ChatEventHandler>();
#endregion
#region Fields
private IChatCallback _callback = null;
private ChatEventHandler _myEventHandler;
private Person _person;
#endregion
#region Delegates
public delegate void ChatEventHandler(object sender, ChatEventArgs e);
#endregion
#region Public Events
public static event ChatEventHandler ChatEvent;
#endregion
#region Public Methods and Operators
public void Say(string message)
{
ChatEventArgs e = new ChatEventArgs(MessageType.Receive, this._person, message);
this.BroadcastMessage(e);
}
public void Whisper(string to, string message)
{
ChatEventArgs e = new ChatEventArgs(MessageType.ReceiveWhisper, this._person, message);
try
{
ChatEventHandler chatterTo;
lock (_syncObj)
{
chatterTo = this.GetPersonHandler(to);
if (chatterTo == null)
{
throw new KeyNotFoundException(
string.Format(
CultureInfo.InvariantCulture,
"The person with name [{0}] could not be found",
to));
}
}
chatterTo.BeginInvoke(this, e, new AsyncCallback(EndAsync), null);
}
catch (KeyNotFoundException)
{
}
}
public Person[] Join(Person person)
{
bool userAdded = false;
this._myEventHandler = new ChatEventHandler(this.MyEventHandler);
lock (_syncObj)
{
if (!this.CheckIfPersonExists(person.Name) && person != null)
{
this._person = person;
_chatters.Add(person, this.MyEventHandler);
userAdded = true;
}
}
if (userAdded)
{
this._callback = OperationContext.Current.GetCallbackChannel<IChatCallback>();
ChatEventArgs e = new ChatEventArgs(MessageType.UserEnter, this._person);
this.BroadcastMessage(e);
ChatEvent += this._myEventHandler;
Person[] list = new Person[_chatters.Count];
lock (_syncObj)
{
_chatters.Keys.CopyTo(list, 0);
}
return list;
}
else
{
return null;
}
}
public void Leave()
{
if (this._person == null)
{
return;
}
ChatEventHandler chatterToRemove = this.GetPersonHandler(this._person.Name);
lock (_syncObj)
{
_chatters.Remove(this._person);
}
ChatEvent -= chatterToRemove;
ChatEventArgs e = new ChatEventArgs(MessageType.UserLeave, this._person);
this.BroadcastMessage(e);
}
#endregion
private void MyEventHandler(object sender, ChatEventArgs e)
{
try
{
switch (e.MessageType)
{
case MessageType.Receive:
this._callback.Receive(e.Person, e.Message);
break;
case MessageType.ReceiveWhisper:
this._callback.ReceiveWhisper(e.Person, e.Message);
break;
case MessageType.UserEnter:
this._callback.UserEnter(e.Person);
break;
case MessageType.UserLeave:
this._callback.UserLeave(e.Person);
break;
}
}
catch
{
this.Leave();
}
}
private void BroadcastMessage(ChatEventArgs e)
{
ChatEventHandler temp = ChatEvent;
if (temp != null)
{
foreach (ChatEventHandler handler in temp.GetInvocationList())
{
handler.BeginInvoke(this, e, new AsyncCallback(this.EndAsync), null);
}
}
}
private bool CheckIfPersonExists(string name)
{
foreach (Person p in _chatters.Keys)
{
if (p.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private void EndAsync(IAsyncResult ar)
{
ChatEventHandler d = null;
try
{
AsyncResult asres = (AsyncResult)ar;
d = (ChatEventHandler)asres.AsyncDelegate;
d.EndInvoke(ar);
}
catch
{
ChatEvent -= d;
}
}
private ChatEventHandler GetPersonHandler(string name)
{
foreach (Person p in _chatters.Keys)
{
if (p.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
{
ChatEventHandler chatTo = null;
_chatters.TryGetValue(p, out chatTo);
return chatTo;
}
}
return null;
}
}
This is hosted in a console application with an endpoint of net.tcp://localhost:33333/chatservice using the netTcpBinding with the following binding configuration
<system.serviceModel>
<services>
<service name="Cleo.Services.Chat.ChatService" behaviorConfiguration="CleoChatBehavior">
<host>
<baseAddresses>
<add baseAddress="net.tcp://localhost:33333/chatservice"/>
</baseAddresses>
</host>
<endpoint address="" binding="netTcpBinding" bindingConfiguration="DuplexBinding" contract="Cleo.Services.Chat.IChatService"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="CleoChatBehavior">
<serviceThrottling maxConcurrentSessions="10000"/>
<serviceDebug includeExceptionDetailInFaults="true"/>
</behavior>
</serviceBehaviors>
</behaviors>
<bindings>
<netTcpBinding>
<binding name="DuplexBinding" maxBufferSize="67108864" maxReceivedMessageSize="67108864" maxBufferPoolSize="67108864" transferMode="Buffered" closeTimeout="00:00:10" openTimeout="00:00:10" receiveTimeout="00:20:00" sendTimeout="00:01:00" maxConnections="100">
<reliableSession enabled="true" inactivityTimeout="00:20:00" />
<security mode="None" />
<readerQuotas maxArrayLength="67108864" maxBytesPerRead="67108864" maxStringContentLength="67108864" />
</binding>
</netTcpBinding>
</bindings>
</system.serviceModel>
In my WPF client I have implemented a proxy to the service using svcutil which is below:
IChatServiceCallback
[GeneratedCode("System.ServiceModel", "4.0.0.0")]
public interface IChatServiceCallback
{
#region Public Methods and Operators
[OperationContract(IsOneWay = true, Action = "http://tempuri.org/IChatService/Receive")]
void Receive(Person sender, string message);
[OperationContract(IsOneWay = true, AsyncPattern = true, Action = "http://tempuri.org/IChatService/Receive")]
IAsyncResult BeginReceive(Person sender, string message, AsyncCallback callback, object asyncState);
void EndReceive(IAsyncResult result);
[OperationContract(IsOneWay = true, Action = "http://tempuri.org/IChatService/ReceiveWhisper")]
void ReceiveWhisper(Person sender, string message);
[OperationContract(IsOneWay = true, AsyncPattern = true,
Action = "http://tempuri.org/IChatService/ReceiveWhisper")]
IAsyncResult BeginReceiveWhisper(Person sender, string message, AsyncCallback callback, object asyncState);
void EndReceiveWhisper(IAsyncResult result);
[OperationContract(IsOneWay = true, Action = "http://tempuri.org/IChatService/UserEnter")]
void UserEnter(Person person);
[OperationContract(IsOneWay = true, AsyncPattern = true, Action = "http://tempuri.org/IChatService/UserEnter")]
IAsyncResult BeginUserEnter(Person person, AsyncCallback callback, object asyncState);
void EndUserEnter(IAsyncResult result);
[OperationContract(IsOneWay = true, Action = "http://tempuri.org/IChatService/UserLeave")]
void UserLeave(Person person);
[OperationContract(IsOneWay = true, AsyncPattern = true, Action = "http://tempuri.org/IChatService/UserLeave")]
IAsyncResult BeginUserLeave(Person person, AsyncCallback callback, object asyncState);
void EndUserLeave(IAsyncResult result);
#endregion
}
IChatService
[GeneratedCode("System.ServiceModel", "4.0.0.0")]
[ServiceContract(ConfigurationName = "IChatService", CallbackContract = typeof(IChatServiceCallback),
SessionMode = SessionMode.Required)]
public interface IChatService
{
#region Public Methods and Operators
[OperationContract(IsOneWay = true, IsInitiating = false, Action = "http://tempuri.org/IChatService/Say")]
void Say(string message);
[OperationContract(IsOneWay = true, IsInitiating = false, AsyncPattern = true,
Action = "http://tempuri.org/IChatService/Say")]
IAsyncResult BeginSay(string message, AsyncCallback callback, object asyncState);
void EndSay(IAsyncResult result);
[OperationContract(IsOneWay = true, IsInitiating = false, Action = "http://tempuri.org/IChatService/Whisper")]
void Whisper(string to, string message);
[OperationContract(IsOneWay = true, IsInitiating = false, AsyncPattern = true,
Action = "http://tempuri.org/IChatService/Whisper")]
IAsyncResult BeginWhisper(string to, string message, AsyncCallback callback, object asyncState);
void EndWhisper(IAsyncResult result);
[OperationContract(Action = "http://tempuri.org/IChatService/Join",
ReplyAction = "http://tempuri.org/IChatService/JoinResponse")]
Person[] Join(Person person);
[OperationContract(AsyncPattern = true, Action = "http://tempuri.org/IChatService/Join",
ReplyAction = "http://tempuri.org/IChatService/JoinResponse")]
IAsyncResult BeginJoin(Person person, AsyncCallback callback, object asyncState);
Person[] EndJoin(IAsyncResult result);
[OperationContract(IsOneWay = true, IsTerminating = true, IsInitiating = false,
Action = "http://tempuri.org/IChatService/Leave")]
void Leave();
[OperationContract(IsOneWay = true, IsTerminating = true, IsInitiating = false, AsyncPattern = true,
Action = "http://tempuri.org/IChatService/Leave")]
IAsyncResult BeginLeave(AsyncCallback callback, object asyncState);
void EndLeave(IAsyncResult result);
#endregion
}
IChatServiceChannel
[GeneratedCode("System.ServiceModel", "4.0.0.0")]
public interface IChatServiceChannel : IChatService, IClientChannel
{
}
and ChatProxy
[DebuggerStepThrough]
[GeneratedCode("System.ServiceModel", "4.0.0.0")]
public class ChatProxy : DuplexClientBase<IChatService>, IChatService
{
#region Constructors and Destructors
public ChatProxy(InstanceContext callbackInstance)
: base(callbackInstance)
{
}
public ChatProxy(InstanceContext callbackInstance, string endpointConfigurationName)
: base(callbackInstance, endpointConfigurationName)
{
}
public ChatProxy(InstanceContext callbackInstance, string endpointConfigurationName, string remoteAddress)
: base(callbackInstance, endpointConfigurationName, remoteAddress)
{
}
public ChatProxy(
InstanceContext callbackInstance,
string endpointConfigurationName,
EndpointAddress remoteAddress)
: base(callbackInstance, endpointConfigurationName, remoteAddress)
{
}
public ChatProxy(InstanceContext callbackInstance, Binding binding, EndpointAddress remoteAddress)
: base(callbackInstance, binding, remoteAddress)
{
}
#endregion
#region Public Methods and Operators
public void Say(string message)
{
this.Channel.Say(message);
}
public IAsyncResult BeginSay(string message, AsyncCallback callback, object asyncState)
{
return this.Channel.BeginSay(message, callback, asyncState);
}
public void EndSay(IAsyncResult result)
{
this.Channel.EndSay(result);
}
public void Whisper(string to, string message)
{
this.Channel.Whisper(to, message);
}
public IAsyncResult BeginWhisper(string to, string message, AsyncCallback callback, object asyncState)
{
return this.Channel.BeginWhisper(to, message, callback, asyncState);
}
public void EndWhisper(IAsyncResult result)
{
this.Channel.EndWhisper(result);
}
public Person[] Join(Person person)
{
return this.Channel.Join(person);
}
public IAsyncResult BeginJoin(Person person, AsyncCallback callback, object asyncState)
{
return this.Channel.BeginJoin(person, callback, asyncState);
}
public Person[] EndJoin(IAsyncResult result)
{
return this.Channel.EndJoin(result);
}
public void Leave()
{
this.Channel.Leave();
}
public IAsyncResult BeginLeave(AsyncCallback callback, object asyncState)
{
return this.Channel.BeginLeave(callback, asyncState);
}
public void EndLeave(IAsyncResult result)
{
this.Channel.EndLeave(result);
}
#endregion
}
With the client configuration in the main application:
<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding name="CleoDefaultBinding" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00" bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard" maxBufferPoolSize="33554432" maxReceivedMessageSize="4194304" messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true" allowCookies="false">
<readerQuotas maxDepth="32" maxStringContentLength="4194304" maxArrayLength="32768" maxBytesPerRead="4096" maxNameTableCharCount="16384" />
<security mode="Transport">
<transport clientCredentialType="Certificate" />
</security>
</binding>
</wsHttpBinding>
<netTcpBinding>
<binding name="DuplexBinding" sendTimeout="00:00:30">
<reliableSession enabled="true"/>
<security mode="None"/>
</binding>
</netTcpBinding>
</bindings>
<client>
<!-- Cleo Chat Client -->
<endpoint name="CleoChatWcfServiceClient" address="net.tcp://localhost:33333/chatservice" binding="netTcpBinding" bindingConfiguration="DuplexBinding" contract="IChatService"/>
<endpoint address="net.tcp://localhost:51638/services/chat/wcf" binding="netTcpBinding" bindingConfiguration="DuplexBinding" contract="CleoChatClient.ICleoChatWcfService" name="chatWcfService" />
</client>
</system.serviceModel>
Ok, all good except for some reason i am getting an error when running the following code to connect to the service, the code is:
public class ProxySingleton : IChatServiceCallback
{
...
public void Connect(Person p)
{
var site = new InstanceContext(this);
this._proxy = new ChatProxy(site);
var iar = this._proxy.BeginJoin(p, this.OnEndJoin, null);
}
private void OnEndJoin(IAsyncResult ar)
{
try
{
var list = this._proxy.EndJoin(ar); --> Errors here!!
this.HandleEndJoin(list);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
...
}
The error i am getting is:
The remote endpoint requested an address for acknowledgements that is not the same as the address for application messages. The channel could not be opened because this is not supported. Ensure the endpoint address used to create the channel is identical to the one the remote endpoint was set up with.
My question (and sorry for the very long post but I am completely stuck on this) is simply whether anyone else has come across this and could point me towards an answer please?
EDIT:
I have updated to include the full serviceModel sections from the server and client and also updated the ProxySingleton to show that it does implement the callback interface
Here's a fully functional setup for your ChatService:
Host:
class ProgramHost
{
static void Main(string[] args)
{
try
{
ServiceHost host = new ServiceHost(typeof(ChatLib.ChatService));
host.Open();
Console.WriteLine(string.Format("WCF {0} host is running...", host.Description.ServiceType));
Console.WriteLine("Endpoints:");
foreach (ServiceEndpoint se in host.Description.Endpoints)
{
Console.WriteLine("***********************************************");
Console.WriteLine(string.Format("Address = {0}", se.Address));
Console.WriteLine(string.Format("Binding = {0}", se.Binding));
Console.WriteLine(string.Format("Contract = {0}", se.Contract.Name));
}
Console.WriteLine(string.Empty);
Console.WriteLine("Press <ENTER> to terminate.");
Console.ReadLine();
host.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
}
Client:
class ProgramClient
{
static void Main(string[] args)
{
try
{
if (args.Length != 1)
Console.WriteLine("usage: clientconsole username");
else
{
Person user = new Person(args[0]);
IChatServiceCallback callback = new SimpleChatCallback();
InstanceContext instanceContext = new InstanceContext(callback);
ChatServiceClient serviceProxy = new ChatServiceClient(instanceContext);
Console.WriteLine("Endpoint:");
Console.WriteLine("***********************************************");
Console.WriteLine(string.Format("Address = {0}", serviceProxy.Endpoint.Address));
Console.WriteLine(string.Format("Binding = {0}", serviceProxy.Endpoint.Binding));
Console.WriteLine(string.Format("Contract = {0}", serviceProxy.Endpoint.Contract.Name));
Person[] people = serviceProxy.Join(user);
Console.WriteLine("***********************************************");
Console.WriteLine("Connected !");
Console.WriteLine("Online users:");
foreach (Person p in people)
Console.WriteLine(p.Name);
string msg;
while ((msg = Console.ReadLine()) != "exit")
serviceProxy.Say(msg);
serviceProxy.Leave();
if (serviceProxy.State != CommunicationState.Faulted)
serviceProxy.Close();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
Client callback:
public class SimpleChatCallback : IChatServiceCallback
{
public void Receive(Person sender, string message)
{
Console.WriteLine("{0}: {1}", sender.Name, message);
}
public void ReceiveWhisper(Person sender, string message)
{
Console.WriteLine("{0}: {1}", sender.Name, message);
}
public void UserEnter(Person person)
{
Console.WriteLine("{0} has entered", person.Name);
}
public void UserLeave(Person person)
{
Console.WriteLine("{0} has left", person.Name);
}
}
Host config:
<system.serviceModel>
<services>
<service behaviorConfiguration="mexBehavior" name="ChatLib.ChatService">
<clear />
<endpoint address="ChatService.svc" binding="netTcpBinding" bindingConfiguration=""
name="netTcpEndpoint" bindingName="NonSecureTcpBinding" contract="Common.IChatService" />
<endpoint binding="mexHttpBinding" bindingConfiguration="mexHttpBinding"
name="mexHttpEndpoint" contract="IMetadataExchange" />
<host>
<baseAddresses>
<add baseAddress="http://localhost:33334/chatservice" />
<add baseAddress="net.tcp://localhost:33333/chatservice" />
</baseAddresses>
<timeouts openTimeout="00:10:00" />
</host>
</service>
</services>
<bindings>
<netTcpBinding>
<binding name="NonSecureTcpBinding">
<security mode="None">
<transport clientCredentialType="None" protectionLevel="None" />
<message clientCredentialType="None" />
</security>
</binding>
</netTcpBinding>
<mexHttpBinding>
<binding name="mexHttpBinding" />
</mexHttpBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior name="mexBehavior">
<serviceDebug includeExceptionDetailInFaults="true" />
<serviceMetadata httpGetEnabled="true" httpsGetEnabled="true" />
</behavior>
</serviceBehaviors>
</behaviors>
<protocolMapping>
<remove scheme="net.tcp" />
<add scheme="net.tcp" binding="netTcpBinding" bindingConfiguration="NonSecureTcpBinding" />
<add scheme="https" binding="basicHttpsBinding" />
</protocolMapping>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel>
Client config:
<system.serviceModel>
<bindings>
<netTcpBinding>
<binding name="netTcpEndpoint" />
</netTcpBinding>
</bindings>
<client>
<endpoint address="net.tcp://localhost:33333/chatservice/ChatService.svc"
binding="netTcpBinding" bindingConfiguration="netTcpEndpoint"
contract="ServiceReference1.IChatService" name="netTcpEndpoint">
<identity>
<userPrincipalName value="ComputerName\UserName" />
</identity>
</endpoint>
</client>
</system.serviceModel>
Host output:
Client 1 output:
Client 2 output:
Notes:
ServiceReference1 is the default namespace assigned by Visual Studio to the generated proxy client ChatServiceClient.
ChatLib is the locally assigned namespace to your ChatService implementation.

passing culture value inside wcf service hosted by iis

Here is my code..
private IHelloWorld ChannelFactoryWebService()
{
ServiceEndpoint tcpEndPoint = new ServiceEndpoint(
ContractDescription.GetContract(typeof(IHelloWorld)),
new NetTcpBinding(),
new EndpointAddress("net.tcp://myserver/CultureTest/Service.svc"));
ChannelFactory<IHelloWorld> factory = new ChannelFactory<IHelloWorld>(tcpEndPoint);
factory.Endpoint.Behaviors.Add(new CultureBehaviour());
return factory.CreateChannel();
}
[TestMethod] <-------------- FAILING TEST ----
public void ChangingCultureWASViaEndPointTest()
{
IHelloWorld client = ChannelFactoryWebService();
CultureInfo cultureInfo = new CultureInfo("ar-SA");
Thread.CurrentThread.CurrentCulture = cultureInfo;
client.Hello();
string culture = client.HelloCulture();
Assert.AreEqual("ar-SA", culture); <--- fails here.. I get en-US for culture
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, IncludeExceptionDetailInFaults = true)]
public class Server : IHelloWorld
{
#region IHelloWorld Members
public void Hello()
{
Console.WriteLine(string.Format("Hello world from the server in culture {0}", Thread.CurrentThread.CurrentCulture.Name));
}
public string HelloCulture()
{
string cultureName = Thread.CurrentThread.CurrentCulture.Name;
return cultureName;
}
#endregion
}
[ServiceContract]
public interface IHelloWorld
{
[OperationContract]
void Hello();
[OperationContract]
string HelloCulture();
}
public class CultureMessageInspector : IClientMessageInspector, IDispatchMessageInspector
{
private const string HeaderKey = "culture";
#region IDispatchMessageInspector Members
public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
{
int headerIndex = request.Headers.FindHeader(HeaderKey, string.Empty);
if (headerIndex != -1)
{
Thread.CurrentThread.CurrentCulture = new CultureInfo(request.Headers.GetHeader<String>(headerIndex));
}
return null;
}
public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
{
}
#endregion
#region IClientMessageInspector Members
public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
{
}
public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
{
request.Headers.Add(MessageHeader.CreateHeader(HeaderKey, string.Empty, Thread.CurrentThread.CurrentCulture.Name));
return null;
}
#endregion
}
public class CultureBehaviour : IEndpointBehavior
{
#region IEndpointBehavior Members
public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
{
CultureMessageInspector inspector = new CultureMessageInspector();
clientRuntime.MessageInspectors.Add(inspector);
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
{
CultureMessageInspector inspector = new CultureMessageInspector();
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(inspector);
}
public void Validate(ServiceEndpoint endpoint)
{
}
#endregion
}
public class CultureBehaviorExtension : BehaviorExtensionElement
{
// BehaviorExtensionElement members
public override Type BehaviorType
{
get { return typeof(CultureBehaviour); }
}
protected override object CreateBehavior()
{
return new CultureBehaviour();
}
}
Web config for the hosting site for Service looks as follows:
<endpointBehaviors>
<behavior name="Default">
<CultureExtension/>
</behavior>
</endpointBehaviors>
<serviceBehaviors>
<behavior>
<!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment -->
<serviceMetadata httpGetEnabled="true"/>
<!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<extensions>
<behaviorExtensions>
<add name="CultureExtension" type="Extension.CultureBehaviorExtension, Extension, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</behaviorExtensions>
</extensions>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
My question is, inside the following code when I go through the service hosted by website,
I am unable to get the culture value of ar-SA inside the Hello method of the service.
I tried my best to clarify the question here. Please let me know what else needs to be clarified.
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, IncludeExceptionDetailInFaults = true)]
public class Server : IHelloWorld
{
#region IHelloWorld Members
public void Hello()
{
<--- problem here unable to get culture value of ar-SA here --->
Console.WriteLine(string.Format("Hello world from the server in culture {0}", Thread.CurrentThread.CurrentCulture.Name));
}
public string HelloCulture()
{
string cultureName = Thread.CurrentThread.CurrentCulture.Name;
Console.WriteLine("**************************hello**********************************");
Console.WriteLine(cultureName);
return cultureName;
}
#endregion
}
Its not really an answer why that particular code doesnt work, but why dont you just sent in the culture string and set it in your method on the WCF service?
Thats the way we usualy do it, or if its an authenticated user, just save it on the user so they can switch to any language regardless of computer config.

How do I restrict access to some methods in WCF?

I am a bit lost getting started with a simple WCF service. I have two methods and I want to expose one to the world and the second one I want to limit to certain users. Eventually I want to be able to use a client application to use the restricted method. So far I can access both methods anonymously:
C# Code
namespace serviceSpace
{
[ServiceContract]
interface ILocationService
{
[OperationContract]
string GetLocation(string id);
[OperationContract]
string GetHiddenLocation(string id);
}
[AspNetCompatibilityRequirements(
RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class LocationService : ILocationService
{
[WebGet(UriTemplate = "Location/{id}")]
public string GetLocation(string id)
{
return "O hai, I'm available to everyone.";
}
// only use this if authorized somehow
[WebGet(UriTemplate = "Location/hush/{id}")]
public string GetHiddenLocation(string id)
{
return "O hai, I can only be seen by certain users.";
}
}
}
Configuration
<?xml version="1.0"?>
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0" />
</system.web>
<system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
<standardEndpoints>
<webHttpEndpoint>
<standardEndpoint name="" helpEnabled="true"
automaticFormatSelectionEnabled="true"/>
</webHttpEndpoint>
</standardEndpoints>
</system.serviceModel>
</configuration>
How do I get started?
A lot of the answers I found were almost what I needed but not quite right. I wound up setting up ASP.net membership and implementing a custom attribute to pull an authorization header and process login as the request came in. All of the magic happens in BeforeCall and ParseAuthorizationHeader below:
public class UsernamePasswordAuthentication : Attribute, IOperationBehavior, IParameterInspector
{
public void ApplyDispatchBehavior(OperationDescription operationDescription,
DispatchOperation dispatchOperation)
{
dispatchOperation.ParameterInspectors.Add(this);
}
public void AfterCall(string operationName, object[] outputs,
object returnValue, object correlationState)
{
}
public object BeforeCall(string operationName, object[] inputs)
{
var usernamePasswordString = parseAuthorizationHeader(WebOperationContext.Current.IncomingRequest);
if (usernamePasswordString != null)
{
string[] usernamePasswordArray = usernamePasswordString.Split(new char[] { ':' });
string username = usernamePasswordArray[0];
string password = usernamePasswordArray[1];
if ((username != null) && (password != null) && (Membership.ValidateUser(username, password)))
{
Thread.CurrentPrincipal = new GenericPrincipal(new GenericIdentity(username), new string[0]);
return null;
}
}
// if we made it here the user is not authorized
WebOperationContext.Current.OutgoingResponse.StatusCode =
HttpStatusCode.Unauthorized;
throw new WebFaultException<string>("Unauthorized", HttpStatusCode.Unauthorized);
}
private string parseAuthorizationHeader(IncomingWebRequestContext request)
{
string rtnString = null;
string authHeader = request.Headers["Authorization"];
if (authHeader != null)
{
var authStr = authHeader.Trim();
if (authStr.IndexOf("Basic", 0) == 0)
{
string encodedCredentials = authStr.Substring(6);
byte[] decodedBytes = Convert.FromBase64String(encodedCredentials);
rtnString = new ASCIIEncoding().GetString(decodedBytes);
}
}
return rtnString;
}
public void AddBindingParameters(OperationDescription operationDescription, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
{
}
public void Validate(OperationDescription operationDescription)
{
}
}
From there I just need to add my new attribute to the service contract entries. Any request to that method will require a valid authorization header or a Not Authorized response will be sent back with doing any further processing.
[ServiceContract]
interface ILocationService
{
[OperationContract]
string GetLocation(string id);
[OperationContract]
[UsernamePasswordAuthentication] // this attribute will force authentication
string GetHiddenLocation(string id);
}
Use the following steps to restrict access to specific Windows users:
Open the Computer Management Windows applet.
Create a Windows group that contains the specific Windows users to which you wish to give access. For example, a group can be called “CalculatorClients”.
Configure your service to require ClientCredentialType = “Windows”. This will require clients to connect using Windows authentication.
Configure your service methods with the PrincipalPermission attribute to require connecting users be members of the CalculatorClients group.
// Only members of the CalculatorClients group can call this method.
[PrincipalPermission(SecurityAction.Demand, Role = "CalculatorClients")]
public double Add(double a, double b)
{
return a + b;
}

Categories

Resources