ServiceMetadataExtension.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. //
  2. // ServiceMetadataExtension.cs
  3. //
  4. // Author:
  5. // Atsushi Enomoto <[email protected]>
  6. // Ankit Jain <[email protected]>
  7. // Gonzalo Paniagua Javier ([email protected])
  8. //
  9. // Copyright (C) 2005 Novell, Inc. http://www.novell.com
  10. //
  11. // Permission is hereby granted, free of charge, to any person obtaining
  12. // a copy of this software and associated documentation files (the
  13. // "Software"), to deal in the Software without restriction, including
  14. // without limitation the rights to use, copy, modify, merge, publish,
  15. // distribute, sublicense, and/or sell copies of the Software, and to
  16. // permit persons to whom the Software is furnished to do so, subject to
  17. // the following conditions:
  18. //
  19. // The above copyright notice and this permission notice shall be
  20. // included in all copies or substantial portions of the Software.
  21. //
  22. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  23. // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
  24. // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  25. // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
  26. // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
  27. // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
  28. // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  29. //
  30. using System;
  31. using System.Collections;
  32. using System.Collections.Generic;
  33. using System.Collections.ObjectModel;
  34. using System.Collections.Specialized;
  35. using System.IO;
  36. using System.Linq;
  37. using System.ServiceModel.Channels;
  38. using System.ServiceModel.Dispatcher;
  39. using System.Web;
  40. using System.Web.Services;
  41. using System.Web.Services.Description;
  42. using System.Xml;
  43. using System.Xml.Schema;
  44. using WSServiceDescription = System.Web.Services.Description.ServiceDescription;
  45. using WSMessage = System.Web.Services.Description.Message;
  46. using SMMessage = System.ServiceModel.Channels.Message;
  47. using WCFBinding = System.ServiceModel.Channels.Binding;
  48. namespace System.ServiceModel.Description
  49. {
  50. public class ServiceMetadataExtension : IExtension<ServiceHostBase>
  51. {
  52. const string ServiceMetadataBehaviorHttpGetBinding = "ServiceMetadataBehaviorHttpGetBinding";
  53. MetadataSet metadata;
  54. ServiceHostBase owner;
  55. Dictionary<Uri, ChannelDispatcher> dispatchers;
  56. HttpGetWsdl instance;
  57. public ServiceMetadataExtension ()
  58. {
  59. }
  60. [MonoTODO]
  61. public MetadataSet Metadata {
  62. get {
  63. if (metadata == null) {
  64. var exporter = new WsdlExporter ();
  65. exporter.ExportEndpoints (owner.Description.Endpoints, new XmlQualifiedName (owner.Description.Name, owner.Description.Namespace));
  66. metadata = exporter.GetGeneratedMetadata ();
  67. }
  68. return metadata;
  69. }
  70. }
  71. internal ServiceHostBase Owner {
  72. get { return owner; }
  73. }
  74. internal static ServiceMetadataExtension EnsureServiceMetadataExtension (ServiceHostBase serviceHostBase) {
  75. ServiceMetadataExtension sme = serviceHostBase.Extensions.Find<ServiceMetadataExtension> ();
  76. if (sme == null) {
  77. sme = new ServiceMetadataExtension ();
  78. serviceHostBase.Extensions.Add (sme);
  79. }
  80. return sme;
  81. }
  82. // FIXME: distinguish HTTP and HTTPS in the Url properties.
  83. // FIXME: reject such ServiceDescription that has no HTTP(S) binding.
  84. // FIXME: it should not use the binding that is used in the ServiceEndpoint. For example, WSDL results have to be given as text, not binary.
  85. // 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.
  86. internal void EnsureChannelDispatcher (bool isMex, string scheme, Uri uri, WCFBinding binding)
  87. {
  88. if (isMex)
  89. instance.WsdlUrl = uri;
  90. else
  91. instance.HelpUrl = uri;
  92. if (dispatchers == null)
  93. dispatchers = new Dictionary<Uri, ChannelDispatcher> ();
  94. else if (dispatchers.ContainsKey (uri)) {
  95. var info = dispatchers [uri].Listener.GetProperty<MetadataPublishingInfo> ();
  96. if (isMex)
  97. info.SupportsMex = true;
  98. else
  99. info.SupportsHelp = true;
  100. return;
  101. }
  102. if (binding == null) {
  103. switch (scheme) {
  104. case "http":
  105. binding = MetadataExchangeBindings.CreateMexHttpBinding ();
  106. break;
  107. case "https":
  108. binding = MetadataExchangeBindings.CreateMexHttpsBinding ();
  109. break;
  110. case "net.tcp":
  111. binding = MetadataExchangeBindings.CreateMexTcpBinding ();
  112. break;
  113. case "net.pipe":
  114. binding = MetadataExchangeBindings.CreateMexNamedPipeBinding ();
  115. break;
  116. }
  117. }
  118. CustomBinding cb = new CustomBinding (binding) { Name = ServiceMetadataBehaviorHttpGetBinding };
  119. cb.Elements.Find<MessageEncodingBindingElement> ().MessageVersion = MessageVersion.None;
  120. ServiceEndpoint se = new ServiceEndpoint (ContractDescription.GetContract (typeof (IHttpGetHelpPageAndMetadataContract)), cb, new EndpointAddress (uri))
  121. {
  122. ListenUri = uri,
  123. };
  124. var channelDispatcher = new DispatcherBuilder ().BuildChannelDispatcher (owner.Description.ServiceType, se, new BindingParameterCollection ());
  125. // add HttpGetWsdl to indicate that the ChannelDispatcher is for mex or help.
  126. var listener = channelDispatcher.Listener as ChannelListenerBase;
  127. if (listener != null)
  128. listener.Properties.Add (new MetadataPublishingInfo () { SupportsMex = isMex, SupportsHelp = !isMex, Instance = instance });
  129. channelDispatcher.Endpoints [0].DispatchRuntime.InstanceContextProvider = new SingletonInstanceContextProvider (new InstanceContext (owner, instance));
  130. dispatchers.Add (uri, channelDispatcher);
  131. owner.ChannelDispatchers.Add (channelDispatcher);
  132. }
  133. void IExtension<ServiceHostBase>.Attach (ServiceHostBase owner)
  134. {
  135. this.owner = owner;
  136. instance = new HttpGetWsdl (this);
  137. }
  138. void IExtension<ServiceHostBase>.Detach (ServiceHostBase owner)
  139. {
  140. this.owner = null;
  141. }
  142. }
  143. [ServiceContract (Namespace = "http://schemas.microsoft.com/2006/04/http/metadata")]
  144. interface IHttpGetHelpPageAndMetadataContract
  145. {
  146. [OperationContract (Action = "*", ReplyAction = "*")]
  147. SMMessage Get (SMMessage req);
  148. }
  149. // It is used to identify which page to serve when a channel dispatcher
  150. // has a listener to an relatively empty URI (conflicting with the
  151. // target service endpoint)
  152. //
  153. // Can't be enum as it is for GetProperty<T> ().
  154. internal class MetadataPublishingInfo
  155. {
  156. public bool SupportsMex { get; set; }
  157. public bool SupportsHelp { get; set; }
  158. public HttpGetWsdl Instance { get; set; }
  159. }
  160. class HttpGetWsdl : IHttpGetHelpPageAndMetadataContract
  161. {
  162. ServiceMetadataExtension ext;
  163. bool initialized;
  164. Dictionary <string,WSServiceDescription> wsdl_documents =
  165. new Dictionary<string, WSServiceDescription> ();
  166. Dictionary <string, XmlSchema> schemas =
  167. new Dictionary<string, XmlSchema> ();
  168. public HttpGetWsdl (ServiceMetadataExtension ext)
  169. {
  170. this.ext = ext;
  171. }
  172. public Uri HelpUrl { get; set; }
  173. public Uri WsdlUrl { get; set; }
  174. void EnsureMetadata ()
  175. {
  176. if (!initialized) {
  177. GetMetadata ();
  178. initialized = true;
  179. }
  180. }
  181. public SMMessage Get (SMMessage req)
  182. {
  183. EnsureMetadata ();
  184. HttpRequestMessageProperty prop = (HttpRequestMessageProperty) req.Properties [HttpRequestMessageProperty.Name];
  185. NameValueCollection query_string = CreateQueryString (prop.QueryString);
  186. if (query_string == null || query_string.AllKeys.Length != 1) {
  187. if (HelpUrl != null && Uri.Compare (req.Headers.To, HelpUrl, UriComponents.HttpRequestUrl ^ UriComponents.Query, UriFormat.UriEscaped, StringComparison.Ordinal) == 0)
  188. return CreateHelpPage (req);
  189. WSServiceDescription w = GetWsdl ("wsdl");
  190. if (w != null)
  191. return CreateWsdlMessage (w);
  192. }
  193. if (String.Compare (query_string [null], "wsdl", StringComparison.OrdinalIgnoreCase) == 0) {
  194. WSServiceDescription wsdl = GetWsdl ("wsdl");
  195. if (wsdl != null)
  196. return CreateWsdlMessage (wsdl);
  197. } else if (query_string ["wsdl"] != null) {
  198. WSServiceDescription wsdl = GetWsdl (query_string ["wsdl"]);
  199. if (wsdl != null)
  200. return CreateWsdlMessage (wsdl);
  201. } else if (query_string ["xsd"] != null) {
  202. XmlSchema schema = GetXmlSchema (query_string ["xsd"]);
  203. if (schema != null) {
  204. //FIXME: Is this the correct way?
  205. MemoryStream ms = new MemoryStream ();
  206. schema.Write (ms);
  207. ms.Seek (0, SeekOrigin.Begin);
  208. SMMessage ret = SMMessage.CreateMessage (MessageVersion.None, "", XmlReader.Create (ms));
  209. return ret;
  210. }
  211. }
  212. return CreateHelpPage (req);
  213. }
  214. /* Code from HttpListenerRequest */
  215. NameValueCollection CreateQueryString (string query)
  216. {
  217. NameValueCollection query_string = new NameValueCollection ();
  218. if (query == null || query.Length == 0)
  219. return null;
  220. string [] components = query.Split ('&');
  221. foreach (string kv in components) {
  222. int pos = kv.IndexOf ('=');
  223. if (pos == -1) {
  224. query_string.Add (null, HttpUtility.UrlDecode (kv));
  225. } else {
  226. string key = HttpUtility.UrlDecode (kv.Substring (0, pos));
  227. string val = HttpUtility.UrlDecode (kv.Substring (pos + 1));
  228. query_string.Add (key, val);
  229. }
  230. }
  231. return query_string;
  232. }
  233. // It is returned for ServiceDebugBehavior.Http(s)HelpPageUrl.
  234. // They may be empty, and for such case the help page URL is
  235. // simply the service endpoint URL (foobar.svc).
  236. //
  237. // Note that if there is also ServiceMetadataBehavior that
  238. // lacks Http(s)GetUrl, then it is also mapped to the same
  239. // URL, but it requires "?wsdl" parameter and .NET somehow
  240. // differentiates those requests.
  241. //
  242. // If both Http(s)HelpPageUrl and Http(s)GetUrl exist, then
  243. // requests to the service endpoint URL (foobar.svc) results
  244. // in an xml output with empty string (non-WF XML error).
  245. SMMessage CreateHelpPage (SMMessage request)
  246. {
  247. var helpBody = ext.Owner.Description.Behaviors.Find<ServiceMetadataBehavior> () != null ?
  248. String.Format (@"
  249. <p>To create client proxy source, run:</p>
  250. <p><code>svcutil <a href='{0}'>{0}</a></code></p>
  251. <!-- FIXME: add client proxy usage (that required decent ServiceContractGenerator implementation, so I leave it yet.) -->
  252. ", new Uri (WsdlUrl.ToString () + "?wsdl")) : // this Uri.ctor() is nasty, but there is no other way to add "?wsdl" (!!)
  253. String.Format (@"
  254. <p>Service metadata publishing for {0} is not enabled. Service administrators can enable it by adding &lt;serviceMetadata&gt; element in the host configuration (web.config in ASP.NET), or ServiceMetadataBehavior object to the Behaviors collection of the service host's ServiceDescription.</p>", ext.Owner.Description.Name);
  255. var html = String.Format (@"
  256. <html>
  257. <head>
  258. <title>Service {0}</title>
  259. </head>
  260. <body>
  261. {1}
  262. </body>
  263. </html>", ext.Owner.Description.Name, helpBody);
  264. var m = SMMessage.CreateMessage (MessageVersion.None, "", XmlReader.Create (new StringReader (html)));
  265. var rp = new HttpResponseMessageProperty ();
  266. rp.Headers ["Content-Type"] = "text/html";
  267. m.Properties.Add (HttpResponseMessageProperty.Name, rp);
  268. return m;
  269. }
  270. SMMessage CreateWsdlMessage (WSServiceDescription wsdl)
  271. {
  272. MemoryStream ms = new MemoryStream ();
  273. XmlWriter xw = XmlWriter.Create (ms);
  274. WSServiceDescription.Serializer.Serialize (xw, wsdl);
  275. ms.Seek (0, SeekOrigin.Begin);
  276. return SMMessage.CreateMessage (MessageVersion.None, "", XmlReader.Create (ms));
  277. }
  278. void GetMetadata ()
  279. {
  280. if (WsdlUrl == null)
  281. return;
  282. MetadataSet metadata = ext.Metadata;
  283. int xs_i = 0, wsdl_i = 0;
  284. //Dictionary keyed by namespace
  285. StringDictionary wsdl_strings = new StringDictionary ();
  286. StringDictionary xsd_strings = new StringDictionary ();
  287. foreach (MetadataSection section in metadata.MetadataSections) {
  288. string key;
  289. XmlSchema xs = section.Metadata as XmlSchema;
  290. if (xs != null) {
  291. key = String.Format ("xsd{0}", xs_i ++);
  292. schemas [key] = xs;
  293. xsd_strings [xs.TargetNamespace] = key;
  294. continue;
  295. }
  296. WSServiceDescription wsdl = section.Metadata as WSServiceDescription;
  297. if (wsdl == null)
  298. continue;
  299. //if (wsdl.TargetNamespace == "http://tempuri.org/")
  300. if (wsdl.Services.Count > 0)
  301. key = "wsdl";
  302. else
  303. key = String.Format ("wsdl{0}", wsdl_i ++);
  304. wsdl_documents [key] = wsdl;
  305. wsdl_strings [wsdl.TargetNamespace] = key;
  306. }
  307. string base_url = WsdlUrl.ToString ();
  308. foreach (WSServiceDescription wsdl in wsdl_documents.Values) {
  309. foreach (Import import in wsdl.Imports) {
  310. if (!String.IsNullOrEmpty (import.Location))
  311. continue;
  312. import.Location = String.Format ("{0}?wsdl={1}", base_url, wsdl_strings [import.Namespace]);
  313. }
  314. foreach (XmlSchema schema in wsdl.Types.Schemas) {
  315. foreach (XmlSchemaObject obj in schema.Includes) {
  316. XmlSchemaImport imp = obj as XmlSchemaImport;
  317. if (imp == null || imp.SchemaLocation != null)
  318. continue;
  319. imp.SchemaLocation = String.Format ("{0}?xsd={1}", base_url, xsd_strings [imp.Namespace]);
  320. }
  321. }
  322. }
  323. }
  324. WSServiceDescription GetWsdl (string which)
  325. {
  326. EnsureMetadata ();
  327. WSServiceDescription wsdl;
  328. wsdl_documents.TryGetValue (which, out wsdl);
  329. return wsdl;
  330. }
  331. XmlSchema GetXmlSchema (string which)
  332. {
  333. EnsureMetadata ();
  334. XmlSchema schema;
  335. schemas.TryGetValue (which, out schema);
  336. return schema;
  337. }
  338. }
  339. }