PageRenderTime 61ms CodeModel.GetById 32ms RepoModel.GetById 1ms app.codeStats 0ms

/OpenIdProvider/Controllers/ControllerBase.cs

https://bitbucket.org/sparktree/stackexchange.stackid
C# | 359 lines | 206 code | 55 blank | 98 comment | 30 complexity | c1c24c6da47b0f457193e47ba06091d0 MD5 | raw file
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Web;
  5. using System.Web.Mvc;
  6. using OpenIdProvider.Helpers;
  7. using OpenIdProvider;
  8. using System.IO.Compression;
  9. using MvcMiniProfiler;
  10. namespace OpenIdProvider.Controllers
  11. {
  12. // During dev, we've got a self signed cert somewhere and can just delegate all SSL stuff to
  13. [ValidateInput(false)]
  14. public class ControllerBase : Controller
  15. {
  16. protected override void OnActionExecuting(ActionExecutingContext filterContext)
  17. {
  18. using (MiniProfiler.Current.Step("OnActionExecuting"))
  19. {
  20. Current.Controller = filterContext.Controller;
  21. var path = filterContext.HttpContext.Request.Url.AbsolutePath.ToLowerInvariant();
  22. // Sometimes we know that we want to reject a request, but also want to show the user
  23. // something when it happens, so we check here.
  24. // Special exception for /openid/provider, since its a "special" route that dodges all our Route magic
  25. if (path != "/openid/provider" && path != "/affiliate/form/switch" && Current.RejectRequest)
  26. {
  27. filterContext.Result = NotFound();
  28. return;
  29. }
  30. if (IPBanner.IsBanned(Current.RemoteIP))
  31. {
  32. filterContext.Result = Banned();
  33. return;
  34. }
  35. // On prod, we can either be running with IIS handling SSL, *or* behind an SSL accelerator
  36. // If we're not getting direct SSL, check against the a trusted port we've locked down
  37. // for discussion between the accelerator(s) and the web tier
  38. #if !DEBUG
  39. if (!filterContext.HttpContext.Request.IsSecureConnection)
  40. {
  41. var serverVars = filterContext.HttpContext.Request.ServerVariables;
  42. var originatingIP = serverVars["REMOTE_ADDR"];
  43. var forwardedProto = filterContext.HttpContext.Request.Headers["X-Forwarded-Proto"];
  44. if (forwardedProto != "https" || !Current.LoadBalancerIPs.Contains(originatingIP))
  45. {
  46. Current.LogException(new Exception("Warning! Something is talking to the OpenIdProvider nefariously."));
  47. filterContext.Result = GenericSecurityError();
  48. return;
  49. }
  50. }
  51. #endif
  52. // Handle Acccept-Encoding
  53. // As a site note: why, in the year 2011 (offically "the future") do we have to opt into this stuff?
  54. var acceptEncoding = filterContext.HttpContext.Request.Headers["Accept-Encoding"];
  55. if (acceptEncoding.HasValue())
  56. {
  57. acceptEncoding = acceptEncoding.ToLowerInvariant();
  58. var response = filterContext.HttpContext.Response;
  59. if (acceptEncoding.Contains("gzip"))
  60. {
  61. response.AppendHeader("Content-Encoding", "gzip");
  62. response.Filter = new GZipStream(response.Filter, CompressionMode.Compress);
  63. }
  64. else
  65. {
  66. if (acceptEncoding.Contains("deflate"))
  67. {
  68. response.AppendHeader("Content-Encoding", "deflate");
  69. response.Filter = new DeflateStream(response.Filter, CompressionMode.Compress);
  70. }
  71. }
  72. }
  73. base.OnActionExecuting(filterContext);
  74. }
  75. }
  76. protected override void OnResultExecuting(ResultExecutingContext filterContext)
  77. {
  78. using (MiniProfiler.Current.Step("OnResultExecuting"))
  79. {
  80. // An extra layer of defense against embedding frames in unauthorized domains
  81. // We still need the javascript frame busting since older browsers (IE7 and FF3.5) don't
  82. // honor this header.
  83. // See: https://developer.mozilla.org/en/the_x-frame-options_response_header
  84. if (Current.ShouldBustFrames)
  85. {
  86. filterContext.HttpContext.Response.Headers.Add("X-Frame-Options", "DENY");
  87. }
  88. // Generic "try that again" infrastrcture to shove previously seen values back into forms.
  89. if (filterContext.HttpContext.Request.QueryString.AllKeys.Contains("recover") && filterContext.HttpContext.Request.HttpMethod == "GET")
  90. {
  91. var recoverKey = filterContext.HttpContext.Request.QueryString["recover"];
  92. if (recoverKey.HasValue())
  93. {
  94. var recover = Current.GetFromCache<Dictionary<string, string>>(recoverKey);
  95. if (recover != null)
  96. {
  97. foreach (var key in recover.Keys)
  98. {
  99. ViewData[key] = recover[key];
  100. }
  101. Current.RemoveFromCache(recoverKey);
  102. }
  103. }
  104. }
  105. // Advertise the xrds location
  106. filterContext.HttpContext.Response.Headers.Add(
  107. "X-XRDS-Location",
  108. new Uri(Current.AppRootUri, filterContext.HttpContext.Response.ApplyAppPathModifier("~/xrds")).ToString()
  109. );
  110. if (Current.NoCache)
  111. {
  112. // http://stackoverflow.com/questions/49547/making-sure-a-web-page-is-not-cached-across-all-browsers
  113. var response = filterContext.HttpContext.Response;
  114. response.AppendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1.
  115. response.AppendHeader("Pragma", "no-cache"); // HTTP 1.0.
  116. response.AppendHeader("Expires", "0"); // Proxies.
  117. }
  118. base.OnResultExecuting(filterContext);
  119. }
  120. }
  121. public ActionResult PostExpectedAndNotReceived()
  122. {
  123. return View("PostExpectedAndNotReceived");
  124. }
  125. /// <summary>
  126. /// A curt "you've been banned" message.
  127. /// </summary>
  128. public ActionResult Banned()
  129. {
  130. Response.StatusCode = (int)System.Net.HttpStatusCode.Forbidden;
  131. return TextPlain("This IP address has been banned from making further requests. If you believe this to be in error, contact us.");
  132. }
  133. /// <summary>
  134. /// Common Not Found
  135. /// </summary>
  136. public ActionResult NotFound()
  137. {
  138. Response.StatusCode = (int)System.Net.HttpStatusCode.NotFound;
  139. return View("NotFound");
  140. }
  141. /// <summary>
  142. /// text/plain result with the given content.
  143. /// </summary>
  144. public ActionResult TextPlain(string text)
  145. {
  146. return
  147. new ContentResult
  148. {
  149. Content = text,
  150. ContentType = "text/plain"
  151. };
  152. }
  153. /// <summary>
  154. /// Displays a whole-page error with the given title and message.
  155. ///
  156. /// Use when whatever we encountered needs to be communicated to the user, but we can't expect them to be able
  157. /// to take any action to recover from it.
  158. ///
  159. /// Example: whenever a user lands a page from a link in an email. If something goes wrong with validation, they can't
  160. /// modify the request (since it has an auth code) nor can they go "back" and fix any fields since there is no back to go to.
  161. ///
  162. /// This method centralizes reporting these kinds of errors, to make changes to the underlying view easier.
  163. /// </summary>
  164. public ActionResult IrrecoverableError(string title, string message)
  165. {
  166. ViewData["title"] = title;
  167. ViewData["message"] = message;
  168. return View("IrrecoverableError");
  169. }
  170. /// <summary>
  171. /// Identical to IrrecoverableError, but shows the user some guidance since the problem is suspected to be on
  172. /// their end.
  173. ///
  174. /// Advises them to try
  175. /// </summary>
  176. public ActionResult IrrecoverableErrorWithHelp(string title, string message)
  177. {
  178. ViewData["title"] = title;
  179. ViewData["message"] = message;
  180. return View("IrrecoverableErrorWithHelp");
  181. }
  182. /// <summary>
  183. /// Common "what happened here?" result, displays message to the user.
  184. /// </summary>
  185. public ActionResult UnexpectedState(string message)
  186. {
  187. Current.LogException(new Exception(message));
  188. return IrrecoverableError("Unexpected Situation", message);
  189. }
  190. /// <summary>
  191. /// Common "something is fishy, bail" security error
  192. /// </summary>
  193. public ActionResult GenericSecurityError()
  194. {
  195. return IrrecoverableError("Authentication Failure", "It appears that the security of this request has been tampered with.");
  196. }
  197. /// <summary>
  198. /// If called from a "/submit" route, redirects to the *preceeding* route with the given message and the passed fields restored.
  199. ///
  200. /// If called from any other route type, this throws an exception.
  201. ///
  202. /// We fill values to be rendered into ViewData (after clearing it, for security purposes) to accomplish this.
  203. /// Anything that also appears in the target method signature will be set as a query parameter as well.
  204. ///
  205. /// So, if you call this from "/login/submit" with passBack = { username = "blah" } it will render "/login" with ViewData["username"] = "blah".
  206. /// message is stashed into ViewData["error_message"].
  207. ///
  208. /// Note that this method does not work from route with parameters in the path.
  209. /// </summary>
  210. public ActionResult RecoverableError(string message, object passBack)
  211. {
  212. const string submit = "/submit";
  213. var request = Current.RequestUri.AbsolutePath.ToLower();
  214. if (!request.EndsWith(submit)) throw new InvalidOperationException("Cannot recover from an error if a route isn't handling a POST");
  215. var previousRoute = request.Substring(0, request.Length - submit.Length);
  216. var trueRandom = Current.UniqueId().ToString();
  217. var toStore = passBack.PropertiesAsStrings();
  218. toStore["error_message"] = message;
  219. Current.AddToCache(trueRandom, toStore, TimeSpan.FromMinutes(5));
  220. var queryString = "?recover=" + trueRandom;
  221. var route = RouteAttribute.GetDecoratedRoutes()[previousRoute.Substring(1)];
  222. foreach (var param in route.GetParameters())
  223. {
  224. if (toStore.ContainsKey(param.Name))
  225. {
  226. queryString += "&" + param.Name + "=" + HttpUtility.UrlEncode(toStore[param.Name]);
  227. }
  228. }
  229. return Redirect(previousRoute + queryString);
  230. }
  231. /// <summary>
  232. /// Show the user some generic success message. Meant to centralize all sorts of
  233. /// "great, now you're done!" messages into a single code path.
  234. ///
  235. /// Example usages:
  236. /// - Logout
  237. /// - Registration Step #1
  238. /// - Registration via Affiliate Step #1
  239. /// - Affiliate Registration
  240. /// </summary>
  241. public ActionResult Success(string title, string message)
  242. {
  243. ViewData["title"] = title;
  244. ViewData["message"] = message;
  245. return View("Success");
  246. }
  247. /// <summary>
  248. /// Analogous to Success(), but displays a "if you didn't get the e-mail, blah blah blah" text too.
  249. /// </summary>
  250. public ActionResult SuccessEmail(string title, string message)
  251. {
  252. ViewData["title"] = title;
  253. ViewData["message"] = message;
  254. ViewData["isEmail"] = true;
  255. return View("Success");
  256. }
  257. /// <summary>
  258. /// Redirect to a given method (which is decorated with a RouteAttribute),
  259. /// with the given parameters.
  260. ///
  261. /// Lets us centralize all parameter encoding to reduce the odds of mistakenly
  262. /// passing things unencoded. Also lets us catch re-named or unadorned routes
  263. /// a little easier.
  264. ///
  265. /// Makes it less tempting to resort to error prone string.Format() stuff everywhere
  266. /// too.
  267. ///
  268. /// Also, not that { Controller = "Blah", Action = "MoreBlah" } stuff that's just as nasty
  269. /// as string.Format IMO.
  270. ///
  271. /// Note does not work with routes with "in path" parameters, only query string passed
  272. /// parameters.
  273. ///
  274. /// As an aside, boy would it be handy if you could actually use MethodGroups for something,
  275. /// instead of just being a source of compiler errors.
  276. /// </summary>
  277. public RedirectResult SafeRedirect(Delegate target, object @params = null)
  278. {
  279. var toAction = target.Method;
  280. var routes = RouteAttribute.GetDecoratedRoutes();
  281. if (!routes.Values.Contains(toAction)) throw new ArgumentException("Method not decorated with RouteAttribute: " + toAction);
  282. var registered = routes.Where(v => v.Value == toAction).Select(v => v.Key).Single();
  283. return UnsafeRedirect(registered, @params);
  284. }
  285. /// <summary>
  286. /// Redirect to a specific route, this is unsafe as it doesn't guarantee the route exists
  287. /// before creating the redirect.
  288. /// </summary>
  289. public RedirectResult UnsafeRedirect(string route, object @params = null)
  290. {
  291. var encoded = new List<string>();
  292. if (@params != null)
  293. {
  294. foreach (var k in @params.PropertiesAsStrings())
  295. encoded.Add(k.Key + "=" + Server.UrlEncode(k.Value));
  296. }
  297. var paramStr = string.Join("&", encoded);
  298. var url = "/" + route;
  299. if (paramStr.Length != 0) url += "?" + paramStr;
  300. var vals = @params == null ? "" : string.Join(", ", @params.PropertiesAsStrings().Select(s => s.Key + "=" + s.Value));
  301. return Redirect(url);
  302. }
  303. }
  304. }