Merge pull request #4569 from lambdageek/dev-coop-handles-march
[mono.git] / mcs / class / System / System.Net / HttpListener.cs
1 //
2 // System.Net.HttpListener
3 //
4 // Authors:
5 //      Gonzalo Paniagua Javier (gonzalo@novell.com)
6 //      Marek Safar (marek.safar@gmail.com)
7 //
8 // Copyright (c) 2005 Novell, Inc. (http://www.novell.com)
9 // Copyright 2011 Xamarin Inc.
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
31 #if SECURITY_DEP
32 #if MONO_SECURITY_ALIAS
33 extern alias MonoSecurity;
34 using MonoSecurity::Mono.Security.Authenticode;
35 using MSI = MonoSecurity::Mono.Security.Interface;
36 #else
37 using Mono.Security.Authenticode;
38 using MSI = Mono.Security.Interface;
39 #endif
40
41 using System.IO;
42 using System.Collections;
43 using System.Threading;
44 using System.Threading.Tasks;
45 using System.Security.Authentication.ExtendedProtection;
46 using System.Security.Cryptography;
47 using System.Security.Cryptography.X509Certificates;
48
49 using Mono.Net.Security;
50
51
52 //TODO: logging
53 namespace System.Net {
54         public sealed class HttpListener : IDisposable {
55                 AuthenticationSchemes auth_schemes;
56                 HttpListenerPrefixCollection prefixes;
57                 AuthenticationSchemeSelector auth_selector; 
58                 string realm;
59                 bool ignore_write_exceptions;
60                 bool unsafe_ntlm_auth;
61                 bool listening;
62                 bool disposed;
63
64                 IMonoTlsProvider tlsProvider;
65                 MSI.MonoTlsSettings tlsSettings;
66                 X509Certificate certificate;
67
68                 Hashtable registry;   // Dictionary<HttpListenerContext,HttpListenerContext> 
69                 ArrayList ctx_queue;  // List<HttpListenerContext> ctx_queue;
70                 ArrayList wait_queue; // List<ListenerAsyncResult> wait_queue;
71                 Hashtable connections;
72
73                 ServiceNameStore defaultServiceNames;
74                 ExtendedProtectionPolicy extendedProtectionPolicy;
75                 ExtendedProtectionSelector extendedProtectionSelectorDelegate;
76
77                 public delegate ExtendedProtectionPolicy ExtendedProtectionSelector (HttpListenerRequest request);
78
79                 public HttpListener ()
80                 {
81                         prefixes = new HttpListenerPrefixCollection (this);
82                         registry = new Hashtable ();
83                         connections = Hashtable.Synchronized (new Hashtable ());
84                         ctx_queue = new ArrayList ();
85                         wait_queue = new ArrayList ();
86                         auth_schemes = AuthenticationSchemes.Anonymous;
87                         defaultServiceNames = new ServiceNameStore ();
88                         extendedProtectionPolicy = new ExtendedProtectionPolicy (PolicyEnforcement.Never);
89                 }
90
91                 internal HttpListener (X509Certificate certificate, IMonoTlsProvider tlsProvider, MSI.MonoTlsSettings tlsSettings)
92                         : this ()
93                 {
94                         this.certificate = certificate;
95                         this.tlsProvider = tlsProvider;
96                         this.tlsSettings = tlsSettings;
97                 }
98
99                 internal X509Certificate LoadCertificateAndKey (IPAddress addr, int port)
100                 {
101                         lock (registry) {
102                                 if (certificate != null)
103                                         return certificate;
104
105                                 // Actually load the certificate
106                                 try {
107                                         string dirname = Environment.GetFolderPath (Environment.SpecialFolder.ApplicationData);
108                                         string path = Path.Combine (dirname, ".mono");
109                                         path = Path.Combine (path, "httplistener");
110                                         string cert_file = Path.Combine (path, String.Format ("{0}.cer", port));
111                                         if (!File.Exists (cert_file))
112                                                 return null;
113                                         string pvk_file = Path.Combine (path, String.Format ("{0}.pvk", port));
114                                         if (!File.Exists (pvk_file))
115                                                 return null;
116                                         var cert = new X509Certificate2 (cert_file);
117                                         cert.PrivateKey = PrivateKey.CreateFromFile (pvk_file).RSA;
118                                         certificate = cert;
119                                         return certificate;
120                                 } catch {
121                                         // ignore errors
122                                         certificate = null;
123                                         return null;
124                                 }
125                         }
126                 }
127
128                 internal IMonoSslStream CreateSslStream (Stream innerStream, bool ownsStream, MSI.MonoRemoteCertificateValidationCallback callback)
129                 {
130                         lock (registry) {
131                                 if (tlsProvider == null)
132                                         tlsProvider = MonoTlsProviderFactory.GetProviderInternal ();
133                                 if (tlsSettings == null)
134                                         tlsSettings = MSI.MonoTlsSettings.CopyDefaultSettings ();
135                                 if (tlsSettings.RemoteCertificateValidationCallback == null)
136                                         tlsSettings.RemoteCertificateValidationCallback = callback;
137                                 return tlsProvider.CreateSslStream (innerStream, ownsStream, tlsSettings);
138                         }
139                 }
140
141                 // TODO: Digest, NTLM and Negotiate require ControlPrincipal
142                 public AuthenticationSchemes AuthenticationSchemes {
143                         get { return auth_schemes; }
144                         set {
145                                 CheckDisposed ();
146                                 auth_schemes = value;
147                         }
148                 }
149
150                 public AuthenticationSchemeSelector AuthenticationSchemeSelectorDelegate {
151                         get { return auth_selector; }
152                         set {
153                                 CheckDisposed ();
154                                 auth_selector = value;
155                         }
156                 }
157
158                 public ExtendedProtectionSelector ExtendedProtectionSelectorDelegate
159                 {
160                         get { return extendedProtectionSelectorDelegate; }
161                         set {
162                                 CheckDisposed();
163                                 if (value == null)
164                                         throw new ArgumentNullException ();
165
166                                 if (!AuthenticationManager.OSSupportsExtendedProtection)
167                                         throw new PlatformNotSupportedException (SR.GetString (SR.security_ExtendedProtection_NoOSSupport));
168
169                                 extendedProtectionSelectorDelegate = value;
170                         }
171                 }
172
173                 public bool IgnoreWriteExceptions {
174                         get { return ignore_write_exceptions; }
175                         set {
176                                 CheckDisposed ();
177                                 ignore_write_exceptions = value;
178                         }
179                 }
180
181                 public bool IsListening {
182                         get { return listening; }
183                 }
184
185                 public static bool IsSupported {
186                         get { return true; }
187                 }
188
189                 public HttpListenerPrefixCollection Prefixes {
190                         get {
191                                 CheckDisposed ();
192                                 return prefixes;
193                         }
194                 }
195
196                 [MonoTODO]
197                 public HttpListenerTimeoutManager TimeoutManager {
198                         get {
199                                 throw new NotImplementedException ();
200                         }
201                 }
202
203                 [MonoTODO ("not used anywhere in the implementation")]
204                 public ExtendedProtectionPolicy ExtendedProtectionPolicy
205                 {
206                         get {
207                                 return extendedProtectionPolicy;
208                         }
209                         set {
210                                 CheckDisposed ();
211
212                                 if (value == null)
213                                         throw new ArgumentNullException ("value");
214
215                                 if (!AuthenticationManager.OSSupportsExtendedProtection && value.PolicyEnforcement == PolicyEnforcement.Always)
216                                         throw new PlatformNotSupportedException (SR.GetString(SR.security_ExtendedProtection_NoOSSupport));
217
218                                 if (value.CustomChannelBinding != null)
219                                         throw new ArgumentException (SR.GetString (SR.net_listener_cannot_set_custom_cbt), "CustomChannelBinding");
220  
221                                 extendedProtectionPolicy = value;
222                         }
223                 }
224
225                 public ServiceNameCollection DefaultServiceNames
226                 {
227                         get {
228                                 return defaultServiceNames.ServiceNames;
229                         }
230                 }
231
232                 // TODO: use this
233                 public string Realm {
234                         get { return realm; }
235                         set {
236                                 CheckDisposed ();
237                                 realm = value;
238                         }
239                 }
240
241                 [MonoTODO ("Support for NTLM needs some loving.")]
242                 public bool UnsafeConnectionNtlmAuthentication {
243                         get { return unsafe_ntlm_auth; }
244                         set {
245                                 CheckDisposed ();
246                                 unsafe_ntlm_auth = value;
247                         }
248                 }
249
250                 public void Abort ()
251                 {
252                         if (disposed)
253                                 return;
254
255                         if (!listening) {
256                                 return;
257                         }
258
259                         Close (true);
260                 }
261
262                 public void Close ()
263                 {
264                         if (disposed)
265                                 return;
266
267                         if (!listening) {
268                                 disposed = true;
269                                 return;
270                         }
271
272                         Close (true);
273                         disposed = true;
274                 }
275
276                 void Close (bool force)
277                 {
278                         CheckDisposed ();
279                         EndPointManager.RemoveListener (this);
280                         Cleanup (force);
281                 }
282
283                 void Cleanup (bool close_existing)
284                 {
285                         lock (registry) {
286                                 if (close_existing) {
287                                         // Need to copy this since closing will call UnregisterContext
288                                         ICollection keys = registry.Keys;
289                                         var all = new HttpListenerContext [keys.Count];
290                                         keys.CopyTo (all, 0);
291                                         registry.Clear ();
292                                         for (int i = all.Length - 1; i >= 0; i--)
293                                                 all [i].Connection.Close (true);
294                                 }
295
296                                 lock (connections.SyncRoot) {
297                                         ICollection keys = connections.Keys;
298                                         var conns = new HttpConnection [keys.Count];
299                                         keys.CopyTo (conns, 0);
300                                         connections.Clear ();
301                                         for (int i = conns.Length - 1; i >= 0; i--)
302                                                 conns [i].Close (true);
303                                 }
304                                 lock (ctx_queue) {
305                                         var ctxs = (HttpListenerContext []) ctx_queue.ToArray (typeof (HttpListenerContext));
306                                         ctx_queue.Clear ();
307                                         for (int i = ctxs.Length - 1; i >= 0; i--)
308                                                 ctxs [i].Connection.Close (true);
309                                 }
310
311                                 lock (wait_queue) {
312                                         Exception exc = new ObjectDisposedException ("listener");
313                                         foreach (ListenerAsyncResult ares in wait_queue) {
314                                                 ares.Complete (exc);
315                                         }
316                                         wait_queue.Clear ();
317                                 }
318                         }
319                 }
320
321                 public IAsyncResult BeginGetContext (AsyncCallback callback, Object state)
322                 {
323                         CheckDisposed ();
324                         if (!listening)
325                                 throw new InvalidOperationException ("Please, call Start before using this method.");
326
327                         ListenerAsyncResult ares = new ListenerAsyncResult (callback, state);
328
329                         // lock wait_queue early to avoid race conditions
330                         lock (wait_queue) {
331                                 lock (ctx_queue) {
332                                         HttpListenerContext ctx = GetContextFromQueue ();
333                                         if (ctx != null) {
334                                                 ares.Complete (ctx, true);
335                                                 return ares;
336                                         }
337                                 }
338
339                                 wait_queue.Add (ares);
340                         }
341
342                         return ares;
343                 }
344
345                 public HttpListenerContext EndGetContext (IAsyncResult asyncResult)
346                 {
347                         CheckDisposed ();
348                         if (asyncResult == null)
349                                 throw new ArgumentNullException ("asyncResult");
350
351                         ListenerAsyncResult ares = asyncResult as ListenerAsyncResult;
352                         if (ares == null)
353                                 throw new ArgumentException ("Wrong IAsyncResult.", "asyncResult");
354                         if (ares.EndCalled)
355                                 throw new ArgumentException ("Cannot reuse this IAsyncResult");
356                         ares.EndCalled = true;
357
358                         if (!ares.IsCompleted)
359                                 ares.AsyncWaitHandle.WaitOne ();
360
361                         lock (wait_queue) {
362                                 int idx = wait_queue.IndexOf (ares);
363                                 if (idx >= 0)
364                                         wait_queue.RemoveAt (idx);
365                         }
366
367                         HttpListenerContext context = ares.GetContext ();
368                         context.ParseAuthentication (SelectAuthenticationScheme (context));
369                         return context; // This will throw on error.
370                 }
371
372                 internal AuthenticationSchemes SelectAuthenticationScheme (HttpListenerContext context)
373                 {
374                         if (AuthenticationSchemeSelectorDelegate != null)
375                                 return AuthenticationSchemeSelectorDelegate (context.Request);
376                         else
377                                 return auth_schemes;
378                 }
379
380                 public HttpListenerContext GetContext ()
381                 {
382                         // The prefixes are not checked when using the async interface!?
383                         if (prefixes.Count == 0)
384                                 throw new InvalidOperationException ("Please, call AddPrefix before using this method.");
385
386                         ListenerAsyncResult ares = (ListenerAsyncResult) BeginGetContext (null, null);
387                         ares.InGet = true;
388                         return EndGetContext (ares);
389                 }
390
391                 public void Start ()
392                 {
393                         CheckDisposed ();
394                         if (listening)
395                                 return;
396
397                         EndPointManager.AddListener (this);
398                         listening = true;
399                 }
400
401                 public void Stop ()
402                 {
403                         CheckDisposed ();
404                         listening = false;
405                         Close (false);
406                 }
407
408                 void IDisposable.Dispose ()
409                 {
410                         if (disposed)
411                                 return;
412
413                         Close (true); //TODO: Should we force here or not?
414                         disposed = true;
415                 }
416
417                 public Task<HttpListenerContext> GetContextAsync ()
418                 {
419                         return Task<HttpListenerContext>.Factory.FromAsync (BeginGetContext, EndGetContext, null);
420                 }
421
422                 internal void CheckDisposed ()
423                 {
424                         if (disposed)
425                                 throw new ObjectDisposedException (GetType ().ToString ());
426                 }
427
428                 // Must be called with a lock on ctx_queue
429                 HttpListenerContext GetContextFromQueue ()
430                 {
431                         if (ctx_queue.Count == 0)
432                                 return null;
433
434                         HttpListenerContext context = (HttpListenerContext) ctx_queue [0];
435                         ctx_queue.RemoveAt (0);
436                         return context;
437                 }
438
439                 internal void RegisterContext (HttpListenerContext context)
440                 {
441                         lock (registry)
442                                 registry [context] = context;
443
444                         ListenerAsyncResult ares = null;
445                         lock (wait_queue) {
446                                 if (wait_queue.Count == 0) {
447                                         lock (ctx_queue)
448                                                 ctx_queue.Add (context);
449                                 } else {
450                                         ares = (ListenerAsyncResult) wait_queue [0];
451                                         wait_queue.RemoveAt (0);
452                                 }
453                         }
454                         if (ares != null)
455                                 ares.Complete (context);
456                 }
457
458                 internal void UnregisterContext (HttpListenerContext context)
459                 {
460                         lock (registry)
461                                 registry.Remove (context);
462                         lock (ctx_queue) {
463                                 int idx = ctx_queue.IndexOf (context);
464                                 if (idx >= 0)
465                                         ctx_queue.RemoveAt (idx);
466                         }
467                 }
468
469                 internal void AddConnection (HttpConnection cnc)
470                 {
471                         connections [cnc] = cnc;
472                 }
473
474                 internal void RemoveConnection (HttpConnection cnc)
475                 {
476                         connections.Remove (cnc);
477                 }
478         }
479 }
480 #else // SECURITY_DEP
481 namespace System.Net
482 {
483         public sealed class HttpListener
484         {
485         }
486 }
487 #endif