PageRenderTime 46ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/Hosts/Silverlight/Chiron/HttpServer.cs

http://github.com/IronLanguages/main
C# | 436 lines | 308 code | 62 blank | 66 comment | 78 complexity | 52504819932a3efbcc4eaba60d048b78 MD5 | raw file
Possible License(s): CPL-1.0, BSD-3-Clause, ISC, GPL-2.0, MPL-2.0-no-copyleft-exception
  1. /* ****************************************************************************
  2. *
  3. * Copyright (c) Microsoft Corporation.
  4. *
  5. * This source code is subject to terms and conditions of the Apache License, Version 2.0. A
  6. * copy of the license can be found in the License.html file at the root of this distribution. If
  7. * you cannot locate the Apache License, Version 2.0, please send an email to
  8. * dlr@microsoft.com. By using this source code in any fashion, you are agreeing to be bound
  9. * by the terms of the Apache License, Version 2.0.
  10. *
  11. * You must not remove this notice, or any other, from this software.
  12. *
  13. *
  14. * ***************************************************************************/
  15. using System;
  16. using System.Collections.Generic;
  17. using System.Globalization;
  18. using System.IO;
  19. using System.Net;
  20. using System.Net.Sockets;
  21. using System.Text;
  22. using System.Threading;
  23. namespace Chiron {
  24. class HttpServer {
  25. int _port;
  26. string _dir;
  27. bool _shutdown;
  28. Socket _socketIPv6;
  29. Socket _socketIPv4;
  30. public HttpServer(int port, string dir) {
  31. _port = port;
  32. _dir = dir;
  33. if (_dir[_dir.Length - 1] != Path.DirectorySeparatorChar)
  34. _dir += Path.DirectorySeparatorChar;
  35. }
  36. private Socket Connect(AddressFamily family, IPAddress address) {
  37. Socket socket = new Socket(family, SocketType.Stream, ProtocolType.Tcp);
  38. socket.Bind(new IPEndPoint(address, _port));
  39. socket.Listen(0x1000);
  40. return socket;
  41. }
  42. public void Start() {
  43. // Listen to IPv6 and IPv4 at the same time.
  44. // (Otherwise Firefox is very slow because it will try IPv6 first for
  45. // every single download & time out)
  46. bool ipv6Works = false;
  47. var ipAddress = Chiron.AnyAddress ? IPAddress.Any : IPAddress.Loopback;
  48. var ipv6Address = Chiron.AnyAddress ? IPAddress.IPv6Any : IPAddress.IPv6Loopback;
  49. try {
  50. _socketIPv6 = Connect(AddressFamily.InterNetworkV6, ipv6Address);
  51. Accept(_socketIPv6);
  52. ipv6Works = true;
  53. }
  54. catch { }
  55. try {
  56. _socketIPv4 = Connect(AddressFamily.InterNetwork, ipAddress);
  57. Accept(_socketIPv4);
  58. }
  59. catch {
  60. if (!ipv6Works)
  61. throw;
  62. }
  63. }
  64. public void Stop() {
  65. _shutdown = true;
  66. try {
  67. if (_socketIPv4 != null) _socketIPv4.Close();
  68. if (_socketIPv6 != null) _socketIPv6.Close();
  69. }
  70. catch {} finally {
  71. _socketIPv4 = null;
  72. _socketIPv6 = null;
  73. }
  74. }
  75. public bool IsRunning {
  76. get {
  77. return (_socketIPv4 != null || _socketIPv6 != null);
  78. }
  79. }
  80. void Accept(Socket mySocket) {
  81. ThreadPool.QueueUserWorkItem(delegate {
  82. while (!_shutdown) {
  83. try {
  84. Socket socket = mySocket.Accept();
  85. ThreadPool.QueueUserWorkItem(delegate {
  86. if (!_shutdown)
  87. ProcessRequest(new HttpSocket(socket));
  88. }, socket);
  89. } catch {
  90. Thread.Sleep(100);
  91. }
  92. }
  93. });
  94. }
  95. void ProcessRequest(HttpSocket s) {
  96. HttpRequestData r = null;
  97. string path = null;
  98. // reply to unreadable requests
  99. if (!s.TryReadRequest(out r)) {
  100. s.WriteErrorResponse(400, "Unparsable bad request");
  101. }
  102. // deny non-GET requests
  103. else if (r.Method != "GET") {
  104. s.WriteErrorResponse(405, "Method other than GET");
  105. }
  106. // process special commands
  107. else if (TryProcessSpecialCommand(s, r.Uri)) {
  108. // done
  109. }
  110. // deny requests that cannot be mapped to disk
  111. else if (!TryMapUri(r.Uri, out path)) {
  112. s.WriteErrorResponse(404, "URI cannot be mapped to disk");
  113. }
  114. // process file requests
  115. else if (TryProcessFileRequest(s, r.Uri, path)) {
  116. // done
  117. }
  118. // process directory requests
  119. else if (TryProcessDirectoryRequest(s, r.Uri, path)) {
  120. // done
  121. }
  122. // process XAP requests
  123. else if (TryProcessXapRequest(s, path)) {
  124. // done
  125. }
  126. // process XAP listing requests
  127. else if (TryProcessXapListingRequest(s, r.Uri, path)) {
  128. // done
  129. }
  130. // process requests for files contained in Chiron's localAssemblyPath
  131. else if (TryProcessBuildRequest(s, r.Uri)) {
  132. // done
  133. }
  134. else {
  135. // not found
  136. s.WriteErrorResponse(404, "Resource not found");
  137. }
  138. Chiron.Log(s.StatusCode, (r != null && r.Uri != null ? r.Uri : "[unknown]"), s.BytesSent, s.Message);
  139. }
  140. bool TryMapUri(string uri, out string path) {
  141. path = null;
  142. // strip query string
  143. int i = uri.IndexOf('?');
  144. if (i > 0) uri = uri.Substring(0, i);
  145. // check for special cases
  146. if (string.IsNullOrEmpty(uri) || uri[0] != '/' || uri.IndexOf("..") >= 0 || uri.IndexOf('\\') >= 0)
  147. return false;
  148. if (uri == "/") { path = _dir; return true; }
  149. // decode %XX
  150. if (uri.IndexOf('%') >= 0) {
  151. uri = DecodeUri(uri);
  152. if (uri.IndexOf('%') >= 0) return false;
  153. }
  154. // combine path and validate
  155. try {
  156. string p1 = Path.Combine(_dir, uri.Substring(1)).Replace('/', Path.DirectorySeparatorChar);
  157. string p2 = Path.GetFullPath(p1);
  158. // normalization check
  159. if (string.Compare(p1, p2, StringComparison.OrdinalIgnoreCase) != 0) return false;
  160. path = p2;
  161. return true;
  162. }
  163. catch {
  164. return false;
  165. }
  166. }
  167. bool TryProcessSpecialCommand(HttpSocket s, string uri) {
  168. uri = uri.ToLowerInvariant();
  169. switch (uri) {
  170. case "/bye!":
  171. s.WriteTextResponse(200, "plain", ":(", false);
  172. ThreadPool.QueueUserWorkItem(delegate { Thread.Sleep(100); Stop(); });
  173. return true;
  174. case "/ping!":
  175. s.WriteTextResponse(200, "plain", ":)", false);
  176. return true;
  177. case "/sl.png!":
  178. s.WriteBinaryResponse(200, "image/png", GetResourceBytes("sl.png"), false);
  179. return true;
  180. case "/slx.png!":
  181. s.WriteBinaryResponse(200, "image/png", GetResourceBytes("slx.png"), false);
  182. return true;
  183. case "/style.css!":
  184. s.WriteTextResponse(200, "css", HtmlFormatter.Style, false);
  185. return true;
  186. default:
  187. return false;
  188. }
  189. }
  190. bool TryProcessFileRequest(HttpSocket s, string uri, string path) {
  191. // path shouldn't end with '\' (that's for XAP listing)
  192. if (path.EndsWith(Path.DirectorySeparatorChar.ToString())) return false;
  193. // file must exist
  194. if (!File.Exists(path)) return false;
  195. // check extension
  196. string mimeType = HttpSocket.GetMimeType(path);
  197. if (string.IsNullOrEmpty(mimeType)) {
  198. s.WriteErrorResponse(403);
  199. return true;
  200. }
  201. // read the file
  202. byte[] body = null;
  203. try {
  204. body = File.ReadAllBytes(path);
  205. }
  206. catch (Exception ex) {
  207. s.WriteErrorResponse(500, ex.Message + "\r\n" + ex.StackTrace);
  208. return true;
  209. }
  210. // write the response
  211. s.WriteResponse(200, string.Format("Content-type: {0}\r\n", mimeType), body, false);
  212. return true;
  213. }
  214. bool TryProcessDirectoryRequest(HttpSocket s, string uri, string path) {
  215. if (!Directory.Exists(path)) return false;
  216. string q = string.Empty;
  217. int i = uri.IndexOf('?');
  218. if (i > 0) { q = uri.Substring(i); uri = uri.Substring(0, i); }
  219. // redirect to trailing '/' to fix-up relative links
  220. if (!uri.EndsWith("/")) {
  221. string newUri = uri + "/" + q;
  222. s.WriteResponse(302,
  223. string.Format("Content-Type: text/html; charset=utf-8\r\nLocation: {0}\r\n", newUri),
  224. Encoding.UTF8.GetBytes(
  225. string.Format(@"<html><head><title>Object moved</title></head><body>
  226. <h2>Object moved to <a href=""{0}"">here</a>.</h2></body></html>", newUri)),
  227. false);
  228. return true;
  229. }
  230. // get all files and subdirs
  231. FileSystemInfo[] infos;
  232. try {
  233. infos = new DirectoryInfo(path).GetFileSystemInfos();
  234. }
  235. catch {
  236. infos = new FileSystemInfo[0];
  237. }
  238. // decode %XX
  239. uri = DecodeUri(uri);
  240. // determine if parent is appropriate
  241. string parent = null;
  242. if (uri.Length > 1) {
  243. i = uri.LastIndexOf('/', uri.Length-2);
  244. parent = (i > 0) ? uri.Substring(0, i) : "/";
  245. }
  246. // write the response
  247. s.WriteTextResponse(200, "html", HtmlFormatter.FormatDirectoryListing(uri, parent, infos), false);
  248. return true;
  249. }
  250. bool TryProcessXapRequest(HttpSocket s, string path) {
  251. // must end with XAP
  252. if (string.Compare(Path.GetExtension(path), ".xap", StringComparison.OrdinalIgnoreCase) != 0)
  253. return false;
  254. // XAP already there?
  255. if (File.Exists(path))
  256. return false;
  257. // Directory must be present
  258. string dir = Path.GetDirectoryName(path) + Path.DirectorySeparatorChar + Path.GetFileNameWithoutExtension(path);
  259. if (!Directory.Exists(dir))
  260. return false;
  261. byte[] xapBytes = null;
  262. try {
  263. xapBytes = XapBuilder.XapToMemory(dir);
  264. }
  265. catch (Exception e) {
  266. s.WriteErrorResponse(500, "error generating XAP: " + e.Message);
  267. return true;
  268. }
  269. s.WriteBinaryResponse(200, "application/x-zip-compressed", xapBytes, false);
  270. return true;
  271. }
  272. bool TryProcessXapListingRequest(HttpSocket s, string uri, string path) {
  273. // path should end with '\' (for XAP listing)
  274. if (!path.EndsWith(Path.DirectorySeparatorChar.ToString())) return false;
  275. path = path.Substring(0, path.Length - 1);
  276. // must end with XAP
  277. if (string.Compare(Path.GetExtension(path), ".xap", StringComparison.OrdinalIgnoreCase) != 0)
  278. return false;
  279. // file must exist
  280. if (!File.Exists(path)) return false;
  281. // see if need to serve file from XAP
  282. string filename = null;
  283. int iq = uri.IndexOf('?');
  284. if (iq >= 0) filename = uri.Substring(iq + 1);
  285. ZipArchive xap = null;
  286. try {
  287. // open XAP file
  288. xap = new ZipArchive(path, FileAccess.Read);
  289. if (string.IsNullOrEmpty(filename)) {
  290. // list contents
  291. List<ZipArchiveFile> xapContents = new List<ZipArchiveFile>();
  292. foreach (KeyValuePair<string, ZipArchiveFile> p in xap.entries) xapContents.Add(p.Value);
  293. s.WriteTextResponse(200, "html", HtmlFormatter.FormatXapListing(uri, xapContents), false);
  294. return true;
  295. }
  296. // server file from XAP
  297. ZipArchiveFile f = null;
  298. if (!xap.entries.TryGetValue(filename, out f)) {
  299. s.WriteErrorResponse(404, "Resource not found in XAP");
  300. return true;
  301. }
  302. // check mime type
  303. string mimeType = HttpSocket.GetMimeType(filename);
  304. if (string.IsNullOrEmpty(mimeType)) {
  305. s.WriteErrorResponse(403);
  306. return true;
  307. }
  308. // get the content
  309. byte[] body = new byte[(int)f.Length];
  310. if (body.Length > 0) {
  311. using (Stream fs = f.OpenRead()) {
  312. fs.Read(body, 0, body.Length);
  313. }
  314. }
  315. // write the resposne
  316. s.WriteResponse(200, string.Format("Content-type: {0}\r\n", mimeType), body, false);
  317. return true;
  318. }
  319. catch {
  320. s.WriteErrorResponse(500, "error reading XAP");
  321. return true;
  322. }
  323. finally {
  324. if (xap != null) xap.Close();
  325. }
  326. }
  327. bool TryProcessBuildRequest(HttpSocket s, string uri) {
  328. if (Chiron.UrlPrefix == "")
  329. return false;
  330. int slash = uri.LastIndexOf('/');
  331. if (slash == -1)
  332. return false;
  333. // must start with URL prefix
  334. if (string.Compare(uri.Substring(0, slash + 1), Chiron.UrlPrefix, StringComparison.OrdinalIgnoreCase) != 0)
  335. return false;
  336. uri = uri.Substring(slash + 1);
  337. // get mime type
  338. string mimeType = HttpSocket.GetMimeType(uri);
  339. if (string.IsNullOrEmpty(mimeType)) {
  340. s.WriteErrorResponse(403);
  341. return true;
  342. }
  343. // see if the file exists in the assembly reference path
  344. string path = Chiron.TryGetAssemblyPath(uri);
  345. if (path == null)
  346. return false;
  347. // read the file
  348. byte[] body = null;
  349. try {
  350. body = File.ReadAllBytes(path);
  351. } catch (Exception ex) {
  352. s.WriteErrorResponse(500, ex.Message + "\r\n" + ex.StackTrace);
  353. return true;
  354. }
  355. // write the response
  356. s.WriteResponse(200, string.Format("Content-type: {0}\r\n", mimeType), body, false);
  357. return true;
  358. }
  359. internal static byte[] GetResourceBytes(string name) {
  360. Stream s = typeof(Chiron).Assembly.GetManifestResourceStream("Chiron." + name);
  361. byte[] b = new byte[(int)s.Length];
  362. s.Read(b, 0, b.Length);
  363. return b;
  364. }
  365. static string DecodeUri(string uri) {
  366. try {
  367. return new Uri("http://localhost" + uri).LocalPath;
  368. }
  369. catch {
  370. return uri;
  371. }
  372. }
  373. }
  374. }