From 324816943394d39bece2fafc129af17ae5b8fcb2 Mon Sep 17 00:00:00 2001 From: David Spriggs Date: Thu, 21 Aug 2014 00:18:43 -0500 Subject: [PATCH] Updated grunt tasks, readme, proxys, and tagged commit as v1.2.0. --- Gruntfile.js | 11 +- README.md | 42 +- package.json | 4 +- resource-proxy.zip | Bin 0 -> 54459 bytes viewer/proxy/PROXY_README.md | 96 ++++ viewer/proxy/Web.config | 18 + viewer/proxy/proxy.ashx | 899 ++++++++++++++++++++++++++------ viewer/proxy/proxy.config | 30 +- viewer/proxy/proxy.xsd | 31 ++ viewer/proxy/proxypage_java.zip | Bin 1061 -> 0 bytes viewer/proxy/proxypage_net.zip | Bin 3268 -> 0 bytes viewer/proxy/proxypage_php.zip | Bin 1915 -> 0 bytes viewer/proxy/readme.md | 2 +- 13 files changed, 907 insertions(+), 226 deletions(-) create mode 100644 resource-proxy.zip create mode 100755 viewer/proxy/PROXY_README.md create mode 100755 viewer/proxy/Web.config mode change 100644 => 100755 viewer/proxy/proxy.ashx mode change 100644 => 100755 viewer/proxy/proxy.config create mode 100755 viewer/proxy/proxy.xsd delete mode 100644 viewer/proxy/proxypage_java.zip delete mode 100644 viewer/proxy/proxypage_net.zip delete mode 100644 viewer/proxy/proxypage_php.zip diff --git a/Gruntfile.js b/Gruntfile.js index 769bbaa66..f1e2595f1 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -3,10 +3,8 @@ module.exports = function(grunt) { pkg: grunt.file.readJSON('package.json'), tag: { banner: '/* <%= pkg.name %>\n' + - ' * @version <%= pkg.version %>\n' + - ' * @author <%= pkg.author %>\n' + + ' * version <%= pkg.version %>\n' + ' * Project: <%= pkg.homepage %>\n' + - ' * Copyright <%= pkg.year %>. <%= pkg.license %> licensed.\n' + ' */\n' }, copy: { @@ -19,7 +17,7 @@ module.exports = function(grunt) { }, clean: { build: { - src: ['dist/viewer'] + src: ['dist'] } }, autoprefixer: { @@ -58,10 +56,7 @@ module.exports = function(grunt) { }], options: { banner: '<%= tag.banner %>', - sourceMap: true, - sourceMapName: function(filePath) { - return filePath + '.map'; - } + sourceMap: true } } }, diff --git a/README.md b/README.md index 485cc97cc..8c92d882a 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ This JS web app can be easily configured or used as a boilerplate/starting point for basic viewers. It also demonstrates best practices for modular design and OOP via classes in JS using dojo's great [declare](http://dojotoolkit.org/reference-guide/1.9/dojo/_base/declare.html) system. +![screen shot 2014-08-20 at 9 59 48 pm](https://cloud.githubusercontent.com/assets/661156/3991302/5aa2e0f2-28df-11e4-94d0-9c813937d933.png) ## Demo Site [http://davidspriggs.github.io/ConfigurableViewerJSAPI/viewer](http://davidspriggs.github.io/ConfigurableViewerJSAPI/viewer) -Note: Not all functions work in the demo site due to a limitation in GitHub project hosting (no functioning proxy page). ## Installation: * Download the latest release [here](https://github.com/DavidSpriggs/ConfigurableViewerJSAPI/releases). @@ -18,10 +18,10 @@ Note: Not all functions work in the demo site due to a limitation in GitHub proj * Enjoy! ## Customize: -* Use the ConfigurableViewerJSAPI\js\config\viewer.js file to customize your own map layers, task urls and widgets. -* Use the [wiki](https://github.com/DavidSpriggs/ConfigurableViewerJSAPI/wiki) documentation for guidance on configuring widgets. +* Use the `ConfigurableViewerJSAPI\js\config\viewer.js` file to customize your own map layers, task urls and widgets. +* Use the [wiki](https://github.com/DavidSpriggs/ConfigurableViewerJSAPI/wiki) documentation for guidance on configuring individual widgets. -## Widgets Included: +# Widgets Included: * Base Maps * Bookmarks * Directions @@ -30,49 +30,53 @@ Note: Not all functions work in the demo site due to a limitation in GitHub proj * Find * Geocoder * Growler -* Help Button +* Help * Home -* Identify (for dynamic layers) +* Identify * Legend * Locate Button (Geolocation) * Measure * Overview Map -* Print (Advanced) +* Print * Scalebar * StreetView * Table of contents -* If there is a feature you would like to request, add it to the projects [trello board](https://trello.com/b/TjjipGmV/configurable-map-viewer) for consideration. +* Find +* Map Right click menu with various widget functions. +* Highly configurable UI, right or left sidebars with widgets in both, top and bottom regions for other content. + +## Proposing Features +If there is a feature you would like to request, add it to the [issues list](https://github.com/DavidSpriggs/ConfigurableViewerJSAPI/issues) for consideration. ## Change log: See [releases](https://github.com/DavidSpriggs/ConfigurableViewerJSAPI/releases) for change logs. -## IRC +# Community We have an IRC channel: `#cmv` on freenode for the project. If you have questions, stop on by. I recommend [HexChat](http://hexchat.github.io) as an IRC client or you can use freenode's [webchat](http://webchat.freenode.net) client. -## Contributing to the project +### Contributing to the project There are many ways to contribute: 1. Contribute code as widgets (see below). 2. Created documentation in [the wiki](https://github.com/DavidSpriggs/ConfigurableViewerJSAPI/wiki). -3. Submit issues you find the the [issue log](https://github.com/DavidSpriggs/ConfigurableViewerJSAPI/issues?state=open). -4. Vote, comment on, and submit ideas for things to build and improvements to the viewer in the [trello board](https://trello.com/b/TjjipGmV/configurable-map-viewer). +3. Submit issues you find in the [issues log](https://github.com/DavidSpriggs/ConfigurableViewerJSAPI/issues?state=open). ### Grunt tasks This project uses grunt to automate tasks like minifying css and js as well as js linting and css prefixing. ### To get started setup you dev machine: - Install [node](http://nodejs.org). -- Install the grunt cli (command line interface) globally from the command line with : `npm install -g grunt-cli` this only needs to be done once per dev machine. -- Install jshint globally from the command line with : `npm install -g jshint` this only needs to be done once per dev machine. +- Install the grunt cli (command line interface) globally from the command line with : `npm install -g grunt-cli`, this only needs to be done once per dev machine. +- Install jshint globally from the command line with : `npm install -g jshint`, this only needs to be done once per dev machine. ### Get the code and install dev dependencies: - Fork the repo into your own github account. -- Clone your fork and in the repos directory: -- Install the local dev dependencies for the project in the repo from the command line: `npm install` This only needs to be done once per dev machine. -- Run grunt from the repo with: `grunt` this will lint your js as you code. +- Clone your fork and in cloned directory: +- Install the local dev dependencies for the project in the repo from the command line: `npm install`, this only needs to be done once per dev machine. +- Run grunt from the repo with: `grunt` this will launch a mini dev server and lint your js as you code. - Run grunt from the repo with: `grunt build` this will create a `dist` folder with minified code ready for deployment. -- There are other grunt tasks use: `grunt -h` to see a list +- There are other grunt tasks, use: `grunt -h` to see a list. -## License +# License MIT diff --git a/package.json b/package.json index f9dfcb6e2..3eb9ff12e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "ConfigurableViewerJSAPI", - "version": "v1.0.1", + "name": "ConfigurableMapViewerCMV", + "version": "1.2.0", "author": "David Spriggs", "license": "MIT", "year": "2014", diff --git a/resource-proxy.zip b/resource-proxy.zip new file mode 100644 index 0000000000000000000000000000000000000000..4e127987674fecd73011da88c6b01a23f421843b GIT binary patch literal 54459 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 entry for each service that will use the proxy. The proxy.config allows you to use the serverUrl tag to specify one or more ArcGIS Server services that the proxy will forward requests to. The serverUrl tag has the following attributes: + * **url**: Location of the ArcGIS Server service (or other URL) to proxy. Specify either the specific URL or the root (in which case you should set matchAll="false"). + * **matchAll="true"**: When true all requests that begin with the specified URL are forwarded. Otherwise, the URL requested must match exactly. + * **username**: Username to use when requesting a token - if needed for ArcGIS Server token based authentication. + * **password**: Password to use when requesting a token - if needed for ArcGIS Server token based authentication. + * **clientId**. Used with clientSecret for OAuth authentication to obtain a token - if needed for OAuth 2.0 authentication. **NOTE**: If used to access hosted services, the service(s) must be owned by the user accessing it, (with the exception of credit-based esri services, e.g. routing, geoenrichment, etc.) + * **clientSecret**: Used with clientId for OAuth authentication to obtain a token - if needed for OAuth 2.0 authentication. + * **oauth2Endpoint**: When using OAuth 2.0 authentication specify the portal specific OAuth 2.0 authentication endpoint. The default value is https://www.arcgis.com/sharing/oauth2/. + * **accessToken**: OAuth2 access token to use instead of on-demand access-token generation using clientId & clientSecret. + * **rateLimit**: The maximum number of requests with a particular referer over the specified **rateLimitPeriod**. + * **rateLimitPeriod**: The time period (in minutes) within which the specified number of requests (rate_limit) sent with a particular referer will be tracked. The default value is 60 (one hour). + +Note: Refresh the proxy application after updates to the proxy.config have been made. + +Example of proxy using application credentials and limiting requests to 10/minute +``` + + +``` +Example of a tag for a resource which does not require authentication +``` + + +``` +Note: You may have to refresh the proxy application after updates to the proxy.config have been made. + +##Folders and Files + +The proxy consists of the following files: +* proxy.config: This file contains the configuration settings for the proxy. This is where you will define all the resources that will use the proxy. After updating this file you might need to refresh the proxy application using IIS tools in order for the changes to take effect. +* **Important note:** In order to keep your credentials safe, ensure that your web server will not display the text inside your proxy.config in the browser (ie: http://[yourmachine]/proxy/proxy.config). +* proxy.ashx: The actual proxy application. In most cases you will not need to modify this file. +* web.config: An XML file that stores ASP.NET configuration data. Use this file to configure logging for the proxy. By default the proxy will write log messages to a file named auth_proxy.log located in 'C:\Temp\Shared\proxy_logs'. Note that the folder location needs to exist in order for the log file to be successfully created. +##Requirements + +* ASP.NET 4.0 or greater (4.5 is required on Windows 8/Server 2012, see [this article] (http://www.iis.net/learn/get-started/whats-new-in-iis-8/iis-80-using-aspnet-35-and-aspnet-45) for more information) + +##Issues + +Found a bug or want to request a new feature? Let us know by submitting an issue. + +##Contributing + +All contributions are welcome. + +##Licensing + +Copyright 2014 Esri + +Licensed under the Apache License, Version 2.0 (the "License"); +You may not use this file except in compliance with the License. +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for specific language governing permissions and limitations under the license. diff --git a/viewer/proxy/Web.config b/viewer/proxy/Web.config new file mode 100755 index 000000000..8d1a40128 --- /dev/null +++ b/viewer/proxy/Web.config @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + diff --git a/viewer/proxy/proxy.ashx b/viewer/proxy/proxy.ashx old mode 100644 new mode 100755 index a36effccc..6446e630d --- a/viewer/proxy/proxy.ashx +++ b/viewer/proxy/proxy.ashx @@ -1,281 +1,834 @@ <%@ WebHandler Language="C#" Class="proxy" %> + /* - This proxy page does not have any security checks. It is highly recommended - that a user deploying this proxy page on their web server, add appropriate - security checks, for example checking request path, username/password, target - url, etc. -*/ + * DotNet proxy client. + * + * Version 1.1 beta + * See https://github.com/Esri/resource-proxy for more information. + * + */ + +#define TRACE using System; -using System.Drawing; using System.IO; using System.Web; -using System.Collections.Generic; -using System.Text; using System.Xml.Serialization; using System.Web.Caching; +using System.Collections.Concurrent; +using System.Diagnostics; -/// -/// Forwards requests to an ArcGIS Server REST resource. Uses information in -/// the proxy.config file to determine properties of the server. -/// public class proxy : IHttpHandler { - - public void ProcessRequest (HttpContext context) { - // use the following line to ignore invalid (self signed) ssl certs: - System.Net.ServicePointManager.ServerCertificateValidationCallback = delegate(object s, System.Security.Cryptography.X509Certificates.X509Certificate certificate, System.Security.Cryptography.X509Certificates.X509Chain chain, System.Net.Security.SslPolicyErrors sslPolicyErrors) { return true; }; - + + 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 Object _rateMapLock = new Object(); + + public void ProcessRequest(HttpContext context) { HttpResponse response = context.Response; + if (context.Request.Url.Query.Length < 1) + { + string errorMsg = "No URL specified"; + log(TraceLevel.Error, errorMsg); + sendErrorResponse(context.Response, null, errorMsg, System.Net.HttpStatusCode.BadRequest); + return; + } - // Get the URL requested by the client (take the entire querystring at once - // to handle the case of the URL itself containing querystring parameters) string uri = context.Request.Url.Query.Substring(1); - // Get token, if applicable, and append to the request - string token = getTokenFromConfigFile(uri); - if (!String.IsNullOrEmpty(token)) - { - if (uri.Contains("?")) - uri += "&token=" + token; - else - uri += "?token=" + token; + //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); + + log(TraceLevel.Info, uri); + ServerUrl serverUrl; + bool passThrough = false; + try { + serverUrl = getConfig().GetConfigServerUrl(uri); + passThrough = serverUrl == null; + } + //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; } - - System.Net.HttpWebRequest req = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(uri); - req.Method = context.Request.HttpMethod; - req.ServicePoint.Expect100Continue = false; - req.Referer = context.Request.Headers["referer"]; - - // Set body of request for POST requests - if (context.Request.InputStream.Length > 0) + //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) { - byte[] bytes = new byte[context.Request.InputStream.Length]; - context.Request.InputStream.Read(bytes, 0, (int)context.Request.InputStream.Length); - req.ContentLength = bytes.Length; - - string ctype = context.Request.ContentType; - if (String.IsNullOrEmpty(ctype)) { - req.ContentType = "application/x-www-form-urlencoded"; - } - else { - req.ContentType = ctype; + bool allowed = false; + string requestReferer = context.Request.Headers["referer"]; + + foreach (var referer in allowedReferersArray) + { + if ((allowedReferersArray.Length == 1) && referer == String.Empty) + break; + + if (requestReferer != null && requestReferer != String.Empty && (ProxyConfig.isUrlPrefixMatch(referer, requestReferer)) || referer == "*") + { + allowed = true; + break; + } } - - using (Stream outputStream = req.GetRequestStream()) + + if (!allowed) { - outputStream.Write(bytes, 0, bytes.Length); + string errorMsg = "Proxy is being used from an unsupported referer: " + context.Request.Headers["referer"]; + log(TraceLevel.Error, errorMsg); + sendErrorResponse(context.Response, null, errorMsg, System.Net.HttpStatusCode.Forbidden); + return; } } - else { - req.Method = "GET"; + + //Throttling: checking the rate limit coming from particular client IP + if (!passThrough && serverUrl.RateLimit > -1) { + lock (_rateMapLock) + { + ConcurrentDictionary ratemap = (ConcurrentDictionary)context.Application["rateMap"]; + if (ratemap == null) + { + ratemap = new ConcurrentDictionary(); + 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.PaymentRequired); + 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; + } } - - // Send the request to the server - System.Net.WebResponse serverResponse = null; - - - try + + //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 (!passThrough && serverUrl.Domain != null) { - serverResponse = req.GetResponse(); + credentials = new System.Net.NetworkCredential(serverUrl.Username, serverUrl.Password, serverUrl.Domain); } - catch (System.Net.WebException webExc) + else { - response.StatusCode = 500; - response.StatusDescription = webExc.Status.ToString(); - response.Write(webExc.Response); - response.End(); - return; + //if token comes with client request, it takes precedence over token or credentials stored in configuration + hasClientToken = uri.Contains("?token=") || uri.Contains("&token=") || post.Contains("?token=") || post.Contains("&token="); + + if (!passThrough && !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, uri); + } + + 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"; + + requestUri = addTokenToUri(uri, token, tokenParamName); } - // Set up the response to the client - if (serverResponse != null) { - response.ContentType = serverResponse.ContentType; - using (Stream byteStream = serverResponse.GetResponseStream()) - { - // Text response - if (serverResponse.ContentType.Contains("text") || - serverResponse.ContentType.Contains("json")) + + //forwarding original request + System.Net.WebResponse serverResponse = null; + try { + serverResponse = forwardToServer(context, requestUri, postBody, credentials); + } catch (System.Net.WebException webExc) { + + string errorMsg = webExc.Message + " " + uri; + log(TraceLevel.Error, errorMsg); + + if (webExc.Response != null) + { + copyHeaders(webExc.Response as System.Net.HttpWebResponse, context.Response); + + using (Stream responseStream = webExc.Response.GetResponseStream()) { - using (StreamReader sr = new StreamReader(byteStream)) + byte[] bytes = new byte[32768]; + int bytesRead = 0; + + while ((bytesRead = responseStream.Read(bytes, 0, bytes.Length)) > 0) { - string strResponse = sr.ReadToEnd(); - response.Write(strResponse); + 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 - { - // Binary response (image, lyr file, other binary file) - BinaryReader br = new BinaryReader(byteStream); - byte[] outb = br.ReadBytes((int)serverResponse.ContentLength); - br.Close(); + } + else + { + System.Net.HttpStatusCode statusCode = System.Net.HttpStatusCode.InternalServerError; + sendErrorResponse(context.Response, null, errorMsg, statusCode); + } + return; + } - // Tell client not to cache the image since it's dynamic - response.CacheControl = "no-cache"; + if (passThrough || 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: - // Send the image to the client - // (Note: if large images/files sent, could modify this to send in chunks) - response.OutputStream.Write(outb, 0, outb.Length); - } + //first attempt to send the request: + bool tokenRequired = fetchAndPassBackToClient(serverResponse, response, false); - serverResponse.Close(); + + //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, uri); + serverResponse = forwardToServer(context, addTokenToUri(uri, 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); } } response.End(); } - + public bool IsReusable { - get { - return false; + 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 System.Net.WebResponse forwardToServer(HttpContext context, string uri, byte[] postBody, System.Net.NetworkCredential credentials = null) + { + return + postBody.Length > 0? + doHTTPRequest(uri, postBody, "POST", context.Request.Headers["referer"], context.Request.ContentType, credentials): + doHTTPRequest(uri, context.Request.HttpMethod, credentials); } - // Gets the token for a server URL from a configuration file - // TODO: ?modify so can generate a new short-lived token from username/password in the config file - private string getTokenFromConfigFile(string uri) + /// + /// Attempts to copy all headers from the fromResponse to the the toResponse. + /// + /// The response that we are copying the headers from + /// The response that we are copying the headers to + private void copyHeaders(System.Net.WebResponse fromResponse, HttpResponse toResponse) { - try + foreach (var headerKey in fromResponse.Headers.AllKeys) { - ProxyConfig config = ProxyConfig.GetCurrentConfig(); - if (config != null) - return config.GetToken(uri); - else - throw new ApplicationException( - "Proxy.config file does not exist at application root, or is not readable."); + switch (headerKey.ToLower()) + { + case "content-type": + case "transfer-encoding": + continue; + default: + toResponse.AddHeader(headerKey, fromResponse.Headers[headerKey]); + break; + } } - catch (InvalidOperationException) - { - // Proxy is being used for an unsupported service (proxy.config has mustMatch="true") - HttpResponse response = HttpContext.Current.Response; - response.StatusCode = (int)System.Net.HttpStatusCode.Forbidden; - response.End(); + toResponse.ContentType = fromResponse.ContentType; + } + + private bool fetchAndPassBackToClient(System.Net.WebResponse serverResponse, HttpResponse clientResponse, bool ignoreAuthenticationErrors) { + if (serverResponse != null) { + copyHeaders(serverResponse, clientResponse); + using (Stream byteStream = serverResponse.GetResponseStream()) { + // Text response + if (serverResponse.ContentType.Contains("text") || + serverResponse.ContentType.Contains("json") || + serverResponse.ContentType.Contains("xml")) { + using (StreamReader sr = new StreamReader(byteStream)) { + string strResponse = sr.ReadToEnd(); + if ( + !ignoreAuthenticationErrors + && strResponse.IndexOf("{\"error\":{") > -1 + && (strResponse.IndexOf("\"code\":498") > -1 || strResponse.IndexOf("\"code\":499") > -1) + ) + return true; + clientResponse.Write(strResponse); + } + } else { + // Binary response (image, lyr file, other binary file) + + // 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(); + } } - catch (Exception e) + 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 request!"); + + if (method.Equals("POST")) { - if (e is ApplicationException) - throw e; - - // just return an empty string at this point - // -- may want to throw an exception, or add to a log file + String[] uriArray = uri.Split('?'); + + if (uriArray.Length > 1) + { + contentType = "application/x-www-form-urlencoded"; + String queryString = uriArray[1]; + + bytes = System.Text.Encoding.UTF8.GetBytes(queryString); + } } + + return doHTTPRequest(uri, bytes, method, PROXY_REFERER, contentType, credentials); + } + + private System.Net.WebResponse doHTTPRequest(string uri, byte[] bytes, string method, string referer, string contentType, System.Net.NetworkCredential credentials = null) + { + System.Net.HttpWebRequest req = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(uri); + req.ServicePoint.Expect100Continue = false; + req.Referer = referer; + req.Method = method; + + if (credentials != null) + req.Credentials = credentials; - return string.Empty; + if (bytes != null && bytes.Length > 0 || method == "POST") { + req.Method = "POST"; + req.ContentType = string.IsNullOrEmpty(contentType) ? "application/x-www-form-urlencoded" : contentType; + if (bytes != null && bytes.Length > 0) + req.ContentLength = bytes.Length; + using (Stream outputStream = req.GetRequestStream()) { + outputStream.Write(bytes, 0, bytes.Length); + } + } + return req.GetResponse(); } -} -[XmlRoot("ProxyConfig")] -public class ProxyConfig -{ - #region Static Members + 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 static object _lockobject = new object(); + 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)) + tokenServiceUri = getJsonValue(infoResponse, "tokenServiceUrl"); + 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 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"); + } - public static ProxyConfig LoadProxyConfig(string fileName) + private static void sendErrorResponse(HttpResponse response, String errorDetails, String errorMessage, System.Net.HttpStatusCode errorCode) { - ProxyConfig config = null; + 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; + //this displays our customized error messages instead of IIS's custom errors + response.TrySkipIisCustomErrors = true; + response.Write(message); + response.Flush(); + } - lock (_lockobject) + 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 { - if (System.IO.File.Exists(fileName)) + // 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] == '"' ? + value.Substring(1, value.IndexOf('"', 1) - 1): + value = value.Substring(0, Math.Max(0, Math.Min(Math.Min(value.IndexOf(","), value.IndexOf("]")), value.IndexOf("}")))); + } + return value; + } + + private void cleanUpRatemap(ConcurrentDictionary 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) { + string logMessage = string.Format("{0} {1}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), msg); + if (TraceLevel.Error == logLevel) + { + Trace.TraceError(logMessage); + logMessageToFile(logMessage); + } + else if (TraceLevel.Warning == logLevel) + { + Trace.TraceWarning(logMessage); + logMessageToFile(logMessage); + } + else + { + Trace.TraceInformation(logMessage); + logMessageToFile(logMessage); + } + } + + private static object _lockobject = new object(); + + private static void logMessageToFile(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(".\\")) { - XmlSerializer reader = new XmlSerializer(typeof(ProxyConfig)); - using (System.IO.StreamReader file = new System.IO.StreamReader(fileName)) + if (log.Contains(".\\")) //If this type of relative pathing .\log.txt { - config = (ProxyConfig)reader.Deserialize(file); + 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() - { + public static ProxyConfig GetCurrentConfig() { ProxyConfig config = HttpRuntime.Cache["proxyConfig"] as ProxyConfig; - if (config == null) - { - string fileName = GetFilename(HttpContext.Current); + if (config == null) { + string fileName = HttpContext.Current.Server.MapPath("proxy.config"); config = LoadProxyConfig(fileName); - - if (config != null) - { + if (config != null) { CacheDependency dep = new CacheDependency(fileName); HttpRuntime.Cache.Insert("proxyConfig", config, dep); } } - return config; } - public static string GetFilename(HttpContext context) + //referer + //create an array with valid referers using the allowedReferers String that is defined in the proxy.config + public static String[] GetAllowedReferersArray() { - return context.Server.MapPath("proxy.config"); + 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()); } - #endregion ServerUrl[] serverUrls; + public String logFile; bool mustMatch; + //referer + static String allowedReferers; [XmlArray("serverUrls")] [XmlArrayItem("serverUrl")] - public ServerUrl[] ServerUrls - { + public ServerUrl[] ServerUrls { get { return this.serverUrls; } - set { this.serverUrls = value; } + set + { + this.serverUrls = value; + } } - [XmlAttribute("mustMatch")] - public bool MustMatch - { + public bool MustMatch { get { return mustMatch; } - set { mustMatch = value; } + set + { mustMatch = value; } + } + + //logFile + [XmlAttribute("logFile")] + public String LogFile + { + get { return logFile; } + set + { logFile = value; } } - public string GetToken(string uri) + + //referer + [XmlAttribute("allowedReferers")] + public string AllowedReferers { - foreach (ServerUrl su in serverUrls) + get { return allowedReferers; } + set { - if (su.MatchAll && uri.StartsWith(su.Url, StringComparison.InvariantCultureIgnoreCase)) - { - return su.Token; - } - else - { - if (String.Compare(uri, su.Url, StringComparison.InvariantCultureIgnoreCase) == 0) - return su.Token; - } + allowedReferers = value; } + } - if (mustMatch) - throw new InvalidOperationException(); + 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]); - return string.Empty; + 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) + throw new ArgumentException("Proxy is being used for an unsupported service:"); + + return null; } + + } -public class ServerUrl -{ +public class ServerUrl { string url; bool matchAll; - string token; - + string oauth2Endpoint; + string domain; + string username; + string password; + string clientId; + string clientSecret; + string accessToken; + string tokenParamName; + string rateLimit; + string rateLimitPeriod; + [XmlAttribute("url")] - public string Url - { + public string Url { get { return url; } set { url = value; } } - [XmlAttribute("matchAll")] - public bool MatchAll - { + public bool MatchAll { get { return matchAll; } set { matchAll = value; } } - - [XmlAttribute("token")] - public string Token + [XmlAttribute("oauth2Endpoint")] + public string OAuth2Endpoint { + get { return oauth2Endpoint; } + set { oauth2Endpoint = value; } + } + [XmlAttribute("domain")] + public string Domain { - get { return token; } - set { token = value; } + get { return domain; } + set { domain = 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(); } } } diff --git a/viewer/proxy/proxy.config b/viewer/proxy/proxy.config old mode 100644 new mode 100755 index 7a2793f9a..ef689f898 --- a/viewer/proxy/proxy.config +++ b/viewer/proxy/proxy.config @@ -1,25 +1,9 @@ - - - - - - - - - - + + + + + + diff --git a/viewer/proxy/proxy.xsd b/viewer/proxy/proxy.xsd new file mode 100755 index 000000000..d6c057fa8 --- /dev/null +++ b/viewer/proxy/proxy.xsd @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/viewer/proxy/proxypage_java.zip b/viewer/proxy/proxypage_java.zip deleted file mode 100644 index a5d059be8f16a27b613d261c3281048a72e07df2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1061 zcmV+=1ls#hO9KQH000080H%QVJc;=Mg0TYt02>Ja00{s90B~||czG^rb8w|qU2oeq z6n(Y<|A!j^4W!hO6{jnTICj@%*;*`JGAHc{q<&!RYg>s#DkK$K8~oqLFIjS9Zw2-u zQTOBA%X|8PaW{M9`>k3YV9%|GYSau{99H?!ZwBx1?a8}e;al+nYTjR}%w z30m_}X!~I_i7D=E(BS`{{Pr$@?so#H-Ti)fa}I|<8K|g~0GUeUAm<1P5fWmFX{)*F zwVIaLR#WoAvSto%SqBmnyU?@;tOCodOzlx5_t#gz)r@cCY zt-5EYUW2JP)r*%<&Mc-ATwYFn@2#gdaCii7!Q1x~H^%Z3JcHQx)Y7b~YM+J|kQ-Er z3;pFVb$5;TO2o=^#wf+HR5y>3qn?KU%n@jIS%$PS8I3hmg5LJe>@t^GE`7~R-7&QK z{D^YSCVow=&JvhlE05BpYDlE=rpS~4HpbKd*u=f!swplD-*ZWaI?HRrE;v)pOlB9* z5U0qIZ(_VhGC^KBUE5T`*lDi6rR0SZo!ep=swHT4Q%&y!-56fOe5+H2B!yVm_eOcE0$dK$M^dA!(7MCLPGd?E(Cx3g{~?N^o|*7nAyFM|{Sr<$#$-cBHc021h@qN{ zigLM&d7L2le%w=T~;2Rt#b2z zJ-=~RkWON`3>622yX#SXtM>Ho)$p~HVU~;ePi#`yF?}=9iUoJvx6ZbH8Pt{M$apTs z4XSBH+cSoDllDEFZKrGpWAs1CvRqkW|CKN^`W1|c9MApp<{I000000000103ZMW000000B~|| fczG^rb8t{g1qJ{B000310RT4u007tn00000!6Ei+ diff --git a/viewer/proxy/proxypage_net.zip b/viewer/proxy/proxypage_net.zip deleted file mode 100644 index 0c13888ba42d7711fe0355454847e13cb9b3ae44..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3268 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 VG}a>}`*S1yy@=na^rxf%{sp|)4hjGO diff --git a/viewer/proxy/proxypage_php.zip b/viewer/proxy/proxypage_php.zip deleted file mode 100644 index d65094ee9c36c5efb72a768358592adae25b8aeb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1915 zcmV->2ZZ=gO9KQH000080Ju$$JJ+bQ@dpP00Id`N00{s90B~||czG^xXmG_?TW{J( z6n?MN|8R;>4K2aF?Mnzj*~Fw-r6C2ft13lkVh=Eiv0cx&DO&Zv?>Td^4Q+N?Db@0V z@tphR^JfkQ*(!Vd2*7{#a38V<&){lubQ;*}>z6ZFDV6Q}{()KvxtHbXm(5<0$A^Oh zmWNBcC3*f?gk0ZrYOj7y4O(|Xt>AJrnZivPZQ9Px>z}hTQ0XmC`bWPXp9I6xx7LdN zN>au5wMikNkUWCB6;EKnRk#w#5|oI!ghigNfq~>VJBQcGJqWMH=fJ-x zo=A~mplJ@5Z!ck8NQGv#6Bfr9Bn20zEaFiYrm4OJp*qewepqC}U!-xI-cdxcNHANK z!X_6#)w4W_=HkccU^PbvB9G!lyqWmXy8PVql8 z)c)qi-+Y;c80=`VaFLm>hx_=CAN``OJy3bU4=LR=i8p4*=ofWe3MytSa3~`hL$66= z&GV}~mY&mJuvi)cr7lZ0S{GU@=Tt_Jg}u0Ai6W6G)k8h78|%|olP2^X!?nUADd z0ghlr&N3c~1x_pNDNZVuckQgLDgC1uP4;W8aZylVZ6vtKOOYfruNni%9onl+X!)tN z$QjbTCKYE*t(d~iW1+$W9RxSPgz351G)8$QQ-fD%5!Wm(BCc0%2mCr978G&^Qp)>I zNDeV)pAmx8_u}O1Ol|7(P)!?pWO>duEq!dSZSDJq;90PjY@Hz(YqghM1N4|G?sFHx zz&Gvib4=KG9rwM?R@D#nn~x z(lPmM_Z}K=d^D+_~s{-%&9(yr!h=$v?8mI;)rd&6l%>+Nxj z?%LVmnO?76;O){j70Z`gnR#!{C&Bp7V0`VpzY4}5=9B68?Bdij>!Axl} zp;A)&en%ocHc#eyWvAMjrVX~5mP%9wy^?HMR>~qKm1g@bi0U!Wx3xug>t8d^SgzE4jDrOSw?@OkL$r;V-7qg1a?F^_RA(WWo9z=C4i?TaL$I_Ly z;))egB4nVuU39HTM5i8@n^|p4YxXV3);9RtX1%U8>$PdtFwJj76!D~lj(eJALvpP; zP7_(gN~aj5VX@|kGVGC7rba}GQQJLHl~Lke*pR>#|CXn)3=u4 zXKly#E!S(B(|(l(A)z*?FS>2#1Lxuo^gA=_(u)KT{lGgPZxA2@oG*29!xQL|hyd_74sDz~ia zomV^8bcfr_^B7A%x}46b14tWciEW?14USHNF^T9jcTcC&%SxhdABhUi1p<@Wn{G1P zWDmMEpgA60Oz~xZKK*bR;N!K_8S@3tdA=)7e&*v~7>t9lH#5@homj@f^lE%D9Uomx zh9(|8-`hgN(fRr4uk+FI(e!L|;e<$LtH?;Xbdk%~ZS&_vwc+$9jjk?Eg*qXDzQ*=B z7I7Y$#AU02aE!Pm60~R8gQW$?R9dBO8xCt+z%}gJ#Y{oW>Tiy*~EO0Z_P+>9tQe_1q<^=Bx>-E8tNJS?qPzzox