// // ServiceMetadataExtension.cs // // Author: // Atsushi Enomoto // Ankit Jain // Gonzalo Paniagua Javier (gonzalo@novell.com) // // Copyright (C) 2005 Novell, Inc. http://www.novell.com // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.IO; using System.Linq; using System.ServiceModel.Channels; using System.ServiceModel.Dispatcher; using System.Web; using System.Web.Services; using System.Web.Services.Description; using System.Xml; using System.Xml.Schema; using WSServiceDescription = System.Web.Services.Description.ServiceDescription; using WSMessage = System.Web.Services.Description.Message; using SMMessage = System.ServiceModel.Channels.Message; using WCFBinding = System.ServiceModel.Channels.Binding; namespace System.ServiceModel.Description { public class ServiceMetadataExtension : IExtension { const string ServiceMetadataBehaviorHttpGetBinding = "ServiceMetadataBehaviorHttpGetBinding"; MetadataSet metadata; ServiceHostBase owner; Dictionary dispatchers; HttpGetWsdl instance; public ServiceMetadataExtension () { } internal HttpGetWsdl Instance { get { return instance; } } [MonoTODO] public MetadataSet Metadata { get { if (metadata == null) { var exporter = new WsdlExporter (); exporter.ExportEndpoints (owner.Description.Endpoints, new XmlQualifiedName (owner.Description.Name, owner.Description.Namespace)); metadata = exporter.GetGeneratedMetadata (); } return metadata; } } internal ServiceHostBase Owner { get { return owner; } } internal static ServiceMetadataExtension EnsureServiceMetadataExtension (ServiceHostBase serviceHostBase) { ServiceMetadataExtension sme = serviceHostBase.Extensions.Find (); if (sme == null) { sme = new ServiceMetadataExtension (); serviceHostBase.Extensions.Add (sme); } return sme; } // FIXME: if the ServiceDescription has a base address (e.g. http://localhost:8080) and HttpGetUrl is empty, it returns UnknownDestination while it is expected to return the HTTP help page. internal void EnsureChannelDispatcher (bool isMex, string scheme, Uri uri, WCFBinding binding) { if (isMex) instance.WsdlUrl = uri; else instance.HelpUrl = uri; if (dispatchers == null) dispatchers = new Dictionary (); else if (dispatchers.ContainsKey (uri)) return; // already exists (e.g. reached here for wsdl while help is already filled on the same URI.) if (binding == null) { switch (scheme) { case "http": binding = MetadataExchangeBindings.CreateMexHttpBinding (); break; case "https": binding = MetadataExchangeBindings.CreateMexHttpsBinding (); break; case "net.tcp": binding = MetadataExchangeBindings.CreateMexTcpBinding (); break; case "net.pipe": binding = MetadataExchangeBindings.CreateMexNamedPipeBinding (); break; } } CustomBinding cb = new CustomBinding (binding) { Name = ServiceMetadataBehaviorHttpGetBinding }; cb.Elements.Find ().MessageVersion = MessageVersion.None; ServiceEndpoint se = new ServiceEndpoint (ContractDescription.GetContract (typeof (IHttpGetHelpPageAndMetadataContract)), cb, new EndpointAddress (uri)) { ListenUri = uri, }; var channelDispatcher = new DispatcherBuilder (Owner).BuildChannelDispatcher (owner.Description.ServiceType, se, new BindingParameterCollection ()); channelDispatcher.MessageVersion = MessageVersion.None; // it has no MessageVersion. channelDispatcher.IsMex = true; channelDispatcher.Endpoints [0].DispatchRuntime.InstanceContextProvider = new SingletonInstanceContextProvider (new InstanceContext (owner, instance)); dispatchers.Add (uri, channelDispatcher); owner.ChannelDispatchers.Add (channelDispatcher); } void IExtension.Attach (ServiceHostBase owner) { this.owner = owner; instance = new HttpGetWsdl (this); } void IExtension.Detach (ServiceHostBase owner) { this.owner = null; } } [ServiceContract (Namespace = "http://schemas.microsoft.com/2006/04/http/metadata")] interface IHttpGetHelpPageAndMetadataContract { [OperationContract (Action = "*", ReplyAction = "*")] SMMessage Get (SMMessage req); } class HttpGetWsdl : IHttpGetHelpPageAndMetadataContract { ServiceMetadataExtension ext; bool initialized; Dictionary wsdl_documents = new Dictionary (); Dictionary schemas = new Dictionary (); public HttpGetWsdl (ServiceMetadataExtension ext) { this.ext = ext; } public Uri HelpUrl { get; set; } public Uri WsdlUrl { get; set; } void EnsureMetadata () { if (!initialized) { GetMetadata (); initialized = true; } } public SMMessage Get (SMMessage req) { EnsureMetadata (); HttpRequestMessageProperty prop = (HttpRequestMessageProperty) req.Properties [HttpRequestMessageProperty.Name]; NameValueCollection query_string = CreateQueryString (prop.QueryString); if (query_string == null || query_string.AllKeys.Length != 1) { if (HelpUrl != null && Uri.Compare (req.Headers.To, HelpUrl, UriComponents.HttpRequestUrl ^ UriComponents.Query, UriFormat.UriEscaped, StringComparison.Ordinal) == 0) return CreateHelpPage (req); WSServiceDescription w = GetWsdl ("wsdl"); if (w != null) return CreateWsdlMessage (w); } if (String.Compare (query_string [null], "wsdl", StringComparison.OrdinalIgnoreCase) == 0) { WSServiceDescription wsdl = GetWsdl ("wsdl"); if (wsdl != null) return CreateWsdlMessage (wsdl); } else if (query_string ["wsdl"] != null) { WSServiceDescription wsdl = GetWsdl (query_string ["wsdl"]); if (wsdl != null) return CreateWsdlMessage (wsdl); } else if (query_string ["xsd"] != null) { XmlSchema schema = GetXmlSchema (query_string ["xsd"]); if (schema != null) { //FIXME: Is this the correct way? MemoryStream ms = new MemoryStream (); schema.Write (ms); ms.Seek (0, SeekOrigin.Begin); SMMessage ret = SMMessage.CreateMessage (MessageVersion.None, "", XmlReader.Create (ms)); return ret; } } return CreateHelpPage (req); } /* Code from HttpListenerRequest */ NameValueCollection CreateQueryString (string query) { NameValueCollection query_string = new NameValueCollection (); if (query == null || query.Length == 0) return null; string [] components = query.Split ('&'); foreach (string kv in components) { int pos = kv.IndexOf ('='); if (pos == -1) { query_string.Add (null, HttpUtility.UrlDecode (kv)); } else { string key = HttpUtility.UrlDecode (kv.Substring (0, pos)); string val = HttpUtility.UrlDecode (kv.Substring (pos + 1)); query_string.Add (key, val); } } return query_string; } // It is returned for ServiceDebugBehavior.Http(s)HelpPageUrl. // They may be empty, and for such case the help page URL is // simply the service endpoint URL (foobar.svc). // // Note that if there is also ServiceMetadataBehavior that // lacks Http(s)GetUrl, then it is also mapped to the same // URL, but it requires "?wsdl" parameter and .NET somehow // differentiates those requests. // // If both Http(s)HelpPageUrl and Http(s)GetUrl exist, then // requests to the service endpoint URL (foobar.svc) results // in an xml output with empty string (non-WF XML error). SMMessage CreateHelpPage (SMMessage request) { var helpBody = ext.Owner.Description.Behaviors.Find () != null ? String.Format (@"

To create client proxy source, run:

svcutil {0}

", new Uri (WsdlUrl.ToString () + "?wsdl")) : // this Uri.ctor() is nasty, but there is no other way to add "?wsdl" (!!) String.Format (@"

Service metadata publishing for {0} is not enabled. Service administrators can enable it by adding <serviceMetadata> element in the host configuration (web.config in ASP.NET), or ServiceMetadataBehavior object to the Behaviors collection of the service host's ServiceDescription.

", ext.Owner.Description.Name); var html = String.Format (@" Service {0} {1} ", ext.Owner.Description.Name, helpBody); var m = SMMessage.CreateMessage (MessageVersion.None, "", XmlReader.Create (new StringReader (html))); var rp = new HttpResponseMessageProperty (); rp.Headers ["Content-Type"] = "text/html"; m.Properties.Add (HttpResponseMessageProperty.Name, rp); return m; } SMMessage CreateWsdlMessage (WSServiceDescription wsdl) { MemoryStream ms = new MemoryStream (); XmlWriter xw = XmlWriter.Create (ms); WSServiceDescription.Serializer.Serialize (xw, wsdl); ms.Seek (0, SeekOrigin.Begin); return SMMessage.CreateMessage (MessageVersion.None, "", XmlReader.Create (ms)); } void GetMetadata () { if (WsdlUrl == null) return; MetadataSet metadata = ext.Metadata; int xs_i = 0, wsdl_i = 0; //Dictionary keyed by namespace StringDictionary wsdl_strings = new StringDictionary (); StringDictionary xsd_strings = new StringDictionary (); foreach (MetadataSection section in metadata.MetadataSections) { string key; XmlSchema xs = section.Metadata as XmlSchema; if (xs != null) { key = String.Format ("xsd{0}", xs_i ++); schemas [key] = xs; xsd_strings [xs.TargetNamespace] = key; continue; } WSServiceDescription wsdl = section.Metadata as WSServiceDescription; if (wsdl == null) continue; //if (wsdl.TargetNamespace == "http://tempuri.org/") if (wsdl.Services.Count > 0) key = "wsdl"; else key = String.Format ("wsdl{0}", wsdl_i ++); wsdl_documents [key] = wsdl; wsdl_strings [wsdl.TargetNamespace] = key; } string base_url = WsdlUrl.ToString (); foreach (WSServiceDescription wsdl in wsdl_documents.Values) { foreach (Import import in wsdl.Imports) { if (!String.IsNullOrEmpty (import.Location)) continue; import.Location = String.Format ("{0}?wsdl={1}", base_url, wsdl_strings [import.Namespace]); } foreach (XmlSchema schema in wsdl.Types.Schemas) { foreach (XmlSchemaObject obj in schema.Includes) { XmlSchemaImport imp = obj as XmlSchemaImport; if (imp == null || imp.SchemaLocation != null) continue; imp.SchemaLocation = String.Format ("{0}?xsd={1}", base_url, xsd_strings [imp.Namespace]); } } } } WSServiceDescription GetWsdl (string which) { EnsureMetadata (); WSServiceDescription wsdl; wsdl_documents.TryGetValue (which, out wsdl); return wsdl; } XmlSchema GetXmlSchema (string which) { EnsureMetadata (); XmlSchema schema; schemas.TryGetValue (which, out schema); return schema; } } }