1 namespace StackExchange.Profiling.UI
2 {
3     using System;
4     using System.Collections.Concurrent;
5     using System.Collections.Generic;
6     using System.Globalization;
7     using System.IO;
8     using System.Linq;
9     using System.Text;
10     using System.Web;
11     using System.Web.Routing;
12 
13     using StackExchange.Profiling.Helpers;
14 
15     /// <summary>
16     /// Understands how to route and respond to MiniProfiler UI URLS.
17     /// </summary>
18     public class MiniProfilerHandler : IRouteHandler, IHttpHandler
19     {
20         /// <summary>
21         /// Embedded resource contents keyed by filename.
22         /// </summary>
23         private static readonly ConcurrentDictionary<string, string> ResourceCache = new ConcurrentDictionary<string, string>();
24 
25         /// <summary>
26         /// The bypass local load.
27         /// </summary>
28         private static bool bypassLocalLoad = false;
29 
30         /// <summary>
31         /// Gets a value indicating whether to keep things static and reusable.
32         /// </summary>
33         public bool IsReusable
34         {
35             get { return true; }
36         }
37 
38         /// <summary>
39         /// Usually called internally, sometimes you may clear the routes during the apps lifecycle, if you do that call this to bring back mini profiler.
40         /// </summary>
RegisterRoutes()41         public static void RegisterRoutes()
42         {
43             var routes = RouteTable.Routes;
44             var handler = new MiniProfilerHandler();
45             var prefix = MiniProfiler.Settings.RouteBasePath.Replace("~/", string.Empty).EnsureTrailingSlash();
46 
47             using (routes.GetWriteLock())
48             {
49                 var route = new Route(prefix + "{filename}", handler)
50                                 {
51                                     // we have to specify these, so no MVC route helpers will match, e.g. @Html.ActionLink("Home", "Index", "Home")
52                                     Defaults = new RouteValueDictionary(new { controller = "MiniProfilerHandler", action = "ProcessRequest" }),
53                                     Constraints = new RouteValueDictionary(new { controller = "MiniProfilerHandler", action = "ProcessRequest" })
54                                 };
55 
56                 // put our routes at the beginning, like a boss
57                 routes.Insert(0, route);
58             }
59         }
60 
61         /// <summary>
62         /// Returns this <see cref="MiniProfilerHandler"/> to handle <paramref name="requestContext"/>.
63         /// </summary>
64         /// <param name="requestContext">
65         /// The request Context.
66         /// </param>
67         /// <returns>the http handler implementation</returns>
GetHttpHandler(RequestContext requestContext)68         public IHttpHandler GetHttpHandler(RequestContext requestContext)
69         {
70             return this; // elegant? I THINK SO.
71         }
72 
73         /// <summary>
74         /// Returns either includes' <c>css/javascript</c> or results' html.
75         /// </summary>
76         /// <param name="context">The http context.</param>
ProcessRequest(HttpContext context)77         public void ProcessRequest(HttpContext context)
78         {
79             string output;
80             string path = context.Request.AppRelativeCurrentExecutionFilePath;
81 
82             switch (Path.GetFileNameWithoutExtension(path).ToLowerInvariant())
83             {
84                 case "jquery.1.7.1":
85                 case "jquery.tmpl":
86                 case "includes":
87                 case "list":
88                     output = Includes(context, path);
89                     break;
90 
91                 case "results-index":
92                     output = Index(context);
93                     break;
94 
95                 case "results-list":
96                     output = ResultList(context);
97                     break;
98 
99                 case "results":
100                     output = Results(context);
101                     break;
102 
103                 default:
104                     output = NotFound(context);
105                     break;
106             }
107 
108             context.Response.Write(output);
109         }
110 
111         /// <summary>
112         /// Renders script tag found in "include.partial.html" - this is shared with all other language implementations, so if you change it, you MUST
113         /// provide changes for those other implementations, e.g. ruby.
114         /// </summary>
RenderIncludes( MiniProfiler profiler, RenderPosition? position = null, bool? showTrivial = null, bool? showTimeWithChildren = null, int? maxTracesToShow = null, bool? showControls = null, bool? startHidden = null)115         internal static HtmlString RenderIncludes(
116             MiniProfiler profiler,
117             RenderPosition? position = null,
118             bool? showTrivial = null,
119             bool? showTimeWithChildren = null,
120             int? maxTracesToShow = null,
121             bool? showControls = null,
122             bool? startHidden = null)
123         {
124             if (profiler == null) return new HtmlString("");
125 
126             MiniProfiler.Settings.EnsureStorageStrategy();
127             var authorized = MiniProfiler.Settings.Results_Authorize == null || MiniProfiler.Settings.Results_Authorize(HttpContext.Current.Request);
128 
129             // unviewed ids are added to this list during Storage.Save, but we know we haven't see the current one yet, so go ahead and add it to the end
130             var ids = authorized ? MiniProfiler.Settings.Storage.GetUnviewedIds(profiler.User) : new List<Guid>();
131             ids.Add(profiler.Id);
132 
133             var format = GetResource("include.partial.html");
134             var result = format.Format(new
135             {
136                 path = VirtualPathUtility.ToAbsolute(MiniProfiler.Settings.RouteBasePath).EnsureTrailingSlash(),
137                 version = MiniProfiler.Settings.Version,
138                 ids = string.Join(",", ids.Select(guid => guid.ToString())),
139                 position = (position ?? MiniProfiler.Settings.PopupRenderPosition).ToString().ToLower(),
140                 showTrivial = (showTrivial ?? MiniProfiler.Settings.PopupShowTrivial).ToJs(),
141                 showChildren = (showTimeWithChildren ?? MiniProfiler.Settings.PopupShowTimeWithChildren).ToJs(),
142                 maxTracesToShow = maxTracesToShow ?? MiniProfiler.Settings.PopupMaxTracesToShow,
143                 showControls = (showControls ?? MiniProfiler.Settings.ShowControls).ToJs(),
144                 currentId = profiler.Id,
145                 authorized = authorized.ToJs(),
146                 toggleShortcut = MiniProfiler.Settings.PopupToggleKeyboardShortcut,
147                 startHidden = (startHidden ?? MiniProfiler.Settings.PopupStartHidden).ToJs()
148             });
149 
150             return new HtmlString(result);
151         }
152 
153         /// <summary>
154         /// The result list.
155         /// </summary>
156         /// <param name="context">The context.</param>
157         /// <returns>a string containing the result list.</returns>
ResultList(HttpContext context)158         private static string ResultList(HttpContext context)
159         {
160             string message;
161             if (!AuthorizeRequest(context, isList: true, message: out message))
162             {
163                 return message;
164             }
165 
166             var lastId = context.Request["last-id"];
167             Guid lastGuid = Guid.Empty;
168 
169             if (!lastId.IsNullOrWhiteSpace())
170             {
171                 Guid.TryParse(lastId, out lastGuid);
172             }
173 
174             // After app restart, MiniProfiler.Settings.Storage will be null if no results saved, and NullReferenceException is thrown.
175             if (MiniProfiler.Settings.Storage == null)
176             {
177                 MiniProfiler.Settings.EnsureStorageStrategy();
178             }
179 
180             var guids = MiniProfiler.Settings.Storage.List(100);
181 
182             if (lastGuid != Guid.Empty)
183             {
184                 guids = guids.TakeWhile(g => g != lastGuid);
185             }
186 
187             guids = guids.Reverse();
188 
189             return guids.Select(
190                 g =>
191                 {
192                     var profiler = MiniProfiler.Settings.Storage.Load(g);
193                     return
194                         new
195                             {
196                                 profiler.Id,
197                                 profiler.Name,
198                                 profiler.DurationMilliseconds,
199                                 profiler.DurationMillisecondsInSql,
200                                 profiler.ClientTimings,
201                                 profiler.Started,
202                                 profiler.ExecutedNonQueries,
203                                 profiler.ExecutedReaders,
204                                 profiler.ExecutedScalars,
205                                 profiler.HasAllTrivialTimings,
206                                 profiler.HasDuplicateSqlTimings,
207                                 profiler.HasSqlTimings,
208                                 profiler.HasTrivialTimings,
209                                 profiler.HasUserViewed,
210                                 profiler.MachineName,
211                                 profiler.User
212                             };
213                 }).ToJson();
214         }
215 
216         /// <summary>
217         /// the index (Landing) view.
218         /// </summary>
219         /// <param name="context">The context.</param>
220         /// <returns>a string containing the html.</returns>
Index(HttpContext context)221         private static string Index(HttpContext context)
222         {
223             string message;
224             if (!AuthorizeRequest(context, isList: true, message: out message))
225             {
226                 return message;
227             }
228 
229             context.Response.ContentType = "text/html";
230 
231             var path = VirtualPathUtility.ToAbsolute(MiniProfiler.Settings.RouteBasePath).EnsureTrailingSlash();
232             return new StringBuilder()
233                 .AppendLine("<html><head>")
234                 .AppendFormat("<title>List of profiling sessions</title>")
235                 .AppendLine()
236                 .AppendLine("<script type='text/javascript' src='" + path + "jquery.1.7.1.js?v=" + MiniProfiler.Settings.Version + "'></script>")
237                 .AppendLine("<script id='mini-profiler' data-ids='' type='text/javascript' src='" + path + "includes.js?v=" + MiniProfiler.Settings.Version + "'></script>")
238                 .AppendLine("<script type='text/javascript' src='" + path + "jquery.tmpl.js?v=" + MiniProfiler.Settings.Version + "'></script>")
239                 .AppendLine(
240                     "<script type='text/javascript' src='" + path + "list.js?v=" + MiniProfiler.Settings.Version
241                     + "'></script>")
242                 .AppendLine(
243                     "<link href='" + path + "list.css?v=" + MiniProfiler.Settings.Version
244                     + "' rel='stylesheet' type='text/css'>")
245                 .AppendLine(
246                     "<script type='text/javascript'>MiniProfiler.list.init({path: '" + path + "', version: '"
247                     + MiniProfiler.Settings.Version + "'})</script>")
248                 .AppendLine("</head><body></body></html>")
249                 .ToString();
250         }
251 
252         /// <summary>
253         /// Handles rendering static content files.
254         /// </summary>
255         /// <param name="context">The context.</param>
256         /// <param name="path">The path.</param>
257         /// <returns>a string containing the content type.</returns>
Includes(HttpContext context, string path)258         private static string Includes(HttpContext context, string path)
259         {
260             var response = context.Response;
261             switch (Path.GetExtension(path))
262             {
263                 case ".js":
264                     response.ContentType = "application/javascript";
265                     break;
266                 case ".css":
267                     response.ContentType = "text/css";
268                     break;
269                 case ".tmpl":
270                     response.ContentType = "text/x-jquery-tmpl";
271                     break;
272                 default:
273                     return NotFound(context);
274             }
275 #if !DEBUG
276             var cache = response.Cache;
277             cache.SetCacheability(System.Web.HttpCacheability.Public);
278             cache.SetExpires(DateTime.Now.AddDays(7));
279             cache.SetValidUntilExpires(true);
280 #endif
281 
282             var embeddedFile = Path.GetFileName(path);
283             return GetResource(embeddedFile);
284         }
285 
286         /// <summary>
287         /// Handles rendering a previous <c>MiniProfiler</c> session, identified by its <c>"?id=GUID"</c> on the query.
288         /// </summary>
289         /// <param name="context">The context.</param>
290         /// <returns>a string containing the rendered content</returns>
Results(HttpContext context)291         private static string Results(HttpContext context)
292         {
293             // when we're rendering as a button/popup in the corner, we'll pass ?popup=1
294             // if it's absent, we're rendering results as a full page for sharing
295             var isPopup = !string.IsNullOrWhiteSpace(context.Request["popup"]);
296 
297             // this guid is the MiniProfiler.Id property
298             // if this guid is not supplied, the last set of results needs to be
299             // displayed. The home page doesn't have profiling otherwise.
300             Guid id;
301             if (!Guid.TryParse(context.Request["id"], out id))
302                 id = MiniProfiler.Settings.Storage.List(1).FirstOrDefault();
303 
304             if (id == default(Guid))
305                 return isPopup ? NotFound(context) : NotFound(context, "text/plain", "No Guid id specified on the query string");
306 
307             MiniProfiler.Settings.EnsureStorageStrategy();
308             var profiler = MiniProfiler.Settings.Storage.Load(id);
309 
310             var provider = WebRequestProfilerProvider.Settings.UserProvider;
311             string user = null;
312             if (provider != null)
313             {
314                 user = provider.GetUser(context.Request);
315             }
316 
317             MiniProfiler.Settings.Storage.SetViewed(user, id);
318 
319             if (profiler == null)
320             {
321                 return isPopup ? NotFound(context) : NotFound(context, "text/plain", "No MiniProfiler results found with Id=" + id.ToString());
322             }
323 
324             bool needsSave = false;
325             if (profiler.ClientTimings == null)
326             {
327                 profiler.ClientTimings = ClientTimings.FromRequest(context.Request);
328                 if (profiler.ClientTimings != null)
329                 {
330                     needsSave = true;
331                 }
332             }
333 
334             if (profiler.HasUserViewed == false)
335             {
336                 profiler.HasUserViewed = true;
337                 needsSave = true;
338             }
339 
340             if (needsSave) MiniProfiler.Settings.Storage.Save(profiler);
341 
342             var authorize = MiniProfiler.Settings.Results_Authorize;
343 
344             if (authorize != null && !authorize(context.Request))
345             {
346                 context.Response.ContentType = "application/json";
347                 return "hidden".ToJson();
348             }
349 
350             return isPopup ? ResultsJson(context, profiler) : ResultsFullPage(context, profiler);
351         }
352 
353         /// <summary>
354         /// authorize the request.
355         /// </summary>
356         /// <param name="context">The context.</param>
357         /// <param name="isList">is list.</param>
358         /// <param name="message">The message.</param>
359         /// <returns>true if the request is authorised.</returns>
AuthorizeRequest(HttpContext context, bool isList, out string message)360         private static bool AuthorizeRequest(HttpContext context, bool isList, out string message)
361         {
362             message = null;
363             var authorize = MiniProfiler.Settings.Results_Authorize;
364             var authorizeList = MiniProfiler.Settings.Results_List_Authorize;
365 
366             if ((authorize != null && !authorize(context.Request)) || (isList && (authorizeList == null || !authorizeList(context.Request))))
367             {
368                 context.Response.StatusCode = 401;
369                 context.Response.ContentType = "text/plain";
370                 message = "unauthorized";
371                 return false;
372             }
373 
374             return true;
375         }
376 
377         /// <summary>
378         /// set the JSON results and the content type.
379         /// </summary>
380         /// <param name="context">The context.</param>
381         /// <param name="profiler">The profiler.</param>
382         /// <returns>a string containing the JSON results.</returns>
ResultsJson(HttpContext context, MiniProfiler profiler)383         private static string ResultsJson(HttpContext context, MiniProfiler profiler)
384         {
385             context.Response.ContentType = "application/json";
386             return MiniProfiler.ToJson(profiler);
387         }
388 
389         /// <summary>
390         /// results full page.
391         /// </summary>
392         /// <param name="context">The context.</param>
393         /// <param name="profiler">The profiler.</param>
394         /// <returns>a string containing the results page</returns>
ResultsFullPage(HttpContext context, MiniProfiler profiler)395         private static string ResultsFullPage(HttpContext context, MiniProfiler profiler)
396         {
397             context.Response.ContentType = "text/html";
398 
399             var template = GetResource("share.html");
400             return template.Format(new
401             {
402                 name = profiler.Name,
403                 duration = profiler.DurationMilliseconds.ToString(CultureInfo.InvariantCulture),
404                 path = VirtualPathUtility.ToAbsolute(MiniProfiler.Settings.RouteBasePath).EnsureTrailingSlash(),
405                 json = MiniProfiler.ToJson(profiler),
406                 includes = RenderIncludes(profiler),
407                 version = MiniProfiler.Settings.Version
408             });
409         }
410 
411         /// <summary>
412         /// get the resource.
413         /// </summary>
414         /// <param name="filename">The filename.</param>
415         /// <returns>a string containing the resource</returns>
GetResource(string filename)416         private static string GetResource(string filename)
417         {
418             filename = filename.ToLower();
419             string result;
420 
421 #if DEBUG
422             // attempt to simply load from file system, this lets up modify js without needing to recompile A MILLION TIMES
423             if (!bypassLocalLoad)
424             {
425 
426                 var trace = new System.Diagnostics.StackTrace(true);
427                 var path = System.IO.Path.GetDirectoryName(trace.GetFrames()[0].GetFileName()) + "\\..\\UI\\" + filename;
428                 try
429                 {
430                     return File.ReadAllText(path);
431                 }
432                 catch
433                 {
434                     bypassLocalLoad = true;
435                 }
436             }
437 #endif
438 
439             if (!ResourceCache.TryGetValue(filename, out result))
440             {
441                 string customTemplatesPath = HttpContext.Current.Server.MapPath(MiniProfiler.Settings.CustomUITemplates);
442                 string customTemplateFile = Path.Combine(customTemplatesPath, filename);
443 
444                 if (File.Exists(customTemplateFile))
445                 {
446                     result = File.ReadAllText(customTemplateFile);
447                 }
448                 else
449                 {
450                     using (var stream = typeof(MiniProfilerHandler).Assembly.GetManifestResourceStream("StackExchange.Profiling.UI." + filename))
451                     using (var reader = new StreamReader(stream))
452                     {
453                         result = reader.ReadToEnd();
454                     }
455                 }
456 
457                 ResourceCache[filename] = result;
458             }
459 
460             return result;
461         }
462 
463         /// <summary>
464         /// Helper method that sets a proper 404 response code.
465         /// </summary>
466         /// <param name="context">The context.</param>
467         /// <param name="contentType">The content Type.</param>
468         /// <param name="message">The message.</param>
469         /// <returns>a string containing the 'not found' message.</returns>
NotFound(HttpContext context, string contentType = R, string message = null)470         private static string NotFound(HttpContext context, string contentType = "text/plain", string message = null)
471         {
472             context.Response.StatusCode = 404;
473             context.Response.ContentType = contentType;
474 
475             return message;
476         }
477 
478     }
479 }
480