2 // ApplicationServer.cs
5 // Gonzalo Paniagua Javier (gonzalo@ximian.com)
6 // Lluis Sanchez Gual (lluis@ximian.com)
8 // Copyright (c) Copyright 2002,2003,2004 Novell, Inc
10 // Permission is hereby granted, free of charge, to any person obtaining
11 // a copy of this software and associated documentation files (the
12 // "Software"), to deal in the Software without restriction, including
13 // without limitation the rights to use, copy, modify, merge, publish,
14 // distribute, sublicense, and/or sell copies of the Software, and to
15 // permit persons to whom the Software is furnished to do so, subject to
16 // the following conditions:
18 // The above copyright notice and this permission notice shall be
19 // included in all copies or substantial portions of the Software.
21 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
25 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
26 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
27 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
33 using System
.Net
.Sockets
;
36 using System
.Web
.Hosting
;
37 using System
.Collections
;
39 using System
.Threading
;
41 using System
.Globalization
;
42 using System
.Runtime
.InteropServices
;
46 // ApplicationServer runs the main server thread, which accepts client
47 // connections and forwards the requests to the correct web application.
48 // ApplicationServer takes an IWebSource object as parameter in the
49 // constructor. IWebSource provides methods for getting some objects
50 // whose behavior is specific to XSP or mod_mono.
52 // Each web application lives in its own application domain, and incoming
53 // requests are processed in the corresponding application domain.
54 // Since the client Socket can't be passed from one domain to the other, the
55 // flow of information must go through the cross-app domain channel.
57 // For each application two objects are created:
58 // 1) a IApplicationHost object is created in the application domain
59 // 2) a IRequestBroker is created in the main domain.
61 // The IApplicationHost is used by the ApplicationServer to start the
62 // processing of a request in the application domain.
63 // The IRequestBroker is used from the application domain to access
64 // information in the main domain.
66 // The complete sequence of servicing a request is the following:
68 // 1) The listener accepts an incoming connection.
69 // 2) An IWorker object is created (through the IWebSource), and it is
70 // queued in the thread pool.
71 // 3) When the IWorker's run method is called, it registers itself in
72 // the application's request broker, and gets a request id. All this is
73 // done in the main domain.
74 // 4) The IWorker starts the request processing by making a cross-app domain
75 // call to the application host. It passes as parameters the request id
76 // and other information already read from the request.
77 // 5) The application host executes the request. When it needs to read or
78 // write request data, it performs remote calls to the request broker,
79 // passing the request id provided by the IWorker.
80 // 6) When the request broker receives a call from the application host,
81 // it locates the IWorker registered with the provided request id and
82 // forwards the call to it.
84 public class ApplicationServer
: MarshalByRefObject
94 // This is much faster than hashtable for typical cases.
95 ArrayList vpathToHost
= new ArrayList ();
97 public ApplicationServer (IWebSource source
)
102 public bool Verbose
{
103 get { return verbose; }
104 set { verbose = value; }
107 private void AddApplication (string vhost
, int vport
, string vpath
, string fullPath
)
109 // TODO - check for duplicates, sort, optimize, etc.
111 throw new InvalidOperationException ("The server is already started.");
114 Console
.WriteLine("Registering application:");
115 Console
.WriteLine(" Host: {0}", (vhost
!= null) ? vhost
: "any");
116 Console
.WriteLine(" Port: {0}", (vport
!= -1) ?
117 vport
.ToString () : "any");
119 Console
.WriteLine(" Virtual path: {0}", vpath
);
120 Console
.WriteLine(" Physical path: {0}", fullPath
);
123 vpathToHost
.Add (new VPathToHost (vhost
, vport
, vpath
, fullPath
));
126 public void AddApplicationsFromConfigDirectory (string directoryName
)
129 Console
.WriteLine ("Adding applications from *.webapp files in " +
130 "directory '{0}'", directoryName
);
133 DirectoryInfo di
= new DirectoryInfo (directoryName
);
135 Console
.Error
.WriteLine ("Directory {0} does not exist.", directoryName
);
139 foreach (FileInfo fi
in di
.GetFiles ("*.webapp"))
140 AddApplicationsFromConfigFile (fi
.FullName
);
143 public void AddApplicationsFromConfigFile (string fileName
)
146 Console
.WriteLine ("Adding applications from config file '{0}'", fileName
);
150 XmlDocument doc
= new XmlDocument ();
153 foreach (XmlElement el
in doc
.SelectNodes ("//web-application")) {
154 AddApplicationFromElement (el
);
157 Console
.WriteLine ("Error loading '{0}'", fileName
);
162 void AddApplicationFromElement (XmlElement el
)
166 n
= el
.SelectSingleNode ("enabled");
167 if (n
!= null && n
.InnerText
.Trim () == "false")
170 string vpath
= el
.SelectSingleNode ("vpath").InnerText
;
171 string path
= el
.SelectSingleNode ("path").InnerText
;
174 n
= el
.SelectSingleNode ("vhost");
179 // TODO: support vhosts in xsp.exe
180 string name
= el
.SelectSingleNode ("name").InnerText
;
182 Console
.WriteLine ("Ignoring vhost {0} for {1}", n
.InnerText
, name
);
186 n
= el
.SelectSingleNode ("vport");
189 vport
= Convert
.ToInt32 (n
.InnerText
);
191 // TODO: Listen on different ports
193 Console
.WriteLine ("Ignoring vport {0} for {1}", n
.InnerText
, name
);
196 AddApplication (vhost
, vport
, vpath
, path
);
199 public void AddApplicationsFromCommandLine (string applications
)
201 if (applications
== null)
202 throw new ArgumentNullException ("applications");
204 if (applications
== "")
208 Console
.WriteLine("Adding applications '{0}'...", applications
);
211 string [] apps
= applications
.Split (',');
213 foreach (string str
in apps
) {
214 string [] app
= str
.Split (':');
216 if (app
.Length
< 2 || app
.Length
> 4)
217 throw new ArgumentException ("Should be something like " +
218 "[[hostname:]port:]VPath:realpath");
226 if (app
.Length
>= 3) {
232 if (app
.Length
>= 4) {
233 // FIXME: support more than one listen port.
234 vport
= Convert
.ToInt16 (app
[pos
++]);
240 realpath
= app
[pos
++];
242 if (!vpath
.EndsWith ("/"))
245 string fullPath
= System
.IO
.Path
.GetFullPath (realpath
);
246 AddApplication (vhost
, vport
, vpath
, fullPath
);
250 public bool Start (bool bgThread
)
253 throw new InvalidOperationException ("The server is already started.");
255 if (vpathToHost
== null)
256 throw new InvalidOperationException ("SetApplications must be called first.");
258 if (vpathToHost
.Count
== 0)
259 throw new InvalidOperationException ("No applications defined or all of them are disabled");
261 listen_socket
= webSource
.CreateSocket ();
262 listen_socket
.Listen (500);
263 listen_socket
.Blocking
= false;
264 runner
= new Thread (new ThreadStart (RunServer
));
265 runner
.IsBackground
= bgThread
;
268 WebTrace
.WriteLine ("Server started.");
275 throw new InvalidOperationException ("The server is not started.");
278 return; // Just ignore, as we're already stopping
281 webSource
.Dispose ();
283 // A foreground thread is required to end cleanly
284 Thread stopThread
= new Thread (new ThreadStart (RealStop
));
291 listen_socket
.Close ();
294 WebTrace
.WriteLine ("Server stopped.");
297 public void UnloadAll ()
300 foreach (VPathToHost v
in vpathToHost
) {
306 void SetSocketOptions (Socket sock
)
310 sock
.SetSocketOption (SocketOptionLevel
.Socket
, SocketOptionName
.SendTimeout
, 15000); // 15s
311 sock
.SetSocketOption (SocketOptionLevel
.Socket
, SocketOptionName
.ReceiveTimeout
, 15000); // 15s
313 // Ignore exceptions here for systems that do not support these options.
318 SocketPool spool
= new SocketPool ();
322 spool
.AddReadSocket (listen_socket
);
327 ArrayList wSockets
= spool
.SelectRead ();
329 for (int i
= 0; i
< w
; i
++) {
330 Socket s
= (Socket
) wSockets
[i
];
331 if (s
== listen_socket
) {
333 client
= s
.Accept ();
334 client
.Blocking
= true;
335 } catch (Exception e
) {
338 WebTrace
.WriteLine ("Accepted connection.");
339 SetSocketOptions (client
);
340 spool
.AddReadSocket (client
, DateTime
.UtcNow
);
344 spool
.RemoveReadSocket (s
);
345 IWorker worker
= webSource
.CreateWorker (s
, this);
346 ThreadPool
.QueueUserWorkItem (new WaitCallback (worker
.Run
));
353 public VPathToHost
GetApplicationForPath (string vhost
, int port
, string path
,
356 VPathToHost bestMatch
= null;
357 int bestMatchLength
= 0;
359 // Console.WriteLine("GetApplicationForPath({0},{1},{2},{3})", vhost, port,
360 // path, defaultToRoot);
361 for (int i
= vpathToHost
.Count
- 1; i
>= 0; i
--) {
362 VPathToHost v
= (VPathToHost
) vpathToHost
[i
];
363 int matchLength
= v
.vpath
.Length
;
364 if (matchLength
<= bestMatchLength
|| !v
.Match (vhost
, port
, path
))
367 bestMatchLength
= matchLength
;
371 if (bestMatch
!= null) {
373 if (bestMatch
.AppHost
== null)
374 bestMatch
.CreateHost (this, webSource
);
380 return GetApplicationForPath (vhost
, port
, "/", false);
383 Console
.WriteLine ("No application defined for: {0}:{1}{2}", vhost
, port
, path
);
388 public void DestroyHost (IApplicationHost host
)
390 // Called when the host appdomain is being unloaded
391 for (int i
= vpathToHost
.Count
- 1; i
>= 0; i
--) {
392 VPathToHost v
= (VPathToHost
) vpathToHost
[i
];
393 if (v
.TryClearHost (host
))
398 public override object InitializeLifetimeService ()
403 public int GetAvailableReuses (Socket sock
)
405 int res
= spool
.GetReuseCount (sock
);
406 if (res
== -1 || res
>= 100)
412 public void ReuseSocket (Socket sock
)
414 IWorker worker
= webSource
.CreateWorker (sock
, this);
415 spool
.IncrementReuseCount (sock
);
416 ThreadPool
.QueueUserWorkItem (new WaitCallback (worker
.Run
));
419 public void CloseSocket (Socket sock
)
421 spool
.RemoveReadSocket (sock
);
426 ArrayList readSockets
= new ArrayList ();
427 Hashtable timeouts
= new Hashtable ();
428 Hashtable uses
= new Hashtable ();
429 object locker
= new object ();
431 public ArrayList
SelectRead ()
433 if (readSockets
.Count
== 0)
434 throw new InvalidOperationException ("There are no sockets.");
436 ArrayList wSockets
= new ArrayList (readSockets
);
437 // A bug on MS (or is it just me?) makes the following select return immediately
438 // when there's only one socket (the listen socket) in the array:
439 // Socket.Select (wSockets, null, null, (w == 1) ? -1 : 1000 * 1000); // 1s
440 // so i have to do this for the MS runtime not to hung all the CPU.
441 if (wSockets
.Count
> 1) {
442 Socket
.Select (wSockets
, null, null, 1000 * 1000); // 1s
444 Socket sock
= (Socket
) wSockets
[0];
445 sock
.Poll (-1, SelectMode
.SelectRead
);
446 // wSockets already contains listen_socket.
450 CheckTimeouts (wSockets
);
455 void CheckTimeouts (ArrayList wSockets
)
457 int w
= timeouts
.Count
;
459 Socket
[] socks_timeout
= new Socket
[w
];
460 timeouts
.Keys
.CopyTo (socks_timeout
, 0);
461 DateTime now
= DateTime
.UtcNow
;
462 foreach (Socket k
in socks_timeout
) {
463 if (wSockets
.Contains (k
))
466 DateTime atime
= (DateTime
) timeouts
[k
];
467 TimeSpan diff
= now
- atime
;
468 if (diff
.TotalMilliseconds
> 15 * 1000) {
469 RemoveReadSocket (k
);
478 public void IncrementReuseCount (Socket sock
)
481 if (uses
.ContainsKey (sock
)) {
482 int n
= (int) uses
[sock
];
490 public int GetReuseCount (Socket sock
)
493 if (uses
.ContainsKey (sock
))
494 return (int) uses
[sock
];
501 public void AddReadSocket (Socket sock
)
504 readSockets
.Add (sock
);
507 public void AddReadSocket (Socket sock
, DateTime time
)
510 if (readSockets
.Contains (sock
)) {
511 timeouts
[sock
] = time
;
515 readSockets
.Add (sock
);
516 timeouts
[sock
] = time
;
520 public void RemoveReadSocket (Socket sock
)
523 readSockets
.Remove (sock
);
524 timeouts
.Remove (sock
);
530 public class VPathToHost
532 public readonly string vhost
;
533 public readonly int vport
;
534 public readonly string vpath
;
535 public readonly string realPath
;
536 public readonly bool haveWildcard
;
538 public IApplicationHost AppHost
;
539 public IRequestBroker RequestBroker
;
541 public VPathToHost (string vhost
, int vport
, string vpath
, string realPath
)
543 this.vhost
= (vhost
!= null) ? vhost
.ToLower (CultureInfo
.InvariantCulture
) : null;
546 if (vpath
== null || vpath
== "" || vpath
[0] != '/')
547 throw new ArgumentException ("Virtual path must begin with '/': " + vpath
,
550 this.realPath
= realPath
;
553 if (vhost
!= null && this.vhost
.Length
!= 0 && this.vhost
[0] == '*') {
555 if (this.vhost
.Length
> 2 && this.vhost
[1] == '.')
556 this.vhost
= this.vhost
.Substring (2);
561 public bool TryClearHost (IApplicationHost host
)
563 if (this.AppHost
== host
) {
571 public void UnloadHost ()
579 public bool Redirect (string path
, out string redirect
)
582 int plen
= path
.Length
;
583 if (plen
== this.vpath
.Length
- 1) {
584 redirect
= this.vpath
;
591 public bool Match (string vhost
, int vport
, string vpath
)
593 if (vport
!= -1 && this.vport
!= -1 && vport
!= this.vport
)
596 if (vhost
!= null && this.vhost
!= null) {
597 int length
= this.vhost
.Length
;
598 string lwrvhost
= vhost
.ToLower (CultureInfo
.InvariantCulture
);
600 if (this.vhost
== "*")
603 if (length
> vhost
.Length
)
606 if (length
== vhost
.Length
&& this.vhost
!= lwrvhost
)
609 if (vhost
[vhost
.Length
- length
- 1] != '.')
612 if (!lwrvhost
.EndsWith (this.vhost
))
615 } else if (this.vhost
!= lwrvhost
) {
620 int local
= vpath
.Length
;
621 int vlength
= this.vpath
.Length
;
622 if (vlength
> local
) {
623 // Check for /xxx requests to be redirected to /xxx/
624 if (this.vpath
[vlength
- 1] != '/')
627 return (vlength
- 1 == local
&& this.vpath
.Substring (0, vlength
- 1) == vpath
);
630 return (vpath
.StartsWith (this.vpath
));
633 public void CreateHost (ApplicationServer server
, IWebSource webSource
)
636 if (v
!= "/" && v
.EndsWith ("/")) {
637 v
= v
.Substring (0, v
.Length
- 1);
640 AppHost
= ApplicationHost
.CreateApplicationHost (webSource
.GetApplicationHostType(), v
, realPath
) as IApplicationHost
;
641 AppHost
.Server
= server
;
643 // Link the host in the application domain with a request broker in the main domain
644 RequestBroker
= webSource
.CreateRequestBroker ();
645 AppHost
.RequestBroker
= RequestBroker
;
651 static byte [] error500
;
652 static byte [] badRequest
;
656 string s
= "HTTP/1.0 500 Server error\r\n" +
657 "Connection: close\r\n\r\n" +
658 "<html><head><title>500 Server Error</title><body><h1>Server error</h1>\r\n" +
659 "Your client sent a request that was not understood by this server.\r\n" +
660 "</body></html>\r\n";
661 error500
= Encoding
.ASCII
.GetBytes (s
);
663 string br
= "HTTP/1.0 400 Bad Request\r\n" +
664 "Connection: close\r\n\r\n" +
665 "<html><head><title>400 Bad Request</title></head>" +
666 "<body><h1>Bad Request</h1>The request was not understood" +
669 badRequest
= Encoding
.ASCII
.GetBytes (br
);
672 public static byte [] NotFound (string uri
)
674 string s
= String
.Format ("HTTP/1.0 404 Not Found\r\n" +
675 "Connection: close\r\n\r\n" +
676 "<html><head><title>404 Not Found</title></head>\r\n" +
677 "<body><h1>Not Found</h1>The requested URL {0} was not found on this " +
678 "server.<p>\r\n</body></html>\r\n", uri
);
680 return Encoding
.ASCII
.GetBytes (s
);
683 public static byte [] BadRequest ()
688 public static byte [] ServerError ()
699 public static void GetPathsFromUri (string uri
, out string realUri
, out string pathInfo
)
701 // There's a hidden missing feature here... :)
702 realUri
= uri
; pathInfo
= "";
703 string basepath
= HttpRuntime
.AppDomainAppPath
;
704 string vpath
= HttpRuntime
.AppDomainAppVirtualPath
;
705 if (vpath
[vpath
.Length
- 1] != '/')
708 if (vpath
.Length
> uri
.Length
)
711 uri
= uri
.Substring (vpath
.Length
);
712 while (uri
.Length
> 0 && uri
[0] == '/')
713 uri
= uri
.Substring (1);
716 int lastSlash
= uri
.Length
;
717 bool windows
= (Path
.DirectorySeparatorChar
== '\\');
719 for (dot
= uri
.LastIndexOf ('.'); dot
> 0; dot
= uri
.LastIndexOf ('.', dot
- 1)) {
720 slash
= uri
.IndexOf ('/', dot
);
725 partial = uri
.Substring (0, slash
);
727 string partial_win
= null;
729 partial_win
= partial.Replace ('/', '\\');
731 string path
= Path
.Combine (basepath
, (windows
? partial_win
: partial));
732 if (!File
.Exists (path
))
735 realUri
= vpath
+ uri
.Substring (0, slash
);
736 pathInfo
= uri
.Substring (slash
);