ServiceMetadataExtension.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  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 (Owner).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. else
  130. throw new InvalidOperationException ("FIXME: attempt to use ServiceMetadataExtension to not-supported channel listener: " + listener.GetType ());
  131. channelDispatcher.Endpoints [0].DispatchRuntime.InstanceContextProvider = new SingletonInstanceContextProvider (new InstanceContext (owner, instance));
  132. dispatchers.Add (uri, channelDispatcher);
  133. owner.ChannelDispatchers.Add (channelDispatcher);
  134. }
  135. void IExtension<ServiceHostBase>.Attach (ServiceHostBase owner)
  136. {
  137. this.owner = owner;
  138. instance = new HttpGetWsdl (this);
  139. }
  140. void IExtension<ServiceHostBase>.Detach (ServiceHostBase owner)
  141. {
  142. this.owner = null;
  143. }
  144. }
  145. [ServiceContract (Namespace = "http://schemas.microsoft.com/2006/04/http/metadata")]
  146. interface IHttpGetHelpPageAndMetadataContract
  147. {
  148. [OperationContract (Action = "*", ReplyAction = "*")]
  149. SMMessage Get (SMMessage req);
  150. }
  151. // It is used to identify which page to serve when a channel dispatcher
  152. // has a listener to an relatively empty URI (conflicting with the
  153. // target service endpoint)
  154. //
  155. // Can't be enum as it is for GetProperty<T> ().
  156. internal class MetadataPublishingInfo
  157. {
  158. public bool SupportsMex { get; set; }
  159. public bool SupportsHelp { get; set; }
  160. public HttpGetWsdl Instance { get; set; }
  161. }
  162. class HttpGetWsdl : IHttpGetHelpPageAndMetadataContract
  163. {
  164. ServiceMetadataExtension ext;
  165. bool initialized;
  166. Dictionary <string,WSServiceDescription> wsdl_documents =
  167. new Dictionary<string, WSServiceDescription> ();
  168. Dictionary <string, XmlSchema> schemas =
  169. new Dictionary<string, XmlSchema> ();
  170. public HttpGetWsdl (ServiceMetadataExtension ext)
  171. {
  172. this.ext = ext;
  173. }
  174. public Uri HelpUrl { get; set; }
  175. public Uri WsdlUrl { get; set; }
  176. void EnsureMetadata ()
  177. {
  178. if (!initialized) {
  179. GetMetadata ();
  180. initialized = true;
  181. }
  182. }
  183. public SMMessage Get (SMMessage req)
  184. {
  185. EnsureMetadata ();
  186. HttpRequestMessageProperty prop = (HttpRequestMessageProperty) req.Properties [HttpRequestMessageProperty.Name];
  187. NameValueCollection query_string = CreateQueryString (prop.QueryString);
  188. if (query_string == null || query_string.AllKeys.Length != 1) {
  189. if (HelpUrl != null && Uri.Compare (req.Headers.To, HelpUrl, UriComponents.HttpRequestUrl ^ UriComponents.Query, UriFormat.UriEscaped, StringComparison.Ordinal) == 0)
  190. return CreateHelpPage (req);
  191. WSServiceDescription w = GetWsdl ("wsdl");
  192. if (w != null)
  193. return CreateWsdlMessage (w);
  194. }
  195. if (String.Compare (query_string [null], "wsdl", StringComparison.OrdinalIgnoreCase) == 0) {
  196. WSServiceDescription wsdl = GetWsdl ("wsdl");
  197. if (wsdl != null)
  198. return CreateWsdlMessage (wsdl);
  199. } else if (query_string ["wsdl"] != null) {
  200. WSServiceDescription wsdl = GetWsdl (query_string ["wsdl"]);
  201. if (wsdl != null)
  202. return CreateWsdlMessage (wsdl);
  203. } else if (query_string ["xsd"] != null) {
  204. XmlSchema schema = GetXmlSchema (query_string ["xsd"]);
  205. if (schema != null) {
  206. //FIXME: Is this the correct way?
  207. MemoryStream ms = new MemoryStream ();
  208. schema.Write (ms);
  209. ms.Seek (0, SeekOrigin.Begin);
  210. SMMessage ret = SMMessage.CreateMessage (MessageVersion.None, "", XmlReader.Create (ms));
  211. return ret;
  212. }
  213. }
  214. return CreateHelpPage (req);
  215. }
  216. /* Code from HttpListenerRequest */
  217. NameValueCollection CreateQueryString (string query)
  218. {
  219. NameValueCollection query_string = new NameValueCollection ();
  220. if (query == null || query.Length == 0)
  221. return null;
  222. string [] components = query.Split ('&');
  223. foreach (string kv in components) {
  224. int pos = kv.IndexOf ('=');
  225. if (pos == -1) {
  226. query_string.Add (null, HttpUtility.UrlDecode (kv));
  227. } else {
  228. string key = HttpUtility.UrlDecode (kv.Substring (0, pos));
  229. string val = HttpUtility.UrlDecode (kv.Substring (pos + 1));
  230. query_string.Add (key, val);
  231. }
  232. }
  233. return query_string;
  234. }
  235. // It is returned for ServiceDebugBehavior.Http(s)HelpPageUrl.
  236. // They may be empty, and for such case the help page URL is
  237. // simply the service endpoint URL (foobar.svc).
  238. //
  239. // Note that if there is also ServiceMetadataBehavior that
  240. // lacks Http(s)GetUrl, then it is also mapped to the same
  241. // URL, but it requires "?wsdl" parameter and .NET somehow
  242. // differentiates those requests.
  243. //
  244. // If both Http(s)HelpPageUrl and Http(s)GetUrl exist, then
  245. // requests to the service endpoint URL (foobar.svc) results
  246. // in an xml output with empty string (non-WF XML error).
  247. SMMessage CreateHelpPage (SMMessage request)
  248. {
  249. var helpBody = ext.Owner.Description.Behaviors.Find<ServiceMetadataBehavior> () != null ?
  250. String.Format (@"
  251. <p>To create client proxy source, run:</p>
  252. <p><code>svcutil <a href='{0}'>{0}</a></code></p>
  253. <!-- FIXME: add client proxy usage (that required decent ServiceContractGenerator implementation, so I leave it yet.) -->
  254. ", new Uri (WsdlUrl.ToString () + "?wsdl")) : // this Uri.ctor() is nasty, but there is no other way to add "?wsdl" (!!)
  255. String.Format (@"
  256. <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);
  257. var html = String.Format (@"
  258. <html>
  259. <head>
  260. <title>Service {0}</title>
  261. </head>
  262. <body>
  263. {1}
  264. </body>
  265. </html>", ext.Owner.Description.Name, helpBody);
  266. var m = SMMessage.CreateMessage (MessageVersion.None, "", XmlReader.Create (new StringReader (html)));
  267. var rp = new HttpResponseMessageProperty ();
  268. rp.Headers ["Content-Type"] = "text/html";
  269. m.Properties.Add (HttpResponseMessageProperty.Name, rp);
  270. return m;
  271. }
  272. SMMessage CreateWsdlMessage (WSServiceDescription wsdl)
  273. {
  274. MemoryStream ms = new MemoryStream ();
  275. XmlWriter xw = XmlWriter.Create (ms);
  276. WSServiceDescription.Serializer.Serialize (xw, wsdl);
  277. ms.Seek (0, SeekOrigin.Begin);
  278. return SMMessage.CreateMessage (MessageVersion.None, "", XmlReader.Create (ms));
  279. }
  280. void GetMetadata ()
  281. {
  282. if (WsdlUrl == null)
  283. return;
  284. MetadataSet metadata = ext.Metadata;
  285. int xs_i = 0, wsdl_i = 0;
  286. //Dictionary keyed by namespace
  287. StringDictionary wsdl_strings = new StringDictionary ();
  288. StringDictionary xsd_strings = new StringDictionary ();
  289. foreach (MetadataSection section in metadata.MetadataSections) {
  290. string key;
  291. XmlSchema xs = section.Metadata as XmlSchema;
  292. if (xs != null) {
  293. key = String.Format ("xsd{0}", xs_i ++);
  294. schemas [key] = xs;
  295. xsd_strings [xs.TargetNamespace] = key;
  296. continue;
  297. }
  298. WSServiceDescription wsdl = section.Metadata as WSServiceDescription;
  299. if (wsdl == null)
  300. continue;
  301. //if (wsdl.TargetNamespace == "http://tempuri.org/")
  302. if (wsdl.Services.Count > 0)
  303. key = "wsdl";
  304. else
  305. key = String.Format ("wsdl{0}", wsdl_i ++);
  306. wsdl_documents [key] = wsdl;
  307. wsdl_strings [wsdl.TargetNamespace] = key;
  308. }
  309. string base_url = WsdlUrl.ToString ();
  310. foreach (WSServiceDescription wsdl in wsdl_documents.Values) {
  311. foreach (Import import in wsdl.Imports) {
  312. if (!String.IsNullOrEmpty (import.Location))
  313. continue;
  314. import.Location = String.Format ("{0}?wsdl={1}", base_url, wsdl_strings [import.Namespace]);
  315. }
  316. foreach (XmlSchema schema in wsdl.Types.Schemas) {
  317. foreach (XmlSchemaObject obj in schema.Includes) {
  318. XmlSchemaImport imp = obj as XmlSchemaImport;
  319. if (imp == null || imp.SchemaLocation != null)
  320. continue;
  321. imp.SchemaLocation = String.Format ("{0}?xsd={1}", base_url, xsd_strings [imp.Namespace]);
  322. }
  323. }
  324. }
  325. }
  326. WSServiceDescription GetWsdl (string which)
  327. {
  328. EnsureMetadata ();
  329. WSServiceDescription wsdl;
  330. wsdl_documents.TryGetValue (which, out wsdl);
  331. return wsdl;
  332. }
  333. XmlSchema GetXmlSchema (string which)
  334. {
  335. EnsureMetadata ();
  336. XmlSchema schema;
  337. schemas.TryGetValue (which, out schema);
  338. return schema;
  339. }
  340. }
  341. }