//******************************************************************************************************
// SelfHostingService.cs - Gbtc
//
// Copyright © 2012, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
// not use this file except in compliance with the License. You may obtain a copy of the License at:
//
// http://www.opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 08/27/2009 - Pinal C. Patel
// Generated original version of source code.
// 09/02/2009 - Pinal C. Patel
// Modified configuration of the default WebHttpBinding to enable receiving of large payloads.
// 09/15/2009 - Stephen C. Wills
// Added new header and license agreement.
// 11/27/2009 - Pinal C. Patel
// Fixed issue in the initialization of service contract name.
// 03/30/2010 - Pinal C. Patel
// Updated CanRead and CanWrite to not include Enabled in its evaluation.
// 05/28/2010 - Pinal C. Patel
// Added an endpoint for web service help.
// 06/21/2010 - Pinal C. Patel
// Added Singleton property for added control over the hosting process.
// 10/08/2010 - Pinal C. Patel
// Removed REST web service help endpoint since a similar feature is now part of WCF 4.0.
// 10/14/2010 - Pinal C. Patel
// Made changes for hosting flexibility and enabling security:
// Deleted DataFlow since access restriction can now be imposed by enabling security.
// Added SecurityPolicy and PublishMetadata.
// Renamed ServiceUri to Endpoints and ServiceContract to Contract.
// 10/26/2010 - Pinal C. Patel
// Modified the implementation of ISupportLifecycle.Enabled property.
// 10/29/2010 - Pinal C. Patel
// Modified CreateServiceBinding() to explicitly disable security on created binding if specified.
// 11/19/2010 - Pinal C. Patel
// Changed to inherit from Adapter to take advantage of app domain isolation through AdapterLoader.
// 12/09/2010 - Pinal C. Patel
// Modified GetUnusedPort() to use GSF.Security.Cryptography.Random instead of System.Random to
// unsure unique numbers.
// 11/29/2011 - Pinal C. Patel
// Modified InitializeServiceHost() to not use a random port number for the service host address
// so that setting up security on the address is possible.
// 08/02/2012 - J. Ritchie Carroll
// Added cross-domain access support for Silverlight and Flash application using self hosted
// web services (enables access to clientaccesspolicy.xml and crossdomain.xml).
// 12/20/2012 - Starlynn Danyelle Gilliam
// Modified Header.
// 12/11/2013 - Pinal C. Patel
// Added WindowsAuthentication property to allow Windows Authentication to be enabled explicitly
// even if SecurityPolicy is not configured to allow for more flexibility.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.IdentityModel.Policy;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Web;
using System.Text;
using System.Text.RegularExpressions;
using GSF.Adapters;
using GSF.Configuration;
using Random = GSF.Security.Cryptography.Random;
namespace GSF.ServiceModel
{
///
/// A base class for web service that can send and receive data over REST (Representational State Transfer) interface.
///
///
/// This example shows how to create a WCF service derived from that is capable of hosting itself:
///
/// using System.ServiceModel;
/// using System.ServiceModel.Web;
/// using GSF.ServiceModel;
///
/// namespace Services
/// {
/// [ServiceContract()]
/// public interface IService : ISelfHostingService
/// {
/// [OperationContract(), WebGet(UriTemplate = "/hello/{name}")]
/// string Hello(string name);
/// }
///
/// public class Service : SelfHostingService, IService
/// {
/// public string Hello(string name)
/// {
/// return string.Format("Hello {0}!", name);
/// }
/// }
/// }
///
/// This example shows how to activate a WCF service derived from that is capable of hosting itself:
///
/// using System;
/// using System.ServiceModel;
/// using System.ServiceModel.Description;
/// using System.ServiceModel.Web;
/// using Services;
/// using GSF.ServiceModel;
///
/// class Program
/// {
/// static void Main(string[] args)
/// {
/// // Initialize web service.
/// Service service = new Service();
/// service.PublishMetadata = true;
/// service.Endpoints = "http.soap11://localhost:4500/soap; http.rest://localhost:4500/rest";
/// service.Initialize();
///
/// // Show web service status.
/// if (service.ServiceHost.State == CommunicationState.Opened)
/// {
/// Console.WriteLine("\r\n{0} is running:", service.GetType().Name);
/// foreach (ServiceEndpoint endpoint in service.ServiceHost.Description.Endpoints)
/// {
/// Console.WriteLine("- {0} ({1})", endpoint.Address, endpoint.Binding.GetType().Name);
/// }
/// Console.Write("\r\nPress Enter key to stop...");
/// }
/// else
/// {
/// Console.WriteLine("\r\n{0} could not be started", service.GetType().Name);
/// }
///
/// // Shutdown.
/// Console.ReadLine();
/// service.Dispose();
/// }
/// }
///
/// This example shows how to host a WCF service derived from inside ASP.NET:
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
///
/// ]]>
///
///
public class SelfHostingService : Adapter, ISelfHostingService, IPolicyRetriever
{
#region [ Members ]
// Events
///
/// Occurs when the has been created with the specified .
///
///
/// When event is fired, changes like adding new endpoints can be made to the .
///
public event EventHandler ServiceHostCreated;
///
/// Occurs when the has can process requests via all of its endpoints.
///
public event EventHandler ServiceHostStarted;
///
/// Occurs when an is encountered when processing a request.
///
///
/// is the exception encountered when processing a request.
///
public event EventHandler> ServiceProcessException;
// Fields
private string m_endpoints;
private string m_contractInterface;
private bool m_singleton;
private string m_securityPolicy;
private bool m_publishMetadata;
private bool m_allowCrossDomainAccess;
private string m_allowedDomainList;
private bool m_windowsAuthentication;
private bool m_serviceEnabled;
private bool m_disposed;
private bool m_initialized;
private ServiceHost m_serviceHost;
#endregion
#region [ Constructors ]
///
/// Initializes a new instance of the web service.
///
protected SelfHostingService()
{
Type type = GetType();
m_endpoints = string.Empty;
m_contractInterface = type.Namespace + ".I" + type.Name + ", " + type.AssemblyQualifiedName.ToNonNullString().Split(',')[1].Trim();
m_allowCrossDomainAccess = false;
m_allowedDomainList = "*";
m_serviceEnabled = true;
}
///
/// Releases the unmanaged resources before the web service is reclaimed by .
///
~SelfHostingService()
{
Dispose(false);
}
#endregion
#region [ Properties ]
///
/// Gets or sets a boolean value that indicates whether the web service is currently enabled.
///
public override bool Enabled
{
get
{
return ((object)m_serviceHost != null && m_serviceHost.State == CommunicationState.Opened);
}
set
{
if (value && !Enabled)
{
// Enable
if (!m_initialized)
Initialize();
else
InitializeServiceHost();
}
else if (!value && Enabled)
{
// Disable
m_serviceHost.Close();
}
}
}
///
/// Gets or sets a semicolon delimited list of URIs where the web service can be accessed.
///
///
/// Set to a null or empty string to disable web service hosting. Refer to
/// for a list of supported URI formats.
///
public string Endpoints
{
get
{
return m_endpoints;
}
set
{
m_endpoints = value;
}
}
///
/// Gets or sets the of the contract interface implemented by the web service.
///
/// The value being assigned is a null or empty string.
public string ContractInterface
{
get
{
return m_contractInterface;
}
set
{
if (string.IsNullOrEmpty(value))
throw new ArgumentNullException("value");
m_contractInterface = value;
}
}
///
/// Gets or sets a boolean value that indicates whether the will use the current instance of the web service for processing
/// requests or base the web service instance creation on specified in its .
///
public bool Singleton
{
get
{
return m_singleton;
}
set
{
m_singleton = value;
}
}
///
/// Gets or sets the of to be used for securing all web service .
///
public string SecurityPolicy
{
get
{
return m_securityPolicy;
}
set
{
m_securityPolicy = value;
}
}
///
/// Gets or sets a boolean value that indicates whether web service metadata is to made available at all web service .
///
public bool PublishMetadata
{
get
{
return m_publishMetadata;
}
set
{
m_publishMetadata = value;
}
}
///
/// Gets or sets flag that indicates if web services will enable cross-domain access for Silverlight and Flash applications.
///
public bool AllowCrossDomainAccess
{
get
{
return m_allowCrossDomainAccess;
}
set
{
m_allowCrossDomainAccess = value;
}
}
///
/// Gets or sets comma separated list of allowed domains when is true. Use * for domain wildcards, e.g., *.consoto.com.
///
///
/// To allow all domains just set this property to "*".
///
public string AllowedDomainList
{
get
{
return m_allowedDomainList;
}
set
{
m_allowedDomainList = value;
}
}
///
/// Gets or sets a boolean value that indicates whether Windows Authentication is to be enabled.
///
public bool WindowsAuthentication
{
get
{
return m_windowsAuthentication;
}
set
{
m_windowsAuthentication = value;
}
}
///
/// Gets or sets a boolean value that indicates whether the web service is to be enabled at startup.
///
public bool ServiceEnabled
{
get
{
return m_serviceEnabled;
}
set
{
m_serviceEnabled = value;
}
}
///
/// Gets the hosting the web service.
///
///
/// By default, the only has endpoint at the .
/// Additional endpoints can be added to the when event is fired.
///
public ServiceHost ServiceHost
{
get
{
return m_serviceHost;
}
}
#endregion
#region [ Methods ]
///
/// Initializes the web service.
///
public override void Initialize()
{
base.Initialize();
if (!m_initialized)
{
// Initialize the service host.
InitializeServiceHost();
// Initialize only once.
m_initialized = true;
}
}
///
/// Saves web service settings to the config file if the property is set to true.
///
public override void SaveSettings()
{
base.SaveSettings();
if (PersistSettings)
{
// Save settings under the specified category.
ConfigurationFile config = ConfigurationFile.Current;
CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory];
settings["Endpoints", true].Update(m_endpoints);
settings["Contract", true].Update(m_contractInterface);
settings["Singleton", true].Update(m_singleton);
settings["SecurityPolicy", true].Update(m_securityPolicy);
settings["PublishMetadata", true].Update(m_publishMetadata);
settings["AllowCrossDomainAccess", true].Update(m_allowCrossDomainAccess);
settings["AllowedDomainList", true].Update(m_allowedDomainList);
config.Save();
}
}
///
/// Loads saved web service settings from the config file if the property is set to true.
///
public override void LoadSettings()
{
base.LoadSettings();
if (PersistSettings)
{
// Load settings from the specified category.
ConfigurationFile config = ConfigurationFile.Current;
CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory];
settings.Add("Endpoints", m_endpoints, "Semicolon delimited list of URIs where the web service can be accessed.");
settings.Add("Contract", m_contractInterface, "Assembly qualified name of the contract interface implemented by the web service.");
settings.Add("Singleton", m_singleton, "True if the web service is singleton; otherwise False.");
settings.Add("SecurityPolicy", m_securityPolicy, "Assembly qualified name of the authorization policy to be used for securing the web service.");
settings.Add("PublishMetadata", m_publishMetadata, "True if the web service metadata is to be published at all the endpoints; otherwise False.");
settings.Add("AllowCrossDomainAccess", m_allowCrossDomainAccess, "True to allow Silverlight and Flash cross-domain access to the web service.");
settings.Add("AllowedDomainList", m_allowedDomainList, "Comma separated list of domain names for Silverlight and Flash cross-domain access to use when allowCrossDomainAccess is true. Use * for domain wildcards, e.g., *.consoto.com.");
settings.Add("Enabled", "True", "Determines if web service should be enabled at startup.");
Endpoints = settings["Endpoints"].ValueAs(m_endpoints);
ContractInterface = settings["Contract"].ValueAs(m_contractInterface);
Singleton = settings["Singleton"].ValueAs(m_singleton);
SecurityPolicy = settings["SecurityPolicy"].ValueAs(m_securityPolicy);
PublishMetadata = settings["PublishMetadata"].ValueAs(m_publishMetadata);
AllowCrossDomainAccess = settings["AllowCrossDomainAccess"].ValueAs(m_allowCrossDomainAccess);
AllowedDomainList = settings["AllowedDomainList"].ValueAs(m_allowedDomainList);
// Technically removing all end points will "disable" a web service since it would bind to nothing, however,
// this allows you to keep configured end points and still disable the service from configuration
m_serviceEnabled = settings["Enabled"].ValueAsBoolean(true);
}
}
///
/// Get an unused port number.
///
/// An value.
protected virtual int GetUnusedPort()
{
int randomPort = Random.Int32Between(1024, 65535);
IPEndPoint[] tcpListeners = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpListeners();
while (true)
{
if ((object)tcpListeners.FirstOrDefault(ep => ep.Port == randomPort) != null)
randomPort++; // Port in use - pick another one.
else
return randomPort; // Port is not in use - use this.
}
}
///
/// Gets an address where the will host the service.
///
/// A value.
protected virtual string GetServiceAddress()
{
// Regex matches:
// [http | ftp][.]://localhost[:][/]
const string regex = @"(\w+\.*\w*\://)(?\w+\:*\d*)";
// Look through all of the specified endpoints.
foreach (string endpoint in m_endpoints.Split(';'))
{
// Use endpoint host information including port number for service address.
Match match = Regex.Match(endpoint, regex, RegexOptions.IgnoreCase);
if (match.Success && match.Groups["host"].Success)
return string.Format("http://{0}", match.Groups["host"]);
}
// Return an empty string if endpoints are not in a valid format.
return string.Empty;
}
///
/// Initializes the .
///
protected virtual void InitializeServiceHost()
{
if (m_serviceEnabled && !string.IsNullOrEmpty(m_endpoints))
{
// Initialize service host.
string serviceUri = GetServiceAddress();
if (m_singleton)
m_serviceHost = new ServiceHost(this, new Uri(serviceUri));
else
m_serviceHost = new ServiceHost(this.GetType(), new Uri(serviceUri));
// Enable metadata publishing.
if (m_publishMetadata)
{
ServiceMetadataBehavior serviceBehavior = m_serviceHost.Description.Behaviors.Find();
if ((object)serviceBehavior == null)
{
serviceBehavior = new ServiceMetadataBehavior();
m_serviceHost.Description.Behaviors.Add(serviceBehavior);
}
serviceBehavior.HttpGetEnabled = true;
}
// Enable security on the service.
if (!string.IsNullOrEmpty(m_securityPolicy))
{
ServiceAuthorizationBehavior serviceBehavior = m_serviceHost.Description.Behaviors.Find();
if ((object)serviceBehavior == null)
{
serviceBehavior = new ServiceAuthorizationBehavior();
m_serviceHost.Description.Behaviors.Add(serviceBehavior);
}
serviceBehavior.PrincipalPermissionMode = PrincipalPermissionMode.Custom;
List policies = new List();
Type securityPolicyType = Type.GetType(m_securityPolicy);
if ((object)securityPolicyType == null)
throw new NullReferenceException(string.Format("Failed get security policy type '{0}' for self-hosting service - check config file settings.", m_securityPolicy.ToNonNullNorWhiteSpace("[undefined]")));
policies.Add((IAuthorizationPolicy)Activator.CreateInstance(securityPolicyType));
serviceBehavior.ExternalAuthorizationPolicies = policies.AsReadOnly();
}
// Add specified service endpoints.
string serviceAddress;
string[] serviceAddresses;
Binding serviceBinding;
ServiceEndpoint serviceEndpoint;
serviceAddresses = m_endpoints.Split(';');
// Enable Windows Authentication if authorization policy is configured.
if (!string.IsNullOrEmpty(m_securityPolicy))
m_windowsAuthentication = true;
for (int i = 0; i < serviceAddresses.Length; i++)
{
serviceAddress = serviceAddresses[i].Trim();
serviceBinding = Service.CreateServiceBinding(ref serviceAddress, m_windowsAuthentication);
if ((object)serviceBinding == null)
continue;
Type contractInterfaceType = Type.GetType(m_contractInterface);
if ((object)contractInterfaceType == null)
throw new NullReferenceException(string.Format("Failed to get contract interface type '{0}' for self-hosting service - check config file settings.", m_contractInterface.ToNonNullNorWhiteSpace("[undefined]")));
serviceEndpoint = m_serviceHost.AddServiceEndpoint(contractInterfaceType, serviceBinding, serviceAddress);
if (serviceBinding is WebHttpBinding)
{
// Special handling for REST endpoint.
WebHttpBehavior restBehavior = new WebHttpBehavior();
#if !MONO
if (m_publishMetadata)
restBehavior.HelpEnabled = true;
#endif
serviceEndpoint.Behaviors.Add(restBehavior);
}
else if (m_publishMetadata)
{
// Add endpoint for service metadata.
if (serviceAddress.StartsWith("http://"))
m_serviceHost.AddServiceEndpoint(ServiceMetadataBehavior.MexContractName, MetadataExchangeBindings.CreateMexHttpBinding(), serviceAddress + "/mex");
else if (serviceAddress.StartsWith("net.tcp://"))
m_serviceHost.AddServiceEndpoint(ServiceMetadataBehavior.MexContractName, MetadataExchangeBindings.CreateMexTcpBinding(), serviceAddress + "/mex");
else if (serviceAddress.StartsWith("net.pipe://"))
m_serviceHost.AddServiceEndpoint(ServiceMetadataBehavior.MexContractName, MetadataExchangeBindings.CreateMexNamedPipeBinding(), serviceAddress + "/mex");
}
}
// Enable cross domain access.
if (m_allowCrossDomainAccess)
m_serviceHost.AddServiceEndpoint(typeof(IPolicyRetriever), new WebHttpBinding(), "").Behaviors.Add(new WebHttpBehavior());
// Allow for customization.
OnServiceHostCreated();
// Start the service.
m_serviceHost.Open();
OnServiceHostStarted();
}
}
// Converts a string of data to a UTF8 stream using "application/xml" as the content type
private Stream StringToStream(string result)
{
if ((object)WebOperationContext.Current != null)
WebOperationContext.Current.OutgoingResponse.ContentType = "application/xml";
// Using UTF8 result buffer as non-expandable memory stream for string-as-stream
return new MemoryStream(Encoding.UTF8.GetBytes(result));
}
///
/// Gets policy stream for Silverlight applications.
///
/// Stream containing clientaccesspolicy.xml.
public Stream GetSilverlightPolicy()
{
if (m_allowCrossDomainAccess)
{
const string result = "\r\n" +
"\r\n" +
" \r\n" +
" \r\n" +
" \r\n" +
" \r\n" +
" \r\n" +
" \r\n" +
" \r\n" +
" \r\n" +
" \r\n" +
" \r\n" +
"";
return StringToStream(string.Format(result, m_allowedDomainList));
}
return StringToStream("");
}
///
/// Gets policy stream for Flash applications.
///
/// Stream containing crossdomain.xml.
public Stream GetFlashPolicy()
{
if (m_allowCrossDomainAccess)
{
const string result = "\r\n" +
"\r\n" +
"\r\n{0}";
const string allowAccessTag = " \r\n";
StringBuilder allowedDomains = new StringBuilder();
string[] domains = m_allowedDomainList.ToNonNullString("*").Split(',');
foreach (string domain in domains)
{
allowedDomains.AppendFormat(allowAccessTag, domain);
}
return StringToStream(string.Format(result, allowedDomains));
}
return StringToStream("");
}
///
/// Releases the unmanaged resources used by the web service and optionally releases the managed resources.
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
protected override void Dispose(bool disposing)
{
if (!m_disposed)
{
try
{
// This will be done regardless of whether the object is finalized or disposed.
if (disposing)
{
// This will be done only when the object is disposed by calling Dispose().
if ((object)m_serviceHost != null && m_serviceHost.State != CommunicationState.Faulted)
m_serviceHost.Close();
}
}
finally
{
m_disposed = true; // Prevent duplicate dispose.
base.Dispose(disposing); // Call base class Dispose().
}
}
}
///
/// Raises the event.
///
protected virtual void OnServiceHostCreated()
{
if ((object)ServiceHostCreated != null)
ServiceHostCreated(this, EventArgs.Empty);
}
///
/// Raises the event.
///
protected virtual void OnServiceHostStarted()
{
if ((object)ServiceHostStarted != null)
ServiceHostStarted(this, EventArgs.Empty);
}
///
/// Raises the event.
///
/// to sent to event.
protected virtual void OnServiceProcessException(Exception exception)
{
if ((object)ServiceProcessException != null)
ServiceProcessException(this, new EventArgs(exception));
}
#endregion
}
}