2023-04-28 12:22:26 +08:00

1386 lines
52 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<%@ WebHandler Language="C#" Class="proxy" %>
/*
* DotNet proxy client.
*
* Version 1.1.2
* See https://github.com/Esri/resource-proxy for more information.
*
*/
#define TRACE
using System;
using System.IO;
using System.Web;
using System.Xml.Serialization;
using System.Web.Caching;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Net;
public class proxy : IHttpHandler
{
private static String version = "1.1.2";
class RateMeter
{
double _rate; //internal rate is stored in requests per second
int _countCap;
double _count = 0;
DateTime _lastUpdate = DateTime.Now;
public RateMeter(int rate_limit, int rate_limit_period)
{
_rate = (double)rate_limit / rate_limit_period / 60;
_countCap = rate_limit;
}
//called when rate-limited endpoint is invoked
public bool click()
{
TimeSpan ts = DateTime.Now - _lastUpdate;
_lastUpdate = DateTime.Now;
//assuming uniform distribution of requests over time,
//reducing the counter according to # of seconds passed
//since last invocation
_count = Math.Max(0, _count - ts.TotalSeconds * _rate);
if (_count <= _countCap)
{
//good to proceed
_count++;
return true;
}
return false;
}
public bool canBeCleaned()
{
TimeSpan ts = DateTime.Now - _lastUpdate;
return _count - ts.TotalSeconds * _rate <= 0;
}
}
private static string PROXY_REFERER = "http://localhost/proxy/proxy.ashx";
private static string DEFAULT_OAUTH = "https://www.arcgis.com/sharing/oauth2/";
private static int CLEAN_RATEMAP_AFTER = 10000; //clean the rateMap every xxxx requests
private static System.Net.IWebProxy SYSTEM_PROXY = System.Net.HttpWebRequest.DefaultWebProxy; // Use the default system proxy
private static LogTraceListener logTraceListener = null;
private static Object _rateMapLock = new Object();
/// <summary>
/// <20><>ȡԶ<C8A1>̷<EFBFBD><CCB7><EFBFBD><EFBFBD>û<EFBFBD><C3BB><EFBFBD>Ip<49><70>ַ
/// </summary>
/// <returns><3E><><EFBFBD><EFBFBD>Ip<49><70>ַ</returns>
protected string GetLoginIp(HttpRequest Request)
{
string loginip = "";
//Request.ServerVariables[""]--<2D><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
if (Request.ServerVariables["REMOTE_ADDR"] != null) //<2F>жϷ<D0B6><CFB7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD><D4B6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ip<69><70>ַ<EFBFBD>Ƿ<EFBFBD>Ϊ<EFBFBD><CEAA>
{
//<2F><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Զ<EFBFBD><D4B6><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ip<49><70>ַ
loginip = Request.ServerVariables["REMOTE_ADDR"].ToString();
}
//<2F>жϵǼ<CFB5><C7BC>û<EFBFBD><C3BB>Ƿ<EFBFBD>ʹ<EFBFBD><CAB9><EFBFBD><EFBFBD><EFBFBD>ô<EFBFBD><C3B4><EFBFBD>
else if (Request.ServerVariables["HTTP_VIA"] != null)
{
if (Request.ServerVariables["HTTP_X_FORWARDED_FOR"] != null)
{
//<2F><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD><EFBFBD>ķ<EFBFBD><C4B7><EFBFBD><EFBFBD><EFBFBD>Ip<49><70>ַ
loginip = Request.ServerVariables["HTTP_X_FORWARDED_FOR"].ToString();
}
else
{
//<2F><>ȡ<EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD>IP
loginip = Request.UserHostAddress;
}
}
else
{
//<2F><>ȡ<EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD>IP
loginip = Request.UserHostAddress;
}
return loginip;
}
public void ProcessRequest(HttpContext context)
{
IronIntel.Contractor.iisitebase.IronIntelHttpHandlerBase hb = new IronIntel.Contractor.iisitebase.IronIntelHttpHandlerBase(context);
var session = hb.GetCurrentLoginSession();
string rurl = context.Request.Url.AbsoluteUri;
string userip = GetLoginIp(context.Request);
if (session == null || session.User == null)
{
IronIntel.Contractor.SystemParams.WriteLog("Warning", "proxy", "no login - " + userip, rurl);
return;
}
else
{
IronIntel.Contractor.SystemParams.WriteLog("Info", "proxy", session.User.ID + " - " + userip, rurl);
}
if (logTraceListener == null)
{
logTraceListener = new LogTraceListener();
Trace.Listeners.Add(logTraceListener);
}
HttpResponse response = context.Response;
if (context.Request.Url.Query.Length < 1)
{
string errorMsg = "This proxy does not support empty parameters.";
log(TraceLevel.Error, errorMsg);
sendErrorResponse(context.Response, null, errorMsg, System.Net.HttpStatusCode.BadRequest);
return;
}
string uri = context.Request.Url.Query.Substring(1);
log(TraceLevel.Verbose, "URI requested: " + uri);
//if uri is ping
if (uri.Equals("ping", StringComparison.InvariantCultureIgnoreCase))
{
ProxyConfig proxyConfig = ProxyConfig.GetCurrentConfig();
String checkConfig = (proxyConfig == null) ? "Not Readable" : "OK";
String checkLog = "";
if (checkConfig != "OK")
{
checkLog = "Can not verify";
}
else
{
String filename = proxyConfig.logFile;
checkLog = (filename != null && filename != "") ? "OK" : "Not Exist/Readable";
if (checkLog == "OK")
{
log(TraceLevel.Info, "Pinged");
}
}
sendPingResponse(response, version, checkConfig, checkLog);
return;
}
//if url is encoded, decode it.
if (uri.StartsWith("http%3a%2f%2f", StringComparison.InvariantCultureIgnoreCase) || uri.StartsWith("https%3a%2f%2f", StringComparison.InvariantCultureIgnoreCase))
uri = HttpUtility.UrlDecode(uri);
ServerUrl serverUrl;
try
{
serverUrl = getConfig().GetConfigServerUrl(uri);
if (serverUrl == null)
{
//if no serverUrl found, send error message and get out.
string errorMsg = "The request URL does not match with the ServerUrl in proxy.config! Please check the proxy.config!";
log(TraceLevel.Error, errorMsg);
sendErrorResponse(context.Response, null, errorMsg, System.Net.HttpStatusCode.BadRequest);
return;
}
}
//if XML couldn't be parsed
catch (InvalidOperationException ex)
{
string errorMsg = ex.InnerException.Message + " " + uri;
log(TraceLevel.Error, errorMsg);
sendErrorResponse(context.Response, null, errorMsg, System.Net.HttpStatusCode.InternalServerError);
return;
}
//if mustMatch was set to true and URL wasn't in the list
catch (ArgumentException ex)
{
string errorMsg = ex.Message + " " + uri;
log(TraceLevel.Error, errorMsg);
sendErrorResponse(context.Response, null, errorMsg, System.Net.HttpStatusCode.Forbidden);
return;
}
//use actual request header instead of a placeholder, if present
if (context.Request.Headers["referer"] != null)
PROXY_REFERER = context.Request.Headers["referer"];
//referer
//check against the list of referers if they have been specified in the proxy.config
String[] allowedReferersArray = ProxyConfig.GetAllowedReferersArray();
if (allowedReferersArray != null && allowedReferersArray.Length > 0 && context.Request.Headers["referer"] != null)
{
PROXY_REFERER = context.Request.Headers["referer"];
string requestReferer = context.Request.Headers["referer"];
try
{
String checkValidUri = new UriBuilder(requestReferer.StartsWith("//") ? requestReferer.Substring(requestReferer.IndexOf("//") + 2) : requestReferer).Host;
}
catch (Exception e)
{
log(TraceLevel.Warning, "Proxy is being used from an invalid referer: " + context.Request.Headers["referer"]);
sendErrorResponse(context.Response, "Error verifying referer. ", "403 - Forbidden: Access is denied.", System.Net.HttpStatusCode.Forbidden);
return;
}
if (!checkReferer(allowedReferersArray, requestReferer))
{
log(TraceLevel.Warning, "Proxy is being used from an unknown referer: " + context.Request.Headers["referer"]);
sendErrorResponse(context.Response, "Unsupported referer. ", "403 - Forbidden: Access is denied.", System.Net.HttpStatusCode.Forbidden);
}
}
//Check to see if allowed referer list is specified and reject if referer is null
if (context.Request.Headers["referer"] == null && allowedReferersArray != null && !allowedReferersArray[0].Equals("*"))
{
log(TraceLevel.Warning, "Proxy is being called by a null referer. Access denied.");
sendErrorResponse(response, "Current proxy configuration settings do not allow requests which do not include a referer header.", "403 - Forbidden: Access is denied.", System.Net.HttpStatusCode.Forbidden);
return;
}
//Throttling: checking the rate limit coming from particular client IP
if (serverUrl.RateLimit > -1)
{
lock (_rateMapLock)
{
ConcurrentDictionary<string, RateMeter> ratemap = (ConcurrentDictionary<string, RateMeter>)context.Application["rateMap"];
if (ratemap == null)
{
ratemap = new ConcurrentDictionary<string, RateMeter>();
context.Application["rateMap"] = ratemap;
context.Application["rateMap_cleanup_counter"] = 0;
}
string key = "[" + serverUrl.Url + "]x[" + context.Request.UserHostAddress + "]";
RateMeter rate;
if (!ratemap.TryGetValue(key, out rate))
{
rate = new RateMeter(serverUrl.RateLimit, serverUrl.RateLimitPeriod);
ratemap.TryAdd(key, rate);
}
if (!rate.click())
{
log(TraceLevel.Warning, " Pair " + key + " is throttled to " + serverUrl.RateLimit + " requests per " + serverUrl.RateLimitPeriod + " minute(s). Come back later.");
sendErrorResponse(context.Response, "This is a metered resource, number of requests have exceeded the rate limit interval.", "Unable to proxy request for requested resource", (System.Net.HttpStatusCode)429);
return;
}
//making sure the rateMap gets periodically cleaned up so it does not grow uncontrollably
int cnt = (int)context.Application["rateMap_cleanup_counter"];
cnt++;
if (cnt >= CLEAN_RATEMAP_AFTER)
{
cnt = 0;
cleanUpRatemap(ratemap);
}
context.Application["rateMap_cleanup_counter"] = cnt;
}
}
//readying body (if any) of POST request
byte[] postBody = readRequestPostBody(context);
string post = System.Text.Encoding.UTF8.GetString(postBody);
System.Net.NetworkCredential credentials = null;
string requestUri = uri;
bool hasClientToken = false;
string token = string.Empty;
string tokenParamName = null;
if ((serverUrl.HostRedirect != null) && (serverUrl.HostRedirect != string.Empty))
{
requestUri = serverUrl.HostRedirect + new Uri(requestUri).PathAndQuery;
}
if (serverUrl.UseAppPoolIdentity)
{
credentials = CredentialCache.DefaultNetworkCredentials;
}
else if (serverUrl.Domain != null)
{
credentials = new System.Net.NetworkCredential(serverUrl.Username, serverUrl.Password, serverUrl.Domain);
}
else
{
//if token comes with client request, it takes precedence over token or credentials stored in configuration
hasClientToken = requestUri.Contains("?token=") || requestUri.Contains("&token=") || post.Contains("?token=") || post.Contains("&token=");
if (!hasClientToken)
{
// Get new token and append to the request.
// But first, look up in the application scope, maybe it's already there:
token = (String)context.Application["token_for_" + serverUrl.Url];
bool tokenIsInApplicationScope = !String.IsNullOrEmpty(token);
//if still no token, let's see if there is an access token or if are credentials stored in configuration which we can use to obtain new token
if (!tokenIsInApplicationScope)
{
token = serverUrl.AccessToken;
if (String.IsNullOrEmpty(token))
token = getNewTokenIfCredentialsAreSpecified(serverUrl, requestUri);
}
if (!String.IsNullOrEmpty(token) && !tokenIsInApplicationScope)
{
//storing the token in Application scope, to do not waste time on requesting new one untill it expires or the app is restarted.
context.Application.Lock();
context.Application["token_for_" + serverUrl.Url] = token;
context.Application.UnLock();
}
//name by which token parameter is passed (if url actually came from the list)
tokenParamName = serverUrl != null ? serverUrl.TokenParamName : null;
if (String.IsNullOrEmpty(tokenParamName))
tokenParamName = "token";
}
}
//forwarding original request
System.Net.WebResponse serverResponse = null;
try
{
serverResponse = forwardToServer(context.Request, addTokenToUri(requestUri, token, tokenParamName), postBody, credentials);
}
catch (System.Net.WebException webExc)
{
string errorMsg = webExc.Message + " " + uri;
log(TraceLevel.Error, errorMsg);
if (webExc.Response != null)
{
copyResponseHeaders(webExc.Response as System.Net.HttpWebResponse, context.Response);
using (Stream responseStream = webExc.Response.GetResponseStream())
{
byte[] bytes = new byte[32768];
int bytesRead = 0;
while ((bytesRead = responseStream.Read(bytes, 0, bytes.Length)) > 0)
{
responseStream.Write(bytes, 0, bytesRead);
}
context.Response.StatusCode = (int)(webExc.Response as System.Net.HttpWebResponse).StatusCode;
context.Response.OutputStream.Write(bytes, 0, bytes.Length);
}
}
else
{
System.Net.HttpStatusCode statusCode = System.Net.HttpStatusCode.InternalServerError;
sendErrorResponse(context.Response, null, errorMsg, statusCode);
}
return;
}
if (string.IsNullOrEmpty(token) || hasClientToken)
//if token is not required or provided by the client, just fetch the response as is:
fetchAndPassBackToClient(serverResponse, response, true);
else
{
//credentials for secured service have come from configuration file:
//it means that the proxy is responsible for making sure they were properly applied:
//first attempt to send the request:
bool tokenRequired = fetchAndPassBackToClient(serverResponse, response, false);
//checking if previously used token has expired and needs to be renewed
if (tokenRequired)
{
log(TraceLevel.Info, "Renewing token and trying again.");
//server returned error - potential cause: token has expired.
//we'll do second attempt to call the server with renewed token:
token = getNewTokenIfCredentialsAreSpecified(serverUrl, requestUri);
serverResponse = forwardToServer(context.Request, addTokenToUri(requestUri, token, tokenParamName), postBody);
//storing the token in Application scope, to do not waste time on requesting new one untill it expires or the app is restarted.
context.Application.Lock();
context.Application["token_for_" + serverUrl.Url] = token;
context.Application.UnLock();
fetchAndPassBackToClient(serverResponse, response, true);
}
}
// Use instead of response.End() to avoid the "Exception thrown: 'System.Threading.ThreadAbortException' in mscorlib.dll" error
// that appears in the output of Visual Studio. response.End() appears to only really be necessary if you need to end the thread immediately
// (i.e. no more code is processed). Since this call is at the end of the main subroutine we can safely call ApplicationInstance.CompleteRequest()
// and avoid unnecessary exceptions.
// Sources:
// http://stackoverflow.com/questions/14590812/what-is-the-difference-between-use-cases-for-using-response-endfalse-vs-appl
// http://weblogs.asp.net/hajan/why-not-to-use-httpresponse-close-and-httpresponse-end
// http://stackoverflow.com/questions/1087777/is-response-end-considered-harmful
context.ApplicationInstance.CompleteRequest();
}
public bool IsReusable
{
get { return true; }
}
/**
* Private
*/
private byte[] readRequestPostBody(HttpContext context)
{
if (context.Request.InputStream.Length > 0)
{
byte[] bytes = new byte[context.Request.InputStream.Length];
context.Request.InputStream.Read(bytes, 0, (int)context.Request.InputStream.Length);
return bytes;
}
return new byte[0];
}
private void writeRequestPostBody(System.Net.HttpWebRequest req, byte[] bytes)
{
if (bytes != null && bytes.Length > 0)
{
req.ContentLength = bytes.Length;
using (Stream outputStream = req.GetRequestStream())
{
outputStream.Write(bytes, 0, bytes.Length);
}
}
}
private System.Net.WebResponse forwardToServer(HttpRequest req, string uri, byte[] postBody, System.Net.NetworkCredential credentials = null)
{
string method = postBody.Length > 0 ? "POST" : req.HttpMethod;
System.Net.HttpWebRequest forwardReq = createHTTPRequest(uri, method, req.ContentType, credentials);
copyRequestHeaders(req, forwardReq);
writeRequestPostBody(forwardReq, postBody);
return forwardReq.GetResponse();
}
/// <summary>
/// Attempts to copy all headers from the fromResponse to the the toResponse.
/// </summary>
/// <param name="fromResponse">The response that we are copying the headers from</param>
/// <param name="toResponse">The response that we are copying the headers to</param>
private void copyResponseHeaders(System.Net.WebResponse fromResponse, HttpResponse toResponse)
{
foreach (var headerKey in fromResponse.Headers.AllKeys)
{
switch (headerKey.ToLower())
{
case "content-type":
case "transfer-encoding":
case "accept-ranges": // Prevent requests for partial content
case "access-control-allow-origin":
case "access-control-allow-credentials":
case "access-control-expose-headers":
case "access-control-max-age":
continue;
default:
toResponse.AddHeader(headerKey, fromResponse.Headers[headerKey]);
break;
}
}
// Reset the content-type for OGC WMS - issue #367
// Note: this might not be what everyone expects, but it helps some users
// TODO: make this configurable
if (fromResponse.ContentType.Contains("application/vnd.ogc.wms_xml"))
{
toResponse.ContentType = "text/xml";
log(TraceLevel.Verbose, "Adjusting Content-Type for WMS OGC: " + fromResponse.ContentType);
}
else
{
toResponse.ContentType = fromResponse.ContentType;
}
}
private void copyRequestHeaders(HttpRequest fromRequest, System.Net.HttpWebRequest toRequest)
{
foreach (var headerKey in fromRequest.Headers.AllKeys)
{
string headerValue = fromRequest.Headers[headerKey];
string headerKeyLower = headerKey.ToLower();
switch (headerKeyLower)
{
case "accept-encoding":
case "proxy-connection":
continue;
case "range":
setRangeHeader(toRequest, headerValue);
break;
case "accept":
toRequest.Accept = headerValue;
break;
case "if-modified-since":
DateTime modDT;
if (DateTime.TryParse(headerValue, out modDT))
toRequest.IfModifiedSince = modDT;
break;
case "referer":
toRequest.Referer = headerValue;
break;
case "user-agent":
toRequest.UserAgent = headerValue;
break;
default:
// Some headers are restricted and would throw an exception:
// http://msdn.microsoft.com/en-us/library/system.net.httpwebrequest.headers(v=vs.100).aspx
// Also check for our custom list of headers that should not be sent (https://github.com/Esri/resource-proxy/issues/362)
if (!System.Net.WebHeaderCollection.IsRestricted(headerKey) &&
headerKeyLower != "accept-encoding" &&
headerKeyLower != "proxy-connection" &&
headerKeyLower != "connection" &&
headerKeyLower != "keep-alive" &&
headerKeyLower != "proxy-authenticate" &&
headerKeyLower != "proxy-authorization" &&
headerKeyLower != "transfer-encoding" &&
headerKeyLower != "te" &&
headerKeyLower != "trailer" &&
headerKeyLower != "upgrade" &&
toRequest.Headers[headerKey] == null)
toRequest.Headers[headerKey] = headerValue;
break;
}
}
}
private void setRangeHeader(System.Net.HttpWebRequest req, string range)
{
string[] specifierAndRange = range.Split('=');
if (specifierAndRange.Length == 2)
{
string specifier = specifierAndRange[0];
string[] fromAndTo = specifierAndRange[1].Split('-');
if (fromAndTo.Length == 2)
{
int from, to;
if (int.TryParse(fromAndTo[0], out from) && int.TryParse(fromAndTo[1], out to))
req.AddRange(specifier, from, to);
}
}
}
private bool fetchAndPassBackToClient(System.Net.WebResponse serverResponse, HttpResponse clientResponse, bool ignoreAuthenticationErrors)
{
if (serverResponse != null)
{
using (Stream byteStream = serverResponse.GetResponseStream())
{
// Text response
if (serverResponse.ContentType.Contains("text") ||
serverResponse.ContentType.Contains("json") ||
serverResponse.ContentType.Contains("xml") ||
serverResponse.ResponseUri.ToString().Contains("callback"))
{
using (StreamReader sr = new StreamReader(byteStream))
{
string strResponse = sr.ReadToEnd();
if (
!ignoreAuthenticationErrors
&& strResponse.Contains("error")
&& Regex.Match(strResponse, "\"code\"\\s*:\\s*49[89]").Success
)
return true;
//Copy the header info and the content to the reponse to client
copyResponseHeaders(serverResponse, clientResponse);
clientResponse.Write(strResponse);
}
}
else
{
// Binary response (image, lyr file, other binary file)
//Copy the header info to the reponse to client
copyResponseHeaders(serverResponse, clientResponse);
// Tell client not to cache the image since it's dynamic
clientResponse.CacheControl = "no-cache";
byte[] buffer = new byte[32768];
int read;
while ((read = byteStream.Read(buffer, 0, buffer.Length)) > 0)
{
clientResponse.OutputStream.Write(buffer, 0, read);
}
clientResponse.OutputStream.Close();
}
serverResponse.Close();
}
}
return false;
}
private System.Net.WebResponse doHTTPRequest(string uri, string method, System.Net.NetworkCredential credentials = null)
{
byte[] bytes = null;
String contentType = null;
log(TraceLevel.Info, "Sending " + method + " request: " + uri);
if (method.Equals("POST"))
{
String[] uriArray = uri.Split(new char[] { '?' }, 2);
uri = uriArray[0];
if (uriArray.Length > 1)
{
contentType = "application/x-www-form-urlencoded";
String queryString = uriArray[1];
bytes = System.Text.Encoding.UTF8.GetBytes(queryString);
}
}
System.Net.HttpWebRequest req = createHTTPRequest(uri, method, contentType, credentials);
req.Referer = PROXY_REFERER;
writeRequestPostBody(req, bytes);
return req.GetResponse();
}
private System.Net.HttpWebRequest createHTTPRequest(string uri, string method, string contentType, System.Net.NetworkCredential credentials = null)
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
System.Net.HttpWebRequest req = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(uri);
req.ServicePoint.Expect100Continue = false;
req.Method = method;
if (method == "POST")
req.ContentType = string.IsNullOrEmpty(contentType) ? "application/x-www-form-urlencoded" : contentType;
// Use the default system proxy
req.Proxy = SYSTEM_PROXY;
if (credentials != null)
req.Credentials = credentials;
return req;
}
private string webResponseToString(System.Net.WebResponse serverResponse)
{
using (Stream byteStream = serverResponse.GetResponseStream())
{
using (StreamReader sr = new StreamReader(byteStream))
{
string strResponse = sr.ReadToEnd();
return strResponse;
}
}
}
private string getNewTokenIfCredentialsAreSpecified(ServerUrl su, string reqUrl)
{
string token = "";
string infoUrl = "";
bool isUserLogin = !String.IsNullOrEmpty(su.Username) && !String.IsNullOrEmpty(su.Password);
bool isAppLogin = !String.IsNullOrEmpty(su.ClientId) && !String.IsNullOrEmpty(su.ClientSecret);
if (isUserLogin || isAppLogin)
{
log(TraceLevel.Info, "Matching credentials found in configuration file. OAuth 2.0 mode: " + isAppLogin);
if (isAppLogin)
{
//OAuth 2.0 mode authentication
//"App Login" - authenticating using client_id and client_secret stored in config
su.OAuth2Endpoint = string.IsNullOrEmpty(su.OAuth2Endpoint) ? DEFAULT_OAUTH : su.OAuth2Endpoint;
if (su.OAuth2Endpoint[su.OAuth2Endpoint.Length - 1] != '/')
su.OAuth2Endpoint += "/";
log(TraceLevel.Info, "Service is secured by " + su.OAuth2Endpoint + ": getting new token...");
string uri = su.OAuth2Endpoint + "token?client_id=" + su.ClientId + "&client_secret=" + su.ClientSecret + "&grant_type=client_credentials&f=json";
string tokenResponse = webResponseToString(doHTTPRequest(uri, "POST"));
token = extractToken(tokenResponse, "token");
if (!string.IsNullOrEmpty(token))
token = exchangePortalTokenForServerToken(token, su);
}
else
{
//standalone ArcGIS Server/ArcGIS Online token-based authentication
//if a request is already being made to generate a token, just let it go
if (reqUrl.ToLower().Contains("/generatetoken"))
{
string tokenResponse = webResponseToString(doHTTPRequest(reqUrl, "POST"));
token = extractToken(tokenResponse, "token");
return token;
}
//lets look for '/rest/' in the requested URL (could be 'rest/services', 'rest/community'...)
if (reqUrl.ToLower().Contains("/rest/"))
infoUrl = reqUrl.Substring(0, reqUrl.IndexOf("/rest/", StringComparison.OrdinalIgnoreCase));
//if we don't find 'rest', lets look for the portal specific 'sharing' instead
else if (reqUrl.ToLower().Contains("/sharing/"))
{
infoUrl = reqUrl.Substring(0, reqUrl.IndexOf("/sharing/", StringComparison.OrdinalIgnoreCase));
infoUrl = infoUrl + "/sharing";
}
else
throw new ApplicationException("Unable to determine the correct URL to request a token to access private resources.");
if (infoUrl != "")
{
log(TraceLevel.Info, " Querying security endpoint...");
infoUrl += "/rest/info?f=json";
//lets send a request to try and determine the URL of a token generator
string infoResponse = webResponseToString(doHTTPRequest(infoUrl, "GET"));
String tokenServiceUri = getJsonValue(infoResponse, "tokenServicesUrl");
if (string.IsNullOrEmpty(tokenServiceUri))
{
string owningSystemUrl = getJsonValue(infoResponse, "owningSystemUrl");
if (!string.IsNullOrEmpty(owningSystemUrl))
{
tokenServiceUri = owningSystemUrl + "/sharing/generateToken";
}
}
if (tokenServiceUri != "")
{
log(TraceLevel.Info, " Service is secured by " + tokenServiceUri + ": getting new token...");
string uri = tokenServiceUri + "?f=json&request=getToken&referer=" + PROXY_REFERER + "&expiration=60&username=" + su.Username + "&password=" + su.Password;
string tokenResponse = webResponseToString(doHTTPRequest(uri, "POST"));
token = extractToken(tokenResponse, "token");
}
}
}
}
return token;
}
private bool checkWildcardSubdomain(String allowedReferer, String requestedReferer)
{
String[] allowedRefererParts = Regex.Split(allowedReferer, "(\\.)");
String[] refererParts = Regex.Split(requestedReferer, "(\\.)");
if (allowedRefererParts.Length != refererParts.Length)
{
return false;
}
int index = allowedRefererParts.Length - 1;
while (index >= 0)
{
if (allowedRefererParts[index].Equals(refererParts[index], StringComparison.OrdinalIgnoreCase))
{
index = index - 1;
}
else
{
if (allowedRefererParts[index].Equals("*"))
{
index = index - 1;
continue; //next
}
return false;
}
}
return true;
}
private bool pathMatched(String allowedRefererPath, String refererPath)
{
//If equal, return true
if (refererPath.Equals(allowedRefererPath))
{
return true;
}
//If the allowedRefererPath contain a ending star and match the begining part of referer, it is proper start with.
if (allowedRefererPath.EndsWith("*"))
{
String allowedRefererPathShort = allowedRefererPath.Substring(0, allowedRefererPath.Length - 1);
if (refererPath.ToLower().StartsWith(allowedRefererPathShort.ToLower()))
{
return true;
}
}
return false;
}
private bool domainMatched(String allowedRefererDomain, String refererDomain)
{
if (allowedRefererDomain.Equals(refererDomain))
{
return true;
}
//try if the allowed referer contains wildcard for subdomain
if (allowedRefererDomain.Contains("*"))
{
if (checkWildcardSubdomain(allowedRefererDomain, refererDomain))
{
return true;//return true if match wildcard subdomain
}
}
return false;
}
private bool protocolMatch(String allowedRefererProtocol, String refererProtocol)
{
return allowedRefererProtocol.Equals(refererProtocol);
}
private String getDomainfromURL(String url, String protocol)
{
String domain = url.Substring(protocol.Length + 3);
domain = domain.IndexOf('/') >= 0 ? domain.Substring(0, domain.IndexOf('/')) : domain;
return domain;
}
private bool checkReferer(String[] allowedReferers, String referer)
{
if (allowedReferers != null && allowedReferers.Length > 0)
{
if (allowedReferers.Length == 1 && allowedReferers[0].Equals("*")) return true; //speed-up
foreach (String allowedReferer in allowedReferers)
{
//Parse the protocol, domain and path of the referer
String refererProtocol = referer.StartsWith("https://") ? "https" : "http";
String refererDomain = getDomainfromURL(referer, refererProtocol);
String refererPath = referer.Substring(refererProtocol.Length + 3 + refererDomain.Length);
String allowedRefererCannonical = null;
//since the allowedReferer can be a malformed URL, we first construct a valid one to be compared with referer
//if allowedReferer starts with https:// or http://, then exact match is required
if (allowedReferer.StartsWith("https://") || allowedReferer.StartsWith("http://"))
{
allowedRefererCannonical = allowedReferer;
}
else
{
String protocol = refererProtocol;
//if allowedReferer starts with "//" or no protocol, we use the one from refererURL to prefix to allowedReferer.
if (allowedReferer.StartsWith("//"))
{
allowedRefererCannonical = protocol + ":" + allowedReferer;
}
else
{
//if the allowedReferer looks like "example.esri.com"
allowedRefererCannonical = protocol + "://" + allowedReferer;
}
}
//parse the protocol, domain and the path of the allowedReferer
String allowedRefererProtocol = allowedRefererCannonical.StartsWith("https://") ? "https" : "http";
String allowedRefererDomain = getDomainfromURL(allowedRefererCannonical, allowedRefererProtocol);
String allowedRefererPath = allowedRefererCannonical.Substring(allowedRefererProtocol.Length + 3 + allowedRefererDomain.Length);
//Check if both domain and path match
if (protocolMatch(allowedRefererProtocol, refererProtocol) &&
domainMatched(allowedRefererDomain, refererDomain) &&
pathMatched(allowedRefererPath, refererPath))
{
return true;
}
}
return false;//no-match
}
return true;//when allowedReferer is null, then allow everything
}
private string exchangePortalTokenForServerToken(string portalToken, ServerUrl su)
{
//ideally, we should POST the token request
log(TraceLevel.Info, " Exchanging Portal token for Server-specific token for " + su.Url + "...");
string uri = su.OAuth2Endpoint.Substring(0, su.OAuth2Endpoint.IndexOf("/oauth2/", StringComparison.OrdinalIgnoreCase)) +
"/generateToken?token=" + portalToken + "&serverURL=" + su.Url + "&f=json";
string tokenResponse = webResponseToString(doHTTPRequest(uri, "GET"));
return extractToken(tokenResponse, "token");
}
private static void sendPingResponse(HttpResponse response, String version, String config, String log)
{
response.AddHeader("Content-Type", "application/json");
response.AddHeader("Accept-Encoding", "gzip");
String message = "{ " +
"\"Proxy Version\": \"" + version + "\"" +
", \"Configuration File\": \"" + config + "\"" +
", \"Log File\": \"" + log + "\"" +
"}";
response.StatusCode = 200;
response.Write(message);
response.Flush();
}
private static void sendErrorResponse(HttpResponse response, String errorDetails, String errorMessage, System.Net.HttpStatusCode errorCode)
{
String message = string.Format("{{\"error\": {{\"code\": {0},\"message\":\"{1}\"", (int)errorCode, errorMessage);
if (!string.IsNullOrEmpty(errorDetails))
message += string.Format(",\"details\":[\"message\":\"{0}\"]", errorDetails);
message += "}}";
response.StatusCode = (int)errorCode;
//custom status description for when the rate limit has been exceeded
if (response.StatusCode == 429)
{
response.StatusDescription = "Too Many Requests";
}
//this displays our customized error messages instead of IIS's custom errors
response.TrySkipIisCustomErrors = true;
response.Write(message);
response.Flush();
}
private static string getClientIp(HttpRequest request)
{
if (request == null)
return null;
string remoteAddr = request.ServerVariables["HTTP_X_FORWARDED_FOR"];
if (string.IsNullOrWhiteSpace(remoteAddr))
{
remoteAddr = request.ServerVariables["REMOTE_ADDR"];
}
else
{
// the HTTP_X_FORWARDED_FOR may contain an array of IP, this can happen if you connect through a proxy.
string[] ipRange = remoteAddr.Split(',');
remoteAddr = ipRange[ipRange.Length - 1];
}
return remoteAddr;
}
private string addTokenToUri(string uri, string token, string tokenParamName)
{
if (!String.IsNullOrEmpty(token))
uri += uri.Contains("?") ? "&" + tokenParamName + "=" + token : "?" + tokenParamName + "=" + token;
return uri;
}
private string extractToken(string tokenResponse, string key)
{
string token = getJsonValue(tokenResponse, key);
if (string.IsNullOrEmpty(token))
log(TraceLevel.Error, " Token cannot be obtained: " + tokenResponse);
else
log(TraceLevel.Info, " Token obtained: " + token);
return token;
}
private string getJsonValue(string text, string key)
{
int i = text.IndexOf(key);
String value = "";
if (i > -1)
{
value = text.Substring(text.IndexOf(':', i) + 1).Trim();
value = value.Length > 0 && value[0] == '"' ?
// Get the rest of a quoted string
value.Substring(1, Math.Max(0, value.IndexOf('"', 1) - 1)) :
// Get a string up to the closest comma, bracket, or brace
value = value.Substring(0,
Math.Min(
value.Length,
Math.Min(
indexOf_HighFlag(value, ","),
Math.Min(
indexOf_HighFlag(value, "]"),
indexOf_HighFlag(value, "}")
)
)
)
);
}
return value;
}
private int indexOf_HighFlag(string text, string key)
{
int i = text.IndexOf(key);
if (i < 0) i = Int32.MaxValue;
return i;
}
private void cleanUpRatemap(ConcurrentDictionary<string, RateMeter> ratemap)
{
foreach (string key in ratemap.Keys)
{
RateMeter rate = ratemap[key];
if (rate.canBeCleaned())
ratemap.TryRemove(key, out rate);
}
}
/**
* Static
*/
private static ProxyConfig getConfig()
{
ProxyConfig config = ProxyConfig.GetCurrentConfig();
if (config != null)
return config;
else
throw new ApplicationException("The proxy configuration file cannot be found, or is not readable.");
}
//writing Log file
private static void log(TraceLevel logLevel, string msg)
{
if (logLevel <= TraceLevel.Warning)
IronIntel.Contractor.SystemParams.WriteLog(logLevel.ToString(), "trafficproxy", msg, msg);
//string logMessage = string.Format("{0} {1}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), msg);
//ProxyConfig config = ProxyConfig.GetCurrentConfig();
//TraceSwitch ts = null;
//if (config.logLevel != null)
//{
// ts = new TraceSwitch("TraceLevelSwitch2", "TraceSwitch in the proxy.config file", config.logLevel);
//}
//else
//{
// ts = new TraceSwitch("TraceLevelSwitch2", "TraceSwitch in the proxy.config file", "Error");
// config.logLevel = "Error";
//}
//Trace.WriteLineIf(logLevel <= ts.Level, logMessage);
}
private static object _lockobject = new object();
}
class LogTraceListener : TraceListener
{
private static object _lockobject = new object();
public override void Write(string message)
{
//Only log messages to disk if logFile has value in configuration, otherwise log nothing.
ProxyConfig config = ProxyConfig.GetCurrentConfig();
if (config.LogFile != null)
{
string log = config.LogFile;
if (!log.Contains("\\") || log.Contains(".\\"))
{
if (log.Contains(".\\")) //If this type of relative pathing .\log.txt
{
log = log.Replace(".\\", "");
}
string configDirectory = HttpContext.Current.Server.MapPath("proxy.config"); //Cannot use System.Web.Hosting.HostingEnvironment.ApplicationPhysicalPath b/ config may be in a child directory
string path = configDirectory.Replace("proxy.config", "");
log = path + log;
}
lock (_lockobject)
{
using (StreamWriter sw = File.AppendText(log))
{
sw.Write(message);
}
}
}
}
public override void WriteLine(string message)
{
//Only log messages to disk if logFile has value in configuration, otherwise log nothing.
ProxyConfig config = ProxyConfig.GetCurrentConfig();
if (config.LogFile != null)
{
string log = config.LogFile;
if (!log.Contains("\\") || log.Contains(".\\"))
{
if (log.Contains(".\\")) //If this type of relative pathing .\log.txt
{
log = log.Replace(".\\", "");
}
string configDirectory = HttpContext.Current.Server.MapPath("proxy.config"); //Cannot use System.Web.Hosting.HostingEnvironment.ApplicationPhysicalPath b/ config may be in a child directory
string path = configDirectory.Replace("proxy.config", "");
log = path + log;
}
lock (_lockobject)
{
using (StreamWriter sw = File.AppendText(log))
{
sw.WriteLine(message);
}
}
}
}
}
[XmlRoot("ProxyConfig")]
public class ProxyConfig
{
private static object _lockobject = new object();
public static ProxyConfig LoadProxyConfig(string fileName)
{
ProxyConfig config = null;
lock (_lockobject)
{
if (System.IO.File.Exists(fileName))
{
XmlSerializer reader = new XmlSerializer(typeof(ProxyConfig));
using (System.IO.StreamReader file = new System.IO.StreamReader(fileName))
{
try
{
config = (ProxyConfig)reader.Deserialize(file);
}
catch (Exception ex)
{
throw ex;
}
}
}
}
return config;
}
public static ProxyConfig GetCurrentConfig()
{
ProxyConfig config = HttpRuntime.Cache["proxyConfig"] as ProxyConfig;
if (config == null)
{
string fileName = HttpContext.Current.Server.MapPath("proxy.config");
config = LoadProxyConfig(fileName);
if (config != null)
{
CacheDependency dep = new CacheDependency(fileName);
HttpRuntime.Cache.Insert("proxyConfig", config, dep);
}
}
return config;
}
//referer
//create an array with valid referers using the allowedReferers String that is defined in the proxy.config
public static String[] GetAllowedReferersArray()
{
if (allowedReferers == null)
return null;
return allowedReferers.Split(',');
}
//referer
//check if URL starts with prefix...
public static bool isUrlPrefixMatch(String prefix, String uri)
{
return uri.ToLower().StartsWith(prefix.ToLower()) ||
uri.ToLower().Replace("https://", "http://").StartsWith(prefix.ToLower()) ||
uri.ToLower().Substring(uri.IndexOf("//")).StartsWith(prefix.ToLower());
}
ServerUrl[] serverUrls;
public String logFile;
public String logLevel;
bool mustMatch;
//referer
static String allowedReferers;
[XmlArray("serverUrls")]
[XmlArrayItem("serverUrl")]
public ServerUrl[] ServerUrls
{
get { return this.serverUrls; }
set
{
this.serverUrls = value;
}
}
[XmlAttribute("mustMatch")]
public bool MustMatch
{
get { return mustMatch; }
set
{ mustMatch = value; }
}
//logFile
[XmlAttribute("logFile")]
public String LogFile
{
get { return logFile; }
set
{ logFile = value; }
}
//logLevel
[XmlAttribute("logLevel")]
public String LogLevel
{
get { return logLevel; }
set
{ logLevel = value; }
}
//referer
[XmlAttribute("allowedReferers")]
public string AllowedReferers
{
get { return allowedReferers; }
set
{
allowedReferers = Regex.Replace(value, @"\s", "");
}
}
public ServerUrl GetConfigServerUrl(string uri)
{
//split both request and proxy.config urls and compare them
string[] uriParts = uri.Split(new char[] { '/', '?' }, StringSplitOptions.RemoveEmptyEntries);
string[] configUriParts = new string[] { };
foreach (ServerUrl su in serverUrls)
{
//if a relative path is specified in the proxy.config, append what's in the request itself
if (!su.Url.StartsWith("http"))
su.Url = su.Url.Insert(0, uriParts[0]);
configUriParts = su.Url.Split(new char[] { '/', '?' }, StringSplitOptions.RemoveEmptyEntries);
//if the request has less parts than the config, don't allow
if (configUriParts.Length > uriParts.Length) continue;
int i = 0;
for (i = 0; i < configUriParts.Length; i++)
{
if (!configUriParts[i].ToLower().Equals(uriParts[i].ToLower())) break;
}
if (i == configUriParts.Length)
{
//if the urls don't match exactly, and the individual matchAll tag is 'false', don't allow
if (configUriParts.Length == uriParts.Length || su.MatchAll)
return su;
}
}
if (!mustMatch)
{
return new ServerUrl(uri);
}
else
{
throw new ArgumentException("Proxy has not been set up for this URL. Make sure there is a serverUrl in the configuration file that matches: " + uri);
}
}
}
public class ServerUrl
{
string url;
string hostRedirect;
bool matchAll;
string oauth2Endpoint;
string domain;
bool useAppPoolIdentity;
string username;
string password;
string clientId;
string clientSecret;
string accessToken;
string tokenParamName;
string rateLimit;
string rateLimitPeriod;
private ServerUrl()
{
}
public ServerUrl(String url)
{
this.url = url;
}
[XmlAttribute("url")]
public string Url
{
get { return url; }
set { url = value; }
}
[XmlAttribute("hostRedirect")]
public string HostRedirect
{
get { return hostRedirect; }
set { hostRedirect = value; }
}
[XmlAttribute("matchAll")]
public bool MatchAll
{
get { return matchAll; }
set { matchAll = value; }
}
[XmlAttribute("oauth2Endpoint")]
public string OAuth2Endpoint
{
get { return oauth2Endpoint; }
set { oauth2Endpoint = value; }
}
[XmlAttribute("domain")]
public string Domain
{
get { return domain; }
set { domain = value; }
}
[XmlAttribute("useAppPoolIdentity")]
public bool UseAppPoolIdentity
{
get { return useAppPoolIdentity; }
set { useAppPoolIdentity = value; }
}
[XmlAttribute("username")]
public string Username
{
get { return username; }
set { username = value; }
}
[XmlAttribute("password")]
public string Password
{
get { return password; }
set { password = value; }
}
[XmlAttribute("clientId")]
public string ClientId
{
get { return clientId; }
set { clientId = value; }
}
[XmlAttribute("clientSecret")]
public string ClientSecret
{
get { return clientSecret; }
set { clientSecret = value; }
}
[XmlAttribute("accessToken")]
public string AccessToken
{
get { return accessToken; }
set { accessToken = value; }
}
[XmlAttribute("tokenParamName")]
public string TokenParamName
{
get { return tokenParamName; }
set { tokenParamName = value; }
}
[XmlAttribute("rateLimit")]
public int RateLimit
{
get { return string.IsNullOrEmpty(rateLimit) ? -1 : int.Parse(rateLimit); }
set { rateLimit = value.ToString(); }
}
[XmlAttribute("rateLimitPeriod")]
public int RateLimitPeriod
{
get { return string.IsNullOrEmpty(rateLimitPeriod) ? 60 : int.Parse(rateLimitPeriod); }
set { rateLimitPeriod = value.ToString(); }
}
}