2010-06-18 Atsushi Enomoto <atsushi@ximian.com>
[mono.git] / mcs / class / System.ServiceModel / System.ServiceModel.Description / ServiceMetadataExtension.cs
1 //
2 // ServiceMetadataExtension.cs
3 //
4 // Author:
5 //      Atsushi Enomoto <atsushi@ximian.com>
6 //      Ankit Jain <jankit@novell.com>
7 //      Gonzalo Paniagua Javier (gonzalo@novell.com)
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
45 using WSServiceDescription = System.Web.Services.Description.ServiceDescription;
46 using WSMessage = System.Web.Services.Description.Message;
47 using SMMessage = System.ServiceModel.Channels.Message;
48 using WCFBinding = System.ServiceModel.Channels.Binding;
49
50 namespace System.ServiceModel.Description
51 {
52         public class ServiceMetadataExtension : IExtension<ServiceHostBase>
53         {
54                 const string ServiceMetadataBehaviorHttpGetBinding = "ServiceMetadataBehaviorHttpGetBinding";
55
56                 MetadataSet metadata;
57                 ServiceHostBase owner;
58                 Dictionary<Uri, ChannelDispatcher> dispatchers;
59                 HttpGetWsdl instance;
60
61                 public ServiceMetadataExtension ()
62                 {
63                 }
64
65                 [MonoTODO]
66                 public MetadataSet Metadata {
67                         get {
68                                 if (metadata == null) {
69                                         var exporter = new WsdlExporter ();
70                                         exporter.ExportEndpoints (owner.Description.Endpoints, new XmlQualifiedName (owner.Description.Name, owner.Description.Namespace));
71                                         metadata = exporter.GetGeneratedMetadata ();
72                                 }
73                                 return metadata;
74                         }
75                 }
76
77                 internal ServiceHostBase Owner {
78                         get { return owner; }
79                 }
80
81                 internal static ServiceMetadataExtension EnsureServiceMetadataExtension (ServiceHostBase serviceHostBase) {
82                         ServiceMetadataExtension sme = serviceHostBase.Extensions.Find<ServiceMetadataExtension> ();
83                         if (sme == null) {
84                                 sme = new ServiceMetadataExtension ();
85                                 serviceHostBase.Extensions.Add (sme);
86                         }
87                         return sme;
88                 }
89
90                 // FIXME: distinguish HTTP and HTTPS in the Url properties.
91                 // FIXME: reject such ServiceDescription that has no HTTP(S) binding.
92                 // 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.
93                 // 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.
94                 internal void EnsureChannelDispatcher (bool isMex, string scheme, Uri uri, WCFBinding binding)
95                 {
96                         if (isMex)
97                                 instance.WsdlUrl = uri;
98                         else
99                                 instance.HelpUrl = uri;
100
101                         if (dispatchers == null)
102                                 dispatchers = new Dictionary<Uri, ChannelDispatcher> ();
103                         else if (dispatchers.ContainsKey (uri)) {
104                                 var info = dispatchers [uri].Listener.GetProperty<MetadataPublishingInfo> ();
105                                 if (isMex)
106                                         info.SupportsMex = true;
107                                 else
108                                         info.SupportsHelp = true;
109                                 return;
110                         }
111
112                         if (binding == null) {
113                                 switch (scheme) {
114                                 case "http":
115                                         binding = MetadataExchangeBindings.CreateMexHttpBinding ();
116                                         break;
117                                 case "https":
118                                         binding = MetadataExchangeBindings.CreateMexHttpsBinding ();
119                                         break;
120                                 case "net.tcp":
121                                         binding = MetadataExchangeBindings.CreateMexTcpBinding ();
122                                         break;
123                                 case "net.pipe":
124                                         binding = MetadataExchangeBindings.CreateMexNamedPipeBinding ();
125                                         break;
126                                 }
127                         }
128
129                         CustomBinding cb = new CustomBinding (binding) { Name = ServiceMetadataBehaviorHttpGetBinding };
130                         cb.Elements.Find<MessageEncodingBindingElement> ().MessageVersion = MessageVersion.None;
131
132                         ServiceEndpoint se = new ServiceEndpoint (ContractDescription.GetContract (typeof (IHttpGetHelpPageAndMetadataContract)), cb, new EndpointAddress (uri))
133                         {
134                                 ListenUri = uri,
135                         };
136
137                         var channelDispatcher = new DispatcherBuilder (Owner).BuildChannelDispatcher (owner.Description.ServiceType, se, new BindingParameterCollection ());
138                         channelDispatcher.MessageVersion = MessageVersion.None; // it has no MessageVersion.
139                         // add HttpGetWsdl to indicate that the ChannelDispatcher is for mex or help.
140                         var listener = channelDispatcher.Listener as ChannelListenerBase;
141                         if (listener != null)
142                                 listener.Properties.Add (new MetadataPublishingInfo () { SupportsMex = isMex, SupportsHelp = !isMex, Instance = instance });
143                         else
144                                 throw new InvalidOperationException ("FIXME: attempt to use ServiceMetadataExtension to not-supported channel listener: " + listener.GetType ());
145                         channelDispatcher.IsMex = true;
146                         channelDispatcher.Endpoints [0].DispatchRuntime.InstanceContextProvider = new SingletonInstanceContextProvider (new InstanceContext (owner, instance));
147
148                         dispatchers.Add (uri, channelDispatcher);
149                         owner.ChannelDispatchers.Add (channelDispatcher);
150                 }
151
152                 void IExtension<ServiceHostBase>.Attach (ServiceHostBase owner)
153                 {
154                         this.owner = owner;
155                         instance = new HttpGetWsdl (this);
156                 }
157
158                 void IExtension<ServiceHostBase>.Detach (ServiceHostBase owner)
159                 {
160                         this.owner = null;
161                 }
162         }
163
164         [ServiceContract (Namespace = "http://schemas.microsoft.com/2006/04/http/metadata")]
165         interface IHttpGetHelpPageAndMetadataContract
166         {
167                 [OperationContract (Action = "*", ReplyAction = "*")]
168                 SMMessage Get (SMMessage req);
169         }
170
171         // It is used to identify which page to serve when a channel dispatcher 
172         // has a listener to an relatively empty URI (conflicting with the 
173         // target service endpoint)
174         //
175         // Can't be enum as it is for GetProperty<T> ().
176         internal class MetadataPublishingInfo
177         {
178                 public bool SupportsMex { get; set; }
179                 public bool SupportsHelp { get; set; }
180                 public HttpGetWsdl Instance { get; set; }
181         }
182
183         class HttpGetWsdl : IHttpGetHelpPageAndMetadataContract
184         {
185                 ServiceMetadataExtension ext;
186                 bool initialized;
187
188                 Dictionary <string,WSServiceDescription> wsdl_documents = 
189                         new Dictionary<string, WSServiceDescription> ();
190                 Dictionary <string, XmlSchema> schemas = 
191                         new Dictionary<string, XmlSchema> ();
192
193                 public HttpGetWsdl (ServiceMetadataExtension ext)
194                 {
195                         this.ext = ext;
196                 }
197
198                 public Uri HelpUrl { get; set; }
199                 public Uri WsdlUrl { get; set; }
200
201                 void EnsureMetadata ()
202                 {
203                         if (!initialized) {
204                                 GetMetadata ();
205                                 initialized = true;
206                         }
207                 }
208
209                 public SMMessage Get (SMMessage req)
210                 {
211                         EnsureMetadata ();
212
213                         HttpRequestMessageProperty prop = (HttpRequestMessageProperty) req.Properties [HttpRequestMessageProperty.Name];
214
215                         NameValueCollection query_string = CreateQueryString (prop.QueryString);
216                         if (query_string == null || query_string.AllKeys.Length != 1) {
217                                 if (HelpUrl != null && Uri.Compare (req.Headers.To, HelpUrl, UriComponents.HttpRequestUrl ^ UriComponents.Query, UriFormat.UriEscaped, StringComparison.Ordinal) == 0)
218                                         return CreateHelpPage (req);
219                                 WSServiceDescription w = GetWsdl ("wsdl");
220                                 if (w != null)
221                                         return CreateWsdlMessage (w);
222                         }
223
224                         if (String.Compare (query_string [null], "wsdl", StringComparison.OrdinalIgnoreCase) == 0) {
225                                 WSServiceDescription wsdl = GetWsdl ("wsdl");
226                                 if (wsdl != null)
227                                         return CreateWsdlMessage (wsdl);
228                         } else if (query_string ["wsdl"] != null) {
229                                 WSServiceDescription wsdl = GetWsdl (query_string ["wsdl"]);
230                                 if (wsdl != null)
231                                         return CreateWsdlMessage (wsdl);
232                         } else if (query_string ["xsd"] != null) {
233                                 XmlSchema schema = GetXmlSchema (query_string ["xsd"]);
234                                 if (schema != null) {
235                                         //FIXME: Is this the correct way?
236                                         MemoryStream ms = new MemoryStream ();
237
238                                         schema.Write (ms);
239                                         ms.Seek (0, SeekOrigin.Begin);
240                                         SMMessage ret = SMMessage.CreateMessage (MessageVersion.None, "", XmlReader.Create (ms));
241
242                                         return ret;
243                                 }
244                         }
245
246                         return CreateHelpPage (req);
247                 }
248
249                 /* Code from HttpListenerRequest */
250                 NameValueCollection CreateQueryString (string query)
251                 {
252                         NameValueCollection query_string = new NameValueCollection ();
253                         if (query == null || query.Length == 0)
254                                 return null;
255
256                         string [] components = query.Split ('&');
257                         foreach (string kv in components) {
258                                 int pos = kv.IndexOf ('=');
259                                 if (pos == -1) {
260                                         query_string.Add (null, HttpUtility.UrlDecode (kv));
261                                 } else {
262                                         string key = HttpUtility.UrlDecode (kv.Substring (0, pos));
263                                         string val = HttpUtility.UrlDecode (kv.Substring (pos + 1));
264
265                                         query_string.Add (key, val);
266                                 }
267                         }
268
269                         return query_string;
270                 }
271
272                 // It is returned for ServiceDebugBehavior.Http(s)HelpPageUrl.
273                 // They may be empty, and for such case the help page URL is
274                 // simply the service endpoint URL (foobar.svc).
275                 //
276                 // Note that if there is also ServiceMetadataBehavior that
277                 // lacks Http(s)GetUrl, then it is also mapped to the same
278                 // URL, but it requires "?wsdl" parameter and .NET somehow
279                 // differentiates those requests.
280                 //
281                 // If both Http(s)HelpPageUrl and Http(s)GetUrl exist, then
282                 // requests to the service endpoint URL (foobar.svc) results
283                 // in an xml output with empty string (non-WF XML error).
284
285                 SMMessage CreateHelpPage (SMMessage request)
286                 {
287                         var helpBody = ext.Owner.Description.Behaviors.Find<ServiceMetadataBehavior> () != null ?
288                                 String.Format (@"
289 <p>To create client proxy source, run:</p>
290 <p><code>svcutil <a href='{0}'>{0}</a></code></p>
291 <!-- FIXME: add client proxy usage (that required decent ServiceContractGenerator implementation, so I leave it yet.) -->
292 ", new Uri (WsdlUrl.ToString () + "?wsdl")) : // this Uri.ctor() is nasty, but there is no other way to add "?wsdl" (!!)
293                                 String.Format (@"
294 <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);
295
296                         var html = String.Format (@"
297 <html>
298 <head>
299 <title>Service {0}</title>
300 </head>
301 <body>
302 {1}
303 </body>
304 </html>", ext.Owner.Description.Name, helpBody);
305
306                         var m = SMMessage.CreateMessage (MessageVersion.None, "", XmlReader.Create (new StringReader (html)));
307                         var rp = new HttpResponseMessageProperty ();
308                         rp.Headers ["Content-Type"] = "text/html";
309                         m.Properties.Add (HttpResponseMessageProperty.Name, rp);
310                         return m;
311                 }
312
313                 SMMessage CreateWsdlMessage (WSServiceDescription wsdl)
314                 {
315                         MemoryStream ms = new MemoryStream ();
316                         XmlWriter xw = XmlWriter.Create (ms);
317
318                         WSServiceDescription.Serializer.Serialize (xw, wsdl);
319                         ms.Seek (0, SeekOrigin.Begin);
320                         return SMMessage.CreateMessage (MessageVersion.None, "", XmlReader.Create (ms));
321                 }
322
323                 void GetMetadata ()
324                 {
325                         if (WsdlUrl == null)
326                                 return;
327
328                         MetadataSet metadata = ext.Metadata;
329                         int xs_i = 0, wsdl_i = 0;
330
331                         //Dictionary keyed by namespace
332                         StringDictionary wsdl_strings = new StringDictionary ();
333                         StringDictionary xsd_strings = new StringDictionary ();
334
335                         foreach (MetadataSection section in metadata.MetadataSections) {
336                                 string key;
337
338                                 XmlSchema xs = section.Metadata as XmlSchema;
339                                 if (xs != null) {
340                                         key = String.Format ("xsd{0}", xs_i ++);
341                                         schemas [key] = xs;
342                                         xsd_strings [xs.TargetNamespace] = key;
343                                         continue;
344                                 }
345
346                                 WSServiceDescription wsdl = section.Metadata as WSServiceDescription;
347                                 if (wsdl == null)
348                                         continue;
349
350                                 //if (wsdl.TargetNamespace == "http://tempuri.org/")
351                                 if (wsdl.Services.Count > 0)
352                                         key = "wsdl";
353                                 else
354                                         key = String.Format ("wsdl{0}", wsdl_i ++);
355
356                                 wsdl_documents [key] = wsdl;
357                                 wsdl_strings [wsdl.TargetNamespace] = key;
358                         }
359                         
360                         string base_url = WsdlUrl.ToString ();
361                         foreach (WSServiceDescription wsdl in wsdl_documents.Values) {
362                                 foreach (Import import in wsdl.Imports) {
363                                         if (!String.IsNullOrEmpty (import.Location))
364                                                 continue;
365
366                                         import.Location = String.Format ("{0}?wsdl={1}", base_url, wsdl_strings [import.Namespace]);
367                                 }
368
369                                 foreach (XmlSchema schema in wsdl.Types.Schemas) {
370                                         foreach (XmlSchemaObject obj in schema.Includes) {
371                                                 XmlSchemaImport imp = obj as XmlSchemaImport;
372                                                 if (imp == null || imp.SchemaLocation != null)
373                                                         continue;
374
375                                                 imp.SchemaLocation = String.Format ("{0}?xsd={1}", base_url, xsd_strings [imp.Namespace]);
376                                         }
377                                 }
378                         }
379
380                 }
381                 
382                 WSServiceDescription GetWsdl (string which)
383                 {
384                         EnsureMetadata ();
385                         WSServiceDescription wsdl;
386                         wsdl_documents.TryGetValue (which, out wsdl);
387                         return wsdl;
388                 }
389                 
390                 XmlSchema GetXmlSchema (string which)
391                 {
392                         EnsureMetadata ();
393                         XmlSchema schema;
394                         schemas.TryGetValue (which, out schema);
395                         return schema;
396                 }
397
398         }
399
400 }