1 | // -----------------------------------------------------------------------
|
---|
2 | // <copyright file="FileRequestHandler.cs" company="Microsoft">
|
---|
3 | // Copyright (c) Microsoft Corporation. All rights reserved.
|
---|
4 | // </copyright>
|
---|
5 | // -----------------------------------------------------------------------
|
---|
6 |
|
---|
7 | namespace Microsoft.Samples.Kinect.Webserver
|
---|
8 | {
|
---|
9 | using System;
|
---|
10 | using System.Diagnostics;
|
---|
11 | using System.IO;
|
---|
12 | using System.Net;
|
---|
13 | using System.Threading.Tasks;
|
---|
14 | using System.Web;
|
---|
15 |
|
---|
16 | /// <summary>
|
---|
17 | /// Implementation of IHttpRequestHandler used to serve static file content.
|
---|
18 | /// </summary>
|
---|
19 | public class FileRequestHandler : IHttpRequestHandler
|
---|
20 | {
|
---|
21 | /// <summary>
|
---|
22 | /// Root directory in server's file system from which we're serving files.
|
---|
23 | /// </summary>
|
---|
24 | private readonly DirectoryInfo rootDirectory;
|
---|
25 |
|
---|
26 | /// <summary>
|
---|
27 | /// Origin used as a helper for URI parsing and canonicalization.
|
---|
28 | /// </summary>
|
---|
29 | private readonly Uri parseHelperOrigin = new Uri("http://a");
|
---|
30 |
|
---|
31 | /// <summary>
|
---|
32 | /// Initializes a new instance of the <see cref="FileRequestHandler"/> class.
|
---|
33 | /// </summary>
|
---|
34 | /// <param name="rootDirectory">
|
---|
35 | /// Root directory in server's file system from which files should be served.
|
---|
36 | /// </param>
|
---|
37 | internal FileRequestHandler(DirectoryInfo rootDirectory)
|
---|
38 | {
|
---|
39 | if (rootDirectory == null)
|
---|
40 | {
|
---|
41 | throw new ArgumentNullException("rootDirectory");
|
---|
42 | }
|
---|
43 |
|
---|
44 | // Re-create directory name to ensure equivalence between directory names
|
---|
45 | // that end in "\" character and directory names that are identical except
|
---|
46 | // that they don't end in "\" character.
|
---|
47 | var normalizedDirectoryName = rootDirectory.Parent != null
|
---|
48 | ? Path.Combine(rootDirectory.Parent.FullName, rootDirectory.Name)
|
---|
49 | : rootDirectory.Name;
|
---|
50 | this.rootDirectory = new DirectoryInfo(normalizedDirectoryName);
|
---|
51 | }
|
---|
52 |
|
---|
53 | /// <summary>
|
---|
54 | /// Prepares handler to start receiving HTTP requests.
|
---|
55 | /// </summary>
|
---|
56 | /// <returns>
|
---|
57 | /// Await-able task.
|
---|
58 | /// </returns>
|
---|
59 | /// <remarks>
|
---|
60 | /// Return value should never be null. Implementations should use Task.FromResult(0)
|
---|
61 | /// if function is implemented synchronously so that callers can await without
|
---|
62 | /// needing to check for null.
|
---|
63 | /// </remarks>
|
---|
64 | public Task InitializeAsync()
|
---|
65 | {
|
---|
66 | return SharedConstants.EmptyCompletedTask;
|
---|
67 | }
|
---|
68 |
|
---|
69 | /// <summary>
|
---|
70 | /// Handle an http request.
|
---|
71 | /// </summary>
|
---|
72 | /// <param name="requestContext">
|
---|
73 | /// Context containing HTTP request data, which will also contain associated
|
---|
74 | /// response upon return.
|
---|
75 | /// </param>
|
---|
76 | /// <param name="subpath">
|
---|
77 | /// Request URI path relative to the URI prefix associated with this request
|
---|
78 | /// handler in the HttpListener.
|
---|
79 | /// </param>
|
---|
80 | /// <returns>
|
---|
81 | /// Await-able task.
|
---|
82 | /// </returns>
|
---|
83 | /// <remarks>
|
---|
84 | /// Return value should never be null. Implementations should use Task.FromResult(0)
|
---|
85 | /// if function is implemented synchronously so that callers can await without
|
---|
86 | /// needing to check for null.
|
---|
87 | /// </remarks>
|
---|
88 | public async Task HandleRequestAsync(HttpListenerContext requestContext, string subpath)
|
---|
89 | {
|
---|
90 | if (requestContext == null)
|
---|
91 | {
|
---|
92 | throw new ArgumentNullException("requestContext");
|
---|
93 | }
|
---|
94 |
|
---|
95 | if (subpath == null)
|
---|
96 | {
|
---|
97 | throw new ArgumentNullException("subpath");
|
---|
98 | }
|
---|
99 |
|
---|
100 | try
|
---|
101 | {
|
---|
102 | var statusCode = await this.ServeFileAsync(requestContext.Response, subpath);
|
---|
103 | CloseResponse(requestContext, statusCode);
|
---|
104 | }
|
---|
105 | catch (HttpListenerException e)
|
---|
106 | {
|
---|
107 | Trace.TraceWarning(
|
---|
108 | "Problem encountered while serving file at subpath {0}. Client might have aborted request. Cause: \"{1}\"", subpath, e.Message);
|
---|
109 | }
|
---|
110 | }
|
---|
111 |
|
---|
112 | /// <summary>
|
---|
113 | /// Cancel all pending operations.
|
---|
114 | /// </summary>
|
---|
115 | public void Cancel()
|
---|
116 | {
|
---|
117 | }
|
---|
118 |
|
---|
119 | /// <summary>
|
---|
120 | /// Lets handler know that no more HTTP requests will be received, so that it can
|
---|
121 | /// clean up resources associated with request handling.
|
---|
122 | /// </summary>
|
---|
123 | /// <returns>
|
---|
124 | /// Await-able task.
|
---|
125 | /// </returns>
|
---|
126 | /// <remarks>
|
---|
127 | /// Return value should never be null. Implementations should use Task.FromResult(0)
|
---|
128 | /// if function is implemented synchronously so that callers can await without
|
---|
129 | /// needing to check for null.
|
---|
130 | /// </remarks>
|
---|
131 | public Task UninitializeAsync()
|
---|
132 | {
|
---|
133 | return SharedConstants.EmptyCompletedTask;
|
---|
134 | }
|
---|
135 |
|
---|
136 | /// <summary>
|
---|
137 | /// Close response stream and associate a status code with response.
|
---|
138 | /// </summary>
|
---|
139 | /// <param name="context">
|
---|
140 | /// Context whose response we should close.
|
---|
141 | /// </param>
|
---|
142 | /// <param name="statusCode">
|
---|
143 | /// Status code.
|
---|
144 | /// </param>
|
---|
145 | internal static void CloseResponse(HttpListenerContext context, HttpStatusCode statusCode)
|
---|
146 | {
|
---|
147 | context.Response.StatusCode = (int)statusCode;
|
---|
148 | context.Response.Close();
|
---|
149 | }
|
---|
150 |
|
---|
151 | /// <summary>
|
---|
152 | /// Determine if the specified directory is an ancestor of the specified file.
|
---|
153 | /// </summary>
|
---|
154 | /// <param name="ancestorDirectory">
|
---|
155 | /// Information for potential ancestor directory.
|
---|
156 | /// </param>
|
---|
157 | /// <param name="file">
|
---|
158 | /// File for which to determine directory ancestry.
|
---|
159 | /// </param>
|
---|
160 | /// <returns>
|
---|
161 | /// True if <paramref name="ancestorDirectory"/> is an ancestor of <paramref name="file"/>.
|
---|
162 | /// </returns>
|
---|
163 | internal static bool IsAncestorDirectory(DirectoryInfo ancestorDirectory, FileInfo file)
|
---|
164 | {
|
---|
165 | var parentDirectory = file.Directory;
|
---|
166 |
|
---|
167 | while (parentDirectory != null)
|
---|
168 | {
|
---|
169 | if (string.Compare(parentDirectory.FullName, ancestorDirectory.FullName, StringComparison.OrdinalIgnoreCase) == 0)
|
---|
170 | {
|
---|
171 | return true;
|
---|
172 | }
|
---|
173 |
|
---|
174 | parentDirectory = parentDirectory.Parent;
|
---|
175 | }
|
---|
176 |
|
---|
177 | return false;
|
---|
178 | }
|
---|
179 |
|
---|
180 | /// <summary>
|
---|
181 | /// Serve the file corresponding to the specified URI path.
|
---|
182 | /// </summary>
|
---|
183 | /// <param name="response">
|
---|
184 | /// HTTP response where file will be streamed, on success.
|
---|
185 | /// </param>
|
---|
186 | /// <param name="subpath">
|
---|
187 | /// Request URI path relative to the URI prefix associated with this request
|
---|
188 | /// handler in the HttpListener.
|
---|
189 | /// </param>
|
---|
190 | /// <returns>
|
---|
191 | /// Await-able task holding an HTTP status code that should be part of response.
|
---|
192 | /// </returns>
|
---|
193 | private async Task<HttpStatusCode> ServeFileAsync(HttpListenerResponse response, string subpath)
|
---|
194 | {
|
---|
195 | Uri uri;
|
---|
196 |
|
---|
197 | try
|
---|
198 | {
|
---|
199 | uri = new Uri(this.parseHelperOrigin, subpath);
|
---|
200 | }
|
---|
201 | catch (UriFormatException)
|
---|
202 | {
|
---|
203 | return HttpStatusCode.Forbidden;
|
---|
204 | }
|
---|
205 |
|
---|
206 | var filePath = Path.Combine(this.rootDirectory.FullName, uri.AbsolutePath.Substring(1));
|
---|
207 | var fileInfo = new FileInfo(filePath);
|
---|
208 |
|
---|
209 | if (!IsAncestorDirectory(this.rootDirectory, fileInfo))
|
---|
210 | {
|
---|
211 | return HttpStatusCode.Forbidden;
|
---|
212 | }
|
---|
213 |
|
---|
214 | if (!fileInfo.Exists)
|
---|
215 | {
|
---|
216 | return HttpStatusCode.NotFound;
|
---|
217 | }
|
---|
218 |
|
---|
219 | response.ContentType = MimeMapping.GetMimeMapping(fileInfo.FullName);
|
---|
220 |
|
---|
221 | using (var fileStream = fileInfo.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
|
---|
222 | {
|
---|
223 | await fileStream.CopyToAsync(response.OutputStream);
|
---|
224 | }
|
---|
225 |
|
---|
226 | return HttpStatusCode.OK;
|
---|
227 | }
|
---|
228 | }
|
---|
229 | }
|
---|