2010-03-17 Atsushi Enomoto <atsushi@ximian.com>
[mono.git] / mcs / class / System.ServiceModel / System.ServiceModel.Channels / HttpListenerManager.cs
index 5abdc748dac78d960b364064e33b84b3c23a5c55..0ce97088b5b2b4d53fe5e99f48e8402d0022bd82 100644 (file)
@@ -3,8 +3,10 @@
 //
 // Author:
 //     Vladimir Krasnov <vladimirk@mainsoft.com>
+//     Atsushi Enomoto  <atsushi@ximian.com>
 //
 // Copyright (C) 2005-2006 Mainsoft, Inc.  http://www.mainsoft.com
+// Copyright (C) 2009-2010 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
 //
 using System;
 using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.IdentityModel.Selectors;
+using System.IdentityModel.Tokens;
+using System.ServiceModel.Description;
+using System.ServiceModel.Security;
+using System.Security.Principal;
 using System.Text;
+using System.Threading;
 using System.Net;
+using System.Web;
 
 namespace System.ServiceModel.Channels
 {
-       internal class HttpListenerManager<TChannel> where TChannel : class, IChannel
+       abstract class HttpContextInfo
+       {
+               public abstract NameValueCollection QueryString { get; }
+               public abstract Uri RequestUrl { get; }
+               public abstract string HttpMethod { get; }
+               public abstract void Abort ();
+
+               public abstract string User { get; }
+               public abstract string Password { get; }
+               public abstract void ReturnUnauthorized ();
+       }
+
+       class HttpListenerContextInfo : HttpContextInfo
+       {
+               public HttpListenerContextInfo (HttpListenerContext ctx)
+               {
+                       this.ctx = ctx;
+               }
+               
+               HttpListenerContext ctx;
+
+               public HttpListenerContext Source {
+                       get { return ctx; }
+               }
+
+               public override NameValueCollection QueryString {
+                       get { return ctx.Request.QueryString; }
+               }
+               public override Uri RequestUrl {
+                       get { return ctx.Request.Url; }
+               }
+               public override string HttpMethod {
+                       get { return ctx.Request.HttpMethod; }
+               }
+               public override void Abort ()
+               {
+                       ctx.Response.Abort ();
+               }
+
+               public override string User {
+                       get { return ctx.User != null ? ((HttpListenerBasicIdentity) ctx.User.Identity).Name : null; }
+               }
+
+               public override string Password {
+                       get { return ctx.User != null ? ((HttpListenerBasicIdentity) ctx.User.Identity).Password : null; }
+               }
+
+               public override void ReturnUnauthorized ()
+               {
+                       ctx.Response.StatusCode = 401;
+               }
+       }
+
+       class AspNetHttpContextInfo : HttpContextInfo
+       {
+               public AspNetHttpContextInfo (HttpContext ctx)
+               {
+                       this.ctx = ctx;
+               }
+               
+               HttpContext ctx;
+
+               public HttpContext Source {
+                       get { return ctx; }
+               }
+
+               public override NameValueCollection QueryString {
+                       get { return ctx.Request.QueryString; }
+               }
+               public override Uri RequestUrl {
+                       get { return ctx.Request.Url; }
+               }
+               public override string HttpMethod {
+                       get { return ctx.Request.HttpMethod; }
+               }
+
+               public override void Abort ()
+               {
+                       ctx.Response.Close ();
+               }
+
+               public override string User {
+                       get { return ctx.User != null ? ((GenericIdentity) ctx.User.Identity).Name : null; }
+               }
+
+               // FIXME: how to acquire this?
+               public override string Password {
+                       get { return null; }
+               }
+
+               public override void ReturnUnauthorized ()
+               {
+                       ctx.Response.StatusCode = 401;
+               }
+       }
+
+       internal class HttpSimpleListenerManager : HttpListenerManager
        {
                static Dictionary<Uri, HttpListener> opened_listeners;
-               static Dictionary<Uri, List<HttpSimpleChannelListener<TChannel>>> registered_channels;
-               HttpSimpleChannelListener<TChannel> channel_listener;
                HttpListener http_listener;
 
-               static HttpListenerManager ()
+               static HttpSimpleListenerManager ()
                {
                        opened_listeners = new Dictionary<Uri, HttpListener> ();
-                       registered_channels = new Dictionary<Uri, List<HttpSimpleChannelListener<TChannel>>> ();
                }
 
-               public HttpListenerManager (HttpSimpleChannelListener<TChannel> channel_listener)
+               public HttpSimpleListenerManager (IChannelListener channelListener, HttpTransportBindingElement source, ServiceCredentialsSecurityTokenManager securityTokenManager)
+                       : base (channelListener, source, securityTokenManager)
                {
-                       this.channel_listener = channel_listener;
                }
 
-               public void Open (TimeSpan timeout)
+               protected override void OnRegister (IChannelListener channelListener, TimeSpan timeout)
                {
                        lock (opened_listeners) {
-                               if (!opened_listeners.ContainsKey (channel_listener.Uri)) {
+                               if (!opened_listeners.ContainsKey (channelListener.Uri)) {
                                        HttpListener listener = new HttpListener ();
+                                       listener.AuthenticationSchemeSelectorDelegate = delegate (HttpListenerRequest req) {
+                                               return Source.AuthenticationScheme;
+                                       };
+                                       listener.Realm = Source.Realm;
+                                       listener.UnsafeConnectionNtlmAuthentication = Source.UnsafeConnectionNtlmAuthentication;
 
-                                       string uriString = channel_listener.Uri.ToString ();
+                                       string uriString = channelListener.Uri.ToString ();
                                        if (!uriString.EndsWith ("/", StringComparison.Ordinal))
                                                uriString += "/";
                                        listener.Prefixes.Add (uriString);
                                        listener.Start ();
 
-                                       opened_listeners [channel_listener.Uri] = listener;
-                                       List<HttpSimpleChannelListener<TChannel>> registeredList = new List<HttpSimpleChannelListener<TChannel>> ();
-                                       registered_channels [channel_listener.Uri] = registeredList;
+                                       opened_listeners [channelListener.Uri] = listener;
                                }
 
-                               http_listener = opened_listeners [channel_listener.Uri];
-                               registered_channels [channel_listener.Uri].Add (channel_listener);
+                               http_listener = opened_listeners [channelListener.Uri];
                        }
                }
 
-               public void Stop (bool abort)
+               protected override void OnUnregister (IChannelListener listener, bool abort)
                {
                        lock (opened_listeners) {
                                if (http_listener == null)
                                        return;
-                               List<HttpSimpleChannelListener<TChannel>> channelsList = registered_channels [channel_listener.Uri];
-                               channelsList.Remove (channel_listener);
-                               if (channelsList.Count == 0) {
-                                       if (http_listener.IsListening) {
-                                               if (abort)
-                                                       http_listener.Abort ();
-                                               else
-                                                       http_listener.Close ();
+                               if (http_listener.IsListening) {
+                                       if (abort)
+                                               http_listener.Abort ();
+                                       else
+                                               http_listener.Close ();
+                               }
+                               ((IDisposable) http_listener).Dispose ();
+
+                               opened_listeners.Remove (listener.Uri);
+                       }
+
+                       http_listener = null;
+               }
+
+               protected override void KickContextReceiver (IChannelListener listener, Action<HttpContextInfo> contextReceivedCallback)
+               {
+                       http_listener.BeginGetContext (delegate (IAsyncResult result) {
+                               var hctx = http_listener.EndGetContext (result);
+                               contextReceivedCallback (new HttpListenerContextInfo (hctx));
+                       }, null);
+               }
+       }
+
+       internal class AspNetListenerManager : HttpListenerManager
+       {
+               SvcHttpHandler http_handler;
+
+               public AspNetListenerManager (IChannelListener channelListener, HttpTransportBindingElement source, ServiceCredentialsSecurityTokenManager securityTokenManager)
+                       : base (channelListener, source, securityTokenManager)
+               {
+                       http_handler = SvcHttpHandler.Current;
+               }
+
+               internal SvcHttpHandler HttpHandler { get { return http_handler; } }
+
+               protected override void OnRegister (IChannelListener channelListener, TimeSpan timeout)
+               {
+                       http_handler.RegisterListener (channelListener);
+               }
+
+               protected override void OnUnregister (IChannelListener listener, bool abort)
+               {
+                       http_handler.UnregisterListener (listener);
+               }
+
+               Func<IChannelListener,HttpContext> wait_delegate;
+
+               protected override void KickContextReceiver (IChannelListener listener, Action<HttpContextInfo> contextReceivedCallback)
+               {
+                       if (wait_delegate == null)
+                               wait_delegate = new Func<IChannelListener,HttpContext> (http_handler.WaitForRequest);
+                       wait_delegate.BeginInvoke (listener, delegate (IAsyncResult result) {
+                               var ctx = wait_delegate.EndInvoke (result);
+                               contextReceivedCallback (ctx != null ? new AspNetHttpContextInfo (ctx) : null);
+                               }, null);
+               }
+       }
+
+       internal abstract class HttpListenerManager
+       {
+               static Dictionary<Uri, List<IChannelListener>> registered_channels;
+               IChannelListener channel_listener;
+               MetadataPublishingInfo mex_info;
+               HttpGetWsdl wsdl_instance;
+               AutoResetEvent wait_http_ctx = new AutoResetEvent (false);
+               List<HttpContextInfo> pending = new List<HttpContextInfo> ();
+
+               public MetadataPublishingInfo MexInfo { get { return mex_info; } }
+               public HttpTransportBindingElement Source { get; private set; }
+
+               SecurityTokenAuthenticator security_token_authenticator;
+               SecurityTokenResolver security_token_resolver;
+
+               static HttpListenerManager ()
+               {
+                       registered_channels = new Dictionary<Uri, List<IChannelListener>> ();
+               }
+
+               protected HttpListenerManager (IChannelListener channelListener, HttpTransportBindingElement source, ServiceCredentialsSecurityTokenManager securityTokenManager)
+               {
+                       this.channel_listener = channelListener;
+                       // FIXME: this cast should not be required, but current JIT somehow causes an internal error.
+                       mex_info = ((IChannelListener) channelListener).GetProperty<MetadataPublishingInfo> ();
+                       wsdl_instance = mex_info != null ? mex_info.Instance : null;
+                       Source = source;
+
+                       if (securityTokenManager != null) {
+                               var str = new SecurityTokenRequirement () { TokenType = SecurityTokenTypes.UserName };
+                               security_token_authenticator = securityTokenManager.CreateSecurityTokenAuthenticator (str, out security_token_resolver);
+                       }
+               }
+
+               public void Open (TimeSpan timeout)
+               {
+                       if (!registered_channels.ContainsKey (channel_listener.Uri))
+                               registered_channels [channel_listener.Uri] = new List<IChannelListener> ();
+                       OnRegister (channel_listener, timeout);
+                       registered_channels [channel_listener.Uri].Add (channel_listener);
+
+                       // make sure to fill wsdl_instance among other 
+                       // listeners. It is somewhat hacky way, but 
+                       // otherwise there is no assured way to do it.
+                       var wsdl = wsdl_instance;
+                       if (wsdl == null)
+                               foreach (var l in registered_channels [channel_listener.Uri])
+                                       if ((wsdl = l.GetProperty<HttpListenerManager> ().wsdl_instance) != null)
+                                               break;
+                       if (wsdl != null) {
+                               foreach (var l in registered_channels [channel_listener.Uri])
+                                       l.GetProperty<HttpListenerManager> ().wsdl_instance = wsdl;
+                       }
+               }
+
+               public void Stop (bool abort)
+               {
+                       List<IChannelListener> channelsList = registered_channels [channel_listener.Uri];
+                       channelsList.Remove (channel_listener);
+
+                       try {
+                               foreach (var ctx in pending)
+                                       ctx.Abort ();
+                       } catch (Exception ex) {
+                               // FIXME: log it
+                               Console.WriteLine ("error during HTTP channel listener shutdown: " + ex);
+                       }
+
+                       if (channelsList.Count == 0)
+                               OnUnregister (channel_listener, abort);
+               }
+
+               protected abstract void OnRegister (IChannelListener listener, TimeSpan timeout);
+               protected abstract void OnUnregister (IChannelListener listener, bool abort);
+
+               public void CancelGetHttpContextAsync ()
+               {
+                       wait_http_ctx.Set ();
+               }
+
+               // Do not directly handle retrieved HttpListenerContexts when
+               // the listener received ones.
+               // Instead, iterate every listeners to find the most-likely-
+               // matching one and immediately handle the listener context.
+               // If the listener is not requesting a context right now, then
+               // store it in *each* listener's queue.
+
+               public void GetHttpContextAsync (TimeSpan timeout, Action<HttpContextInfo> callback)
+               {
+                       lock (pending) {
+                               foreach (var pctx in pending) {
+                                       if (FilterHttpContext (pctx)) {
+                                               callback (pctx);
+                                               return;
                                        }
-                                       ((IDisposable) http_listener).Dispose ();
+                               }
+                       }
+                       KickContextReceiver (channel_listener, DispatchHttpListenerContext);
+                       wait_http_ctx.WaitOne (timeout);
+                       lock (pending) {
+                               HttpContextInfo ctx = pending.Count > 0 ? pending [0] : null;
+                               if (ctx != null)
+                                       pending.Remove (ctx);
+                               callback (ctx);
+                       }
+               }
+
+               protected abstract void KickContextReceiver (IChannelListener listener, Action<HttpContextInfo> contextReceiverCallback);
 
-                                       opened_listeners.Remove (channel_listener.Uri);
+               void DispatchHttpListenerContext (HttpContextInfo ctx)
+               {
+                       if (wsdl_instance == null) {
+                               AddListenerContext (ctx);
+                               return;
+                       }
+                       foreach (var l in registered_channels [channel_listener.Uri]) {
+                               var lm = l.GetProperty<HttpListenerManager> ();
+                               if (lm.FilterHttpContext (ctx)) {
+                                       lm.AddListenerContext (ctx);
+                                       return;
                                }
                        }
+                       AddListenerContext (ctx);
                }
 
-               public HttpListener HttpListener
+               void AddListenerContext (HttpContextInfo ctx)
                {
-                       get { return http_listener; }
+                       if (Source.AuthenticationScheme != AuthenticationSchemes.Anonymous) {
+                               if (security_token_authenticator != null)
+                                       // FIXME: use return value?
+                                       try {
+                                               security_token_authenticator.ValidateToken (new UserNameSecurityToken (ctx.User, ctx.Password));
+                                       } catch (Exception) {
+                                               ctx.ReturnUnauthorized ();
+                                       }
+                               else {
+                                       ctx.ReturnUnauthorized ();
+                               }
+                       }
+
+                       lock (registered_channels) {
+                               pending.Add (ctx);
+                               // FIXME: this should not be required, but it somehow saves some failures wrt concurrent calls.
+                               Thread.Sleep (100);
+                               wait_http_ctx.Set ();
+                       }
+               }
+
+               const UriComponents cmpflag = UriComponents.HttpRequestUrl ^ UriComponents.Query;
+               const UriFormat fmtflag = UriFormat.SafeUnescaped;
+
+               internal bool FilterHttpContext (HttpContextInfo ctx)
+               {
+                       if (ctx.HttpMethod.ToUpper () != "GET")
+                               return mex_info == null;
+
+                       if (wsdl_instance == null)
+                               return true;
+                       if (channel_listener.State != CommunicationState.Opened)
+                               return true;
+
+                       if (wsdl_instance.WsdlUrl != null && Uri.Compare (ctx.RequestUrl, wsdl_instance.WsdlUrl, cmpflag, fmtflag, StringComparison.Ordinal) == 0) {
+                               if (mex_info == null)
+                                       return false; // Do not handle this at normal dispatcher.
+                               if (ctx.QueryString [null] == "wsdl")
+                                       return mex_info.SupportsMex; // wsdl dispatcher should handle this.
+                               if (!wsdl_instance.HelpUrl.Equals (wsdl_instance.WsdlUrl))
+                                       return true; // in case help URL is not equivalent to WSDL URL, it anyways returns WSDL regardless of ?wsdl existence.
+                       }
+                       if (wsdl_instance.HelpUrl != null && Uri.Compare (ctx.RequestUrl, wsdl_instance.HelpUrl, cmpflag, fmtflag, StringComparison.Ordinal) == 0) {
+                               // Do not handle this at normal dispatcher.
+                               // Do return true otherwise, even if it is with "?wsdl".
+                               // (It must be handled above if applicable.)
+                               return mex_info != null;
+                       }
+
+                       return mex_info == null;
                }
-       }       
+       }
 }