ServiceMetadataExtension.cs 13 KB

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