/
AppHarborIntegrationModule.cs
135 lines (121 loc) · 6.97 KB
/
AppHarborIntegrationModule.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Linq.Expressions;
using System.Reflection;
using System.Web;
namespace Premotion.AspNet.AppHarbor.Integration
{
/// <summary>
/// This module modifies the native <see cref="T:System.Web.HttpContext"/> to hide the AppHarbor load balancing setup from ASP.Net.
/// </summary>
/// <remarks>
/// This should take care of the following issues:
/// http://support.appharbor.com/kb/getting-started/workaround-for-generating-absolute-urls-without-port-number
/// http://support.appharbor.com/kb/getting-started/information-about-our-load-balancer
/// </remarks>
public class AppHarborModule : IHttpModule
{
#region Constants
/// <summary>
/// Defines the name of the setting which to use to detect AppHarbor.
/// </summary>
private const string AppHarborDetectionSettingKey = "appharbor.commit_id";
/// <summary>
/// AppHarbor uses a load balancer which rewrites the REMOTE_ADDR header.
/// The original user's IP addres is stored in a separate header with this name.
/// </summary>
private const string ForwardedForHeaderName = "HTTP_X_FORWARDED_FOR";
/// <summary>
/// AppHarbor uses a load balancer which rewrites the SERVER_PROTOCOL header.
/// The original protocol is stored in a separate header with this name.
/// </summary>
/// <remarks>http://en.wikipedia.org/wiki/X-Forwarded-For</remarks>
private const string ForwardedProtocolHeaderName = "HTTP_X_FORWARDED_PROTO";
/// <summary>
/// Defines the separator which to use to split the Forwarded for header.
/// </summary>
/// <remarks>http://en.wikipedia.org/wiki/X-Forwarded-For</remarks>
private const string ForwardedForAddressesSeparator = ", ";
#endregion
#region Implementation of IHttpModule
/// <summary>
/// Initializes a module and prepares it to handle requests.
/// </summary>
/// <param name="context">An <see cref="T:System.Web.HttpApplication"/> that provides access to the methods, properties, and events common to all application objects within an ASP.NET application </param>
public void Init(HttpApplication context)
{
//If we're not running on AppHarbor, do nothing.
var appHarborCommitId = ConfigurationManager.AppSettings[AppHarborDetectionSettingKey];
if (string.IsNullOrEmpty(appHarborCommitId))
return;
var collectionType = typeof (NameValueCollection);
var readOnlyProperty = collectionType.GetProperty("IsReadOnly", BindingFlags.NonPublic | BindingFlags.Instance);
if (readOnlyProperty == null)
throw new InvalidOperationException(string.Format("Could not find property '{0}' on type '{1}'", "IsReadOnly", collectionType));
var collectionParam = Expression.Parameter(typeof (NameValueCollection));
var isReadOnly = Expression.Lambda<Func<NameValueCollection, bool>>(
Expression.Property(collectionParam, readOnlyProperty),
collectionParam
).Compile();
var valueParam = Expression.Parameter(typeof (bool));
var setReadOnly = Expression.Lambda<Action<NameValueCollection, bool>>(
Expression.Call(collectionParam, readOnlyProperty.GetSetMethod(true), valueParam),
collectionParam, valueParam
).Compile();
// listen to incoming requests to modify
context.BeginRequest += (sender, args) =>
{
// get the http context
var serverVariables = HttpContext.Current.Request.ServerVariables;
// only unlock the collection if it was locked, otherwise an exception will be raised
// see #5 for details
var wasReadOnly = isReadOnly(serverVariables);
if (wasReadOnly)
setReadOnly(serverVariables, false);
// split the forwarded for header by comma+space separated list of IP addresses, the left-most being the farthest downstream client, in order to set the correct REMOTE_ADDR
// see http://en.wikipedia.org/wiki/X-Forwarded-For
// seealso: https://github.com/trilobyte/Premotion-AspNet-AppHarbor-Integration/issues/6
var forwardedFor = serverVariables[ForwardedForHeaderName] ?? string.Empty;
if (string.IsNullOrEmpty(forwardedFor))
throw new InvalidOperationException("The behavior of the AppHarbor loadbalancer changed, it no longer specifies the HTTP_X_FORWARDED_FOR header");
var forwardSeparatorIndex = forwardedFor.LastIndexOf(ForwardedForAddressesSeparator);
// if there is only one result, the HTTP_X_FORWARDED_FOR contains only the client IP
if (forwardSeparatorIndex < 0)
{
// there is only address in the header which is the REMOTE_ADDR
serverVariables.Set("REMOTE_ADDR", forwardedFor);
// remove the HTTP_X_FORWARDED_FOR header because it is set by the AppHarbor loadbalancer
serverVariables.Remove(ForwardedForHeaderName);
}
else
{
// use the right-most address as the REMOTE_ADDR, this is how any other non load-balanced web server would normally see it
serverVariables.Set("REMOTE_ADDR", forwardedFor.Substring(forwardSeparatorIndex + ForwardedForAddressesSeparator.Length));
// remove the last value from the HTTP_X_FORWARDED_FOR header, this value is added by the AppHarbor loadbalancer
serverVariables.Set(ForwardedForHeaderName, forwardedFor.Remove(forwardSeparatorIndex));
}
// get the original protocol and remove the header added by the AppHarbor loadbalancer
var protocol = serverVariables[ForwardedProtocolHeaderName] ?? string.Empty;
serverVariables.Remove(ForwardedProtocolHeaderName);
// fix the port and protocol
var isHttps = "HTTPS".Equals(protocol, StringComparison.OrdinalIgnoreCase);
serverVariables.Set("HTTPS", isHttps ? "on" : "off");
serverVariables.Set("SERVER_PORT", isHttps ? "443" : "80");
serverVariables.Set("SERVER_PORT_SECURE", isHttps ? "1" : "0");
// only lock the collection if it was previously locked
// see #5 for details
if (wasReadOnly)
setReadOnly(serverVariables, true);
};
}
/// <summary>
/// Disposes of the resources (other than memory) used by the module that implements <see cref="T:System.Web.IHttpModule"/>.
/// </summary>
public void Dispose()
{
// nothing to do here
}
#endregion
}
}