diff --git a/lib/resource-proxy/.htaccess b/lib/resource-proxy/.htaccess new file mode 100644 index 0000000..0880f65 --- /dev/null +++ b/lib/resource-proxy/.htaccess @@ -0,0 +1,14 @@ + + Order allow,deny + Deny from All + + + + Order allow,deny + Deny from all + + + + Order allow,deny + Deny from all + \ No newline at end of file diff --git a/lib/resource-proxy/README.md b/lib/resource-proxy/README.md new file mode 100644 index 0000000..ec55f87 --- /dev/null +++ b/lib/resource-proxy/README.md @@ -0,0 +1,384 @@ +PHP Proxy File +================= + +A PHP proxy that handles support for +* Accessing cross domain resources +* Requests that exceed 2048 characters +* Accessing resources secured with token based authentication. +* [OAuth 2.0 app logins](https://developers.arcgis.com/en/authentication). +* Enabling logging +* Both resource and referer based rate limiting + +##Instructions + +* Download and unzip the .zip file, or clone the repository. +* Install the contents of the PHP folder by adding all files into a web directory. +* Test that the proxy is able to forward requests directly in the browser using: +``` +http://[yourmachine]/PHP/proxy.php?http://services.arcgisonline.com/ArcGIS/rest/services/?f=pjson +``` +* Edit the proxy.config file in a text editor to set up your proxy configuration settings. +* Update your application to use the proxy for the specified services. In this JavaScript example requests to route.arcgis.com will utilize the proxy. + +``` + urlUtils.addProxyRule({ + urlPrefix: "route.arcgis.com", + proxyUrl: "http://[yourmachine]/PHP/proxy.php" + }); +``` +* Security tip: By default, the proxy.config allows any referrer. To lock this down, replace the ```*``` in the ```allowedReferers``` property with your own application URLs. +* Security tip: Verify that the ```proxy.config``` file is not accessible via the Internet and that the PHP server is configured correctly. To verify the proxy setup, open ```http://[yourmachine]/PHP/proxy-verification.php``` in a web browser and follow the instructions. + + +##Proxy Configuration Settings + +* Use the ProxyConfig tag to specify the following proxy level settings. + * **mustMatch="true"** : When true only the sites listed using serverUrl will be proxied. Set to false to proxy any site, which can be useful in testing. However, we recommend setting it to "true" for production sites. + * **logFile=""** : When a path to a local file is specified event messages will be logged. + * **allowedReferers="http://server.com/application1,https://server.com/application2"**: A list of referer URLs. Only requests coming from referers in the list will be proxied. +* Add a new \ entry for each service that will use the proxy page. 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="true"). + * **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. + * **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/. + * **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). + +##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. +* proxy.php: The actual proxy application. In most cases you will not need to modify this file. + +Other useful files in the repo: +* .htaccess: This file is an example Apache web server file which includes recommended file filtering. +* proxy-verification.php: Useful testing page if you have installation problem. + +Files created by the proxy: +* proxy.sqlite: This file is created dynamically after proxy.php runs. This file supports rate metering. +* proxy_log.log: This file is created when the proxy.php runs (and logging is enabled). + +##Requirements + +* PHP 5.4.2 (recommended) +* cURL PHP extension +* OpenSSL PHP extension +* PDO_SQLITE PDO PHP extension + +### Example Configurations + +The PHP proxy supports both XML and JSON configurations. +XML is the default. +To change the default you must switch ````$proxyConfig->useXML();```` to ````$proxyConfig->useJSON();```` at the bottom of the proxy.php file. +No matter what style configuration chosen, always save the configuration as ```proxy.config```. +When using this proxy for testing or research and development you may want to add ```*``` to the ```allowedReferers``` property. +However, using ```*``` in production is not recommended. +In order to test the proxy like below make sure to add a ```*``` to the ```allowedReferers``` property. +Note, the example configuration file contains the ```*``` within ```allowedReferers```. + +``` +http://[yourmachine]/PHP/proxy.php?http://[machineyouknow]/arcgis/rest/services +``` + +XML example + +``` + + + + + + + + + + + + + + + + +``` + +JSON example + +``` +{ + "proxyConfig": [ + { + "mustMatch": true, + "logFile": "proxy_log_json.log", + "allowedReferers":["http://server.com/application1","https://server.com/application2","*"] + } + ], + "serverUrls": [ + { + "serverUrl" : [ + { + "url": "http://sampleserver6.arcgisonline.com", + "username": "username", + "password": "password", + "rateLimit": "120", + "rateLimitPeriod": "60", + "matchAll": true + } + ] + }, + { + "serverUrl" : [ + { + "url": "geoenrich.arcgis.com", + "username": "username", + "password": "password", + "rateLimit": "120", + "rateLimitPeriod": "60", + "matchAll": true + } + ] + }, + { + "serverUrl" : [ + { + "url": "http://route.arcgis.com", + "oauth2Endpoint": "https://www.arcgis.com/sharing/oauth2", + "clientId": "6Xo1dcx5L1tz7k9Kn2", + "clientSecret": "5a5d50d0e6fe422c867b6efcf969bdcc6a2", + "rateLimit": "120", + "rateLimitPeriod": "60", + "matchAll": true + } + ] + }, + { + "serverUrl" : [ + { + "url": "http://services.arcgisonline.com/ArcGIS/rest/services/", + "rateLimit": "120", + "rateLimitPeriod": "60", + "matchAll": false + } + ] + } + ] +} +``` + +## Guide for Unix and Mac using Apache's HTTP Server + +### I see my configuration file on the Internet after running the verification test + +This is a problem because ```proxy.config``` may contain sensitive credentials. To resolve this, enable file filtering on your web server. Apache users can do this by adding + the following lines to the ```.htaccess``` file. Note in order to use ```.htaccess``` files you must enable +```mod_rewrite.so``` in your ```httpd.conf``` and change the server directory so that ```AllowOverride``` is set to ```All```. The example below also +shows how to filter ```.sqlite``` files. Sqlite is used to implement Rate Metering in this proxy. It's a good idea to filter ```.sqlite``` and ```.log``` files as well. + +Example .htaccess file: + +``` + + Order allow,deny + Deny from All + + + + Order allow,deny + Deny from all + + + + Order allow,deny + Deny from all + +``` + +### When I ran the verification test my browser displays raw PHP code + +Raw PHP code in the browser indicates that the PHP server is not configured correctly. +If you see this, troubleshoot the Apache ```httpd.conf``` file. On Mac, typically your built in Apache httpd.conf file is located here ```/private/etc/apache2/httpd.conf```. +On a Linux machine sometimes the ```httpd.conf``` file is located in ```etc/httpd/conf```. +A properly configured httpd.conf file, looks something like below. Note that the php_5module +is uncommented and libphp5.so is present in the modules directory. More help [here](http://php.net/manual/en/install.macosx.bundled.php). + + +In addition Apache must be told to accept .php file extensions. This can also be done by adding the below +lines in your ```httpd.conf```. + + +``` + + AddType application/x-httpd-php .php + AddType application/x-httpd-php-source .phps + + + DirectoryIndex index.html index.php + + +``` + +On a Mac an alternative is to uncommented the line that says ```Include /private/etc/apache2/other/*.conf``` in the ```https.conf``` file. + +###PHP version check has failed + +This is because the minimum proxy requirements are set to PHP version 5.4.2. If this fails, the recommendation is to install the most recent stable PHP release. +Other PHP versions may work, however PHP version 5.4.2 contains important security updates. + +### My directory is not writable + +To resolve this you'll need to change the directory to read/write where the ```proxy-verification.php``` file is located. Changing permission on a folder typically requires administrative permissions to make the change. The reason read/write is needed is because this proxy writes to a log and to a sqlite file. + +### OpenSSL, PDO Sqlite and cURL failed + +OpenSSL, PDO Sqlite and cURL are PHP extensions. Depending on how you installed PHP there may be a wizard to install PHP extensions or you may need to manually download / build and configure the required extensions. This guide discusses both options to obtain OpenSSL, PDO Sqlite and Curl. + +To build and configure the required extensions yourself, download the PHP source code from http://php.net and then compile it using the below commands. Since the default version of PHP comes with Sqlite there is no need to issue a command to install this extension. For cURL specific help go [here](http://www.php.net/manual/en/curl.installation.php). + +``` +./configure --with-apxs2=/usr/local/server/sept13/httpd-2.4.6/bin/apxs --with-openssl --enable-shmop --enable-mbstring --with-curl[/usr/curl] + +make + +sudo make install +``` +For more detailed instructions use this [help on building PHP with extensions](http://www.php.net/manual/en/install.unix.apache2.php). + +Alternatively (if you don't want to compile PHP yourself), there are plenty of good 3rd party Apache / PHP products that come with many PHP extensions and a standalone Apache web server. + +You may want to try out these solutions: + +* [Zend Server Project](http://www.zend.com/en/products/server/index) +* [MAMP](http://www.mamp.info/en/index.html) + +Note, on Linux the ```configure```, ```make```, ```make install``` may likely be the shortest path to success to resolve missing extensions. + + +### I see errors regarding dates and times + +Make sure you've set the ```date.timezone``` value in the ```php.ini``` file. For valid PHP timezones, see this [documentation](http://php.net/manual/en/timezones.php). The ```proxy-verification.php``` page +makes an attempt to output the location of the ```php.ini``` used by the PHP server. + + +### Why do I see fail messages after adding extensions? + +Anytime you make a configuration change to PHP ```php.ini``` or Apache ```httpd.conf``` you must do an Apache restart for those changes to take effect. Restarting can be accomplished +by finding the Apache executable and then issuing the proper command in the terminal to ```start```, ```stop```, ```restart```. See example, below. Proper commands are ```start```, ```stop``` and +```restart```. In this example, the Apache executable is ```apachectl```. + +``` +sudo /usr/sbin/apachectl restart +``` + +### When requesting Tiled Map services via the proxy slower performance is observed then when not using the proxy + +Proxies are helpful for overcoming certain browser limitations, requests that exceed 2048 characters, accessing resources secured with +token based authentication and ideal for implementing features like rate limiting. However proxies do add a little overhead. If overhead is a concern, +it's recommended to use proxy rules and avoid such things like ```esri.config.defaults.io.alwaysUseProxy```. Alternatively, consider +implementing Cross-origin resource sharing ```CORS``` on the application server. + +###Where do I get clientId and clientSecret credentials to leverage OAuth2? + +There are several ways to obtain these credentials. Credentials can be created by signing into [ArcGIS for Developers](https://developers.arcgis.com) and clicking ```Applications``` then ```Create an Application```. Another option is to sign into [ArcGIS Online](https://arcgis.com) click ```My Content``` then click ```Add Item``` to go through the steps to add an application. Once the application has been added click the item to ```View item details``` and click ```Register``` within the App Registration section. Tip: OAuth2 workflows contain a variety of value added features for distributing apps, accessing billable services, and getting usage reports. + + + +## Guide for Windows using IIS + +### I see my configuration file on the Internet after running the verification test + +This is a problem because ```proxy.config``` may contain sensitive credentials. The solution to this problem is update IIS so that ```.config``` and ```.sqlite``` file types will be filtered and not be shown to the end user. +Please refer to the IIS help documentation explaining +[how to set up request filtering on IIS](http://www.iis.net/configreference/system.webserver/security/requestfiltering/fileextensions). + + +### When I ran the verification test my browser displays raw PHP code + +When PHP is not installed or configured correctly this is the symptom. On Windows there are several good 3rd party utilities that make installing and configuring PHP easy. +The utilities worth looking into are: + +* [Microsoft's Web Platform Installer] (http://www.microsoft.com/web/downloads/platform.aspx) +* [Zend Server Project](http://www.zend.com/en/products/server/index) +* [WAMP](http://www.wampserver.com/) + +Use the help documentation included in these products to get your PHP server up and running on Windows. + +###PHP version check has failed + +This is because the minimum proxy requirements are set to PHP version 5.4.2. If this fails, the recommendation is to install the most recent stable PHP release. +Other PHP versions may work, however PHP version 5.4.2 contains important security updates. + + +### My directory is not writable + +To resolve this you'll need to change the directory to read/write where the ```proxy-verification.php``` file is located. In most cases, administrative permissions are needed to change file permission levels. The reason read/write is needed +is because this proxy writes to a log and to a sqlite file. This is usually accomplished by right clicking on the folder +to open the ```properties``` dialog box and clicking the ```security``` tab. Use the ```name list box```, select the user, contact, computer, or group whose permissions you want to make writable and assign ```write``` permissions. + +### OpenSSL, PDO Sqlite and cURL failed + +If you've chosen to install PHP using either of these utilities, OpenSSL and PDO Sqlite are a part of the PHP install. However, these extensions need to be activated. You can do this by uncommenting these lines in the ```php.ini``` file. + +``` extension=php_openssl.dll ``` +``` extension=php_pdo.dll ``` +``` extension=php_curl.dll ``` + +After modifying ```php.ini``` restart IIS and run the ```proxy-verification.php``` again. + +### When requesting Tiled Map services via the proxy slower performance is observed then when not using the proxy + +Proxies are helpful for overcoming certain browser limitations, requests that exceed 2048 characters, accessing resources secured with +token based authentication and ideal for implementing features like rate limiting. However proxies do add a little overhead. If overhead is a concern, +it's recommended to use proxy rules and avoid such things like ```esri.config.defaults.io.alwaysUseProxy```. Alternatively, consider +implementing Cross-origin resource sharing ```CORS``` on the application server. + +###Where do I get clientId and clientSecret credentials to leverage OAuth2? + +There are several ways to obtain these credentials. Credentials can be created by signing into [ArcGIS for Developers](https://developers.arcgis.com) and clicking ```Applications``` then ```Create an Application```. Another option is to sign into [ArcGIS Online](https://arcgis.com) click ```My Content``` then click ```Add Item``` to go through the steps to add an application. Once the application has been added click the item to ```View item details``` and click ```Register``` within the App Registration section. Tip: OAuth2 workflows contain a variety of value added features for distributing apps, accessing billable services, and getting usage reports. + + +##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 2013 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/lib/resource-proxy/proxy-verification.php b/lib/resource-proxy/proxy-verification.php new file mode 100644 index 0000000..e18da2c --- /dev/null +++ b/lib/resource-proxy/proxy-verification.php @@ -0,0 +1,208 @@ + +Prerequisites Checker + + + $path) { + + $rp = $path . DIRECTORY_SEPARATOR . $fileName; + + if (is_file($rp)) { + + if($fileName == 'PEAR' . DIRECTORY_SEPARATOR .'Registry.php'){ + + $path = $rp; + } + + if($fileName == 'HTTP' . DIRECTORY_SEPARATOR . 'Request2.php'){ + + $path = $rp; + } + + return array('result' => true, 'path' => $path); + } + } + + return false; +} + +function pear_prerequisutes() +{ + $exists = include_exists('PEAR' . DIRECTORY_SEPARATOR .'Registry.php'); + if($exists['result']) + { + require_once 'PEAR/Registry.php'; + $reg = new PEAR_Registry(); + $packages = $reg->listPackages(); + if(in_array('http_request2', $packages)) + { + $results = array('pear'=>"Pass", 'http_request2'=>"Pass", 'path'=>$exists['path']); + }else{ + $results = array('pear'=>"Pass", 'http_request2'=>"Fail", 'path'=>$exists['path']); + } + }else{ + $results = array('pear'=>"Fail", 'http_request2'=>"Fail", 'path'=>$exists['path']); + } + return $results; +} + +function http_requests2_exists() +{ + $exists = include_exists('HTTP' . DIRECTORY_SEPARATOR . 'Request2.php'); + + if($exists['result']) + { + return array('result' => "Pass", 'path'=> $exists['path']); + + }else{ + + return array('result' => "Fail", 'path'=> $exists['path']); + + } +} + +function can_write() +{ + $dir = getcwd(); + $t = is_writable($dir); + return $t; +} + +function getShmopMessage($shmop) +{ + $message = null; + if($shmop == true) + { + $message = "Pass"; + }else if($shmop == false){ + $message = "Warning"; + }else{ + $message = "Fail"; + } + return $message; +} + +function getMbstringMessage($mbstring) +{ + $message = null; + if($mbstring == true) + { + $message = "Pass"; + }else if($mbstring == false){ + $message = "Warning"; + }else{ + $message = "Fail"; + } + return $message; +} + +function versionPhpCheck() +{ + if (version_compare(PHP_VERSION, '5.4.2') >= 0) { + return "Pass"; + }else{ + return "Fail"; + } +} + +$bool = array(true=>"Pass", false=>"Fail"); +$version = versionPhpCheck(); +$extensions = get_loaded_extensions(); +$writeable = can_write(); +$openssl = in_array('openssl', $extensions); +$pdo_sqlite = in_array('pdo_sqlite', $extensions); +$curl = in_array('curl', $extensions); + + +?> +

Checks php proxy requirements

+* Run this page from a web server directory that includes the proxy.php file
+* It's recommended to remove this file from web server once all below tests pass +

+

Manually test the web server's configuration by clicking here. If the proxy configuration file (proxy.config) displays or gets downloaded, the server is not configured properly to use this proxy (see guide for more details).

+ + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Check PHP version 5.4.2 or newer?' . $version . ''; ?>
Check if directory is writable?' . $bool[$writeable] . ''; ?>
Check for OpenSSL extension?' . $bool[$openssl] . ''; ?>
Check for PDO Sqlite extension?' . $bool[$pdo_sqlite] . ''; ?>
Check for Curl?' . $bool[$curl] . ''; ?>
+ +
+

" . $inipath . ""; ?>

+
+ +
+ + \ No newline at end of file diff --git a/lib/resource-proxy/proxy.config b/lib/resource-proxy/proxy.config new file mode 100644 index 0000000..f60d994 --- /dev/null +++ b/lib/resource-proxy/proxy.config @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/lib/resource-proxy/proxy.php b/lib/resource-proxy/proxy.php new file mode 100644 index 0000000..316a22e --- /dev/null +++ b/lib/resource-proxy/proxy.php @@ -0,0 +1,2394 @@ +"True", false=>"False"); + + /** + * Holds staged file path. + * + * @var string + */ + + private $unlinkPath; + + + + public function __construct($configuration, $log) { + + + $this->proxyLog = $log; + + $this->proxyConfig = $configuration->proxyConfig; + + $this->serverUrls = $configuration->serverUrls; + + $this->setupSession(); + + $this->setupClassProperties(); + + if ($this->proxyConfig['mustmatch'] != null && $this->proxyConfig['mustmatch'] == true || $this->proxyConfig['mustmatch'] == "true") { + + if($this->isAllowedApplication() == false){ + + $this->allowedApplicationError(); + + } + + $this->verifyConfiguration(); + + if ($this->meter->underMeterCap()) { + + $this->runProxy(); + + } else { + + $this->rateMeterExceededError(); + + } + + } else if($this->proxyConfig['mustmatch'] != null && $this->proxyConfig['mustmatch'] == false) { + + $this->runProxy(); + + }else{ + + $this->configurationParameterError(); + + } + + } + + public function setupSession() + { + if (!isset($_SESSION)) { + + session_start(); + + } + + } + + public function makeDirectory($dir, $mode = 0777) //Not implemented. + { + if (is_dir($dir) || @mkdir($dir,$mode)) return true; + + if (!$this->makeDirectory(dirname($dir),$mode)) return false; + + return @mkdir($dir,$mode); + + } + + public function verifyConfiguration() + { + if (!isset($this->resource['ratelimit']) || !isset($this->resource['ratelimitperiod'])) { + + $this->resource['ratelimit'] = null; + + $this->resource['ratelimitperiod'] = null; + } + + if ($this->canProcessRequest()) { + + $this->meter = new RateMeter($this->resource['url'], $this->resource['ratelimit'], $this->resource['ratelimitperiod'], $this->proxyLog); //need to pass meter interval and meter cap + + } else { + + $this->canProcessRequestError(); + + } + } + + public function configurationParameterError() + { + + $this->proxyLog->log("Malformed 'mustMatch' property in config"); + + header('Status: 200', true, 200); + + header('Content-Type: application/json'); + + $configError = array( + "error" => array("code" => 412, + "details" => array("Detected malformed 'mustMatch' property in configuration. The server does not meet one of the preconditions that the requester put on the request."), + "message" => "Proxy failed due to configuration error." + )); + + echo json_encode($configError); + + exit(); + } + + public function rateMeterExceededError() + { + + $this->proxyLog->log("Rate meter exceeded by " . $_SERVER['REMOTE_ADDR']); + + header('Status: 200', true, 200); + + header('Content-Type: application/json'); + + $exceededError = array( + "error" => array("code" => 402, + "details" => array("This is a metered resource, number of requests have exceeded the rate limit interval."), + "message" => "Unable to proxy request for requested resource." + )); + + echo json_encode($exceededError); + + exit(); + } + + public function canProcessRequestError() + { + + $this->proxyLog->log("Proxy could not resolve requested url - " . $this->proxyUrl . ". Possible solution would be to update 'mustMatch', 'matchAll' or 'url' property in config."); + + header('Status: 200', true, 200); + + header('Content-Type: application/json'); + + $configError = array( + "error" => array("code" => 412, + "details" => array("The proxy tried to resolve a url that was not found in the configuration file. Possible solution would be to add another serverUrl into the configuration file or look for typos in the configuration file."), + "message" => "Proxy failed due to configuration error." + )); + + echo json_encode($configError); + + exit(); + } + + public function allowedApplicationError() + { + + header('Status: 200', true, 200); + + header('Content-Type: application/json'); + + $allowedApplicationError = array( + "error" => array("code" => 402, + "details" => array("This is a protected resource. Application access is restricted."), + "message" => "Application access is restricted. Unable to proxy request." + )); + + echo json_encode($allowedApplicationError); + + exit(); + } + + public function setProxyHeaders() + { + $header_size = curl_getinfo($this->ch, CURLINFO_HEADER_SIZE); + + $header_content = trim(substr($this->response,0, $header_size)); + + $headers = preg_split( '/\r\n|\r|\n/', $header_content); + + $this->headers = $headers; + + $hasHeaders = (boolean)$headers; + + if($hasHeaders){ + + foreach($this->headers as $key => $value) { + + if (stripos($value,'ETag:') !== false || stripos($value,'Content-Type:') !== false + || stripos($value,'Connection:') !== false || stripos($value,'Pragma:') !== false + || stripos($value,'Expires:') !== false) { + + header($value); //Sets the header + } + } + + }else{ + + header("Content-Type: text/plain;charset=utf-8"); //If preg_split does not evaulate use text/plain + } + } + + public function setResponseBody() + { + + $header_size = curl_getinfo($this->ch,CURLINFO_HEADER_SIZE); + + $this->proxyBody = substr($this->response, $header_size); + + } + + public function getResponse() + { + + echo $this->proxyBody; + + $this->proxyLog->log("Proxy complete"); + + $this->proxyConfig = null; + + $this->meter = null; + + $this->proxyLog = null; + + exit(); + } + + + public function setupClassProperties() + { + + try { + + if (!empty($_POST) && empty($_FILES)) { // Is it a POST without files? + + $this->proxyLog->log('POST detected'); + + $this->proxyUrl = $_SERVER['QUERY_STRING']; + + $this->proxyData = $_POST; + + $this->proxyMethod = "POST"; + + } else if (!empty($_POST) && !empty($_FILES)) { // Is it a POST with files? + + $this->proxyLog->log('FILES detected'); + + $this->proxyUrl = $_SERVER['QUERY_STRING']; + + $this->proxyData = $_POST; + + $this->proxyMethod = "FILES"; + + } else if (empty($_POST) && empty($_FILES)) { // It must be a GET! + + $this->proxyLog->log('GET detected'); + + $p = preg_split("/\?/", $_SERVER['QUERY_STRING']); // Finds question marks in query string + + $this->proxyUrl = $p[0]; + + $this->proxyUrlWithData = $_SERVER['QUERY_STRING']; + + $this->proxyMethod = "GET"; + } + + } catch (Exception $e) { + + + $this->proxyLog->log("Proxy could not detect request method action type (POST, GET, FILES)."); + } + + } + + public function formatWithPrefix($url) + { + if(substr($url, 0, 4) != "http"){ + + if(substr($this->proxyUrl, 0, 5) == "https") { + + $url = "https://" . $url; + + }else{ + + $url = "http://" . $url; + } + + } + + return $url; + } + + public function removeTrailingSlash($url) + { + if (substr($url, -1) == '/') + { + $url = rtrim($url, "/"); + } + + return $url; + + } + + public function resolveDoubleSlashCondition($url) + { + if (substr($url, 0, strlen("//")) == "//") { + + $url = substr($url, strlen("//")); + } + + return $url; + + } + + public function sanitizeUrl($url) + { + $url = $this->resolveDoubleSlashCondition($url); + + $url = $this->formatWithPrefix($url); + + $url = $this->removeTrailingSlash($url); + + return $url; + + } + + public function canProcessRequest() + { + $canProcess = false; + + if ($this->proxyConfig['mustmatch'] == false || $this->proxyConfig['mustmatch'] === "false") { + + $canProcess = true; + + } else if ($this->proxyConfig['mustmatch'] == true || $this->proxyConfig['mustmatch'] == "true") { + + foreach ($this->serverUrls as $key => $value) { + + $s = $value['serverurl'][0]; + + $s['url'] = $this->sanitizeUrl($s['url']); //Do all the url cleanups and checks at once + + if(is_string($s['matchall'])){ + + $mustmatch = strtolower($s['matchall']); + + $s['matchall'] = $mustmatch; + + } + + if ($s['matchall'] == true || $s['matchall'] === "true") { + + $urlStartsWith = $this->startsWith($this->proxyUrl, $s['url']); + + if ($urlStartsWith){ + + $this->resource = $s; + + $this->sessionUrl = $s['url']; + + $canProcess = true; + + return $canProcess; + + } + + } else if ($s['matchAll'] == false || $s['matchall'] === "false"){ + + $isEqual = $this->equals($this->proxyUrl, $s['url']); + + if($isEqual){ + + $this->resource = $s; + + $this->sessionUrl = $s['url']; + + $canProcess = true; + + return $canProcess; + } + + } + } + + } else { + + $this->proxyLog->log("Proxy has failed. Review configuration file for errors."); + + $canProcess = false; + } + + return $canProcess; + } + + + public function getRequestConfig() + { + + return $this->requestConfig; + + } + + public function useSessionToken() + { + + $sessonKey = 'token_for_' . $this->sessionUrl; + + $sessonKey = sprintf("'%s'", $sessonKey); + + if(isset($_SESSION[$sessonKey])) //Try to get token from session + { + + $token = $_SESSION[$sessonKey]; + + $this->appendToken($token); + + $this->sessionAttempt = true; + + $this->proxyLog->log("Using session token"); + + } + } + + public function runProxy() + { + + $this->useSessionToken(); + + if($this->proxyMethod == "FILES"){ + + $this->proxyFiles(); + + + }else if($this->proxyMethod == "POST"){ + + $this->proxyPost(); + + }else if($this->proxyMethod == "GET"){ + + $this->proxyGet(); + + } + + $isUnauthorized = $this->isUnauthorized(); + + if($isUnauthorized === true) { + + if($this->attemptsCount < $this->allowedAttempts) { + + $this->attemptsCount++; + + $this->proxyLog->log("Retry attempt " . $this->attemptsCount . " of " . $this->allowedAttempts); + + $token = $this->getNewTokenIfCredentialsAreSpecified(); + + if(!empty($token) || $token != null) { + + $this->addTokenToSession($token); + + $this->appendToken($token); + } + + if($this->attemptsCount == $this->allowedAttempts) { + + $this->proxyLog->log("Removing session value"); + + $sessonKey = 'token_for_' . $this->sessionUrl; + + $sessonKey = sprintf("'%s'", $sessonKey); + + unset($_SESSION[$sessonKey]); //Remove token from session + } + + $this->runProxy(); + } + + } else { + + $this->proxyLog->log("Ok to proxy"); + } + + return true; + } + + public function isUnauthorized() + { + + $isUnauthorized = false; + + $jsonData = json_decode($this->proxyBody); + + if (strpos($this->proxyBody,'"code":499') !== false) { + + $isUnauthorized = true; + + } + + if (strpos($this->proxyBody,'"code":498') !== false) { + + $isUnauthorized = true; + + } + + $errorCode = $jsonData->{'error'}->{'code'}; + + if($errorCode == 499 || $errorCode == 498) + { + $isUnauthorized = true; + + } + + if($isUnauthorized){ + + $this->proxyLog->log("Authorization failed : " . $this->proxyBody); + + } + + return $isUnauthorized; + } + + private function appendToken($token) + { + if($this->proxyMethod == 'POST' || $this->proxyMethod == 'FILES') + { + + if(array_key_exists("token", $this->proxyData)) + { + $this->proxyData["token"] = $token; + + }else{ + + $appendedToken = array_merge($this->proxyData, array("token" => $token)); + + $this->proxyData = $appendedToken; + + } + + }else{ + + $pos = strripos($this->proxyUrlWithData, "&token="); + + if($pos > 0) + { + + $this->proxyUrlWithData = substr($this->proxyUrlWithData,0,$pos) . "&token=" . $token; //Remove old tokens. + + }else{ + + $this->proxyUrlWithData = $this->proxyUrlWithData . "&token=" . $token; + + } + + } + } + + public function initCurl() + { + $headers = array('Expect:', 'Referer: ' . $this->referer); + + $this->ch = curl_init(); + + curl_setopt($this->ch, CURLOPT_HTTPHEADER, $headers); + + curl_setopt($this->ch, CURLOPT_SSL_VERIFYPEER , false); + + curl_setopt($this->ch, CURLOPT_SSL_VERIFYHOST , false); + + curl_setopt($this->ch, CURLOPT_HEADER, true); + + } + + + + public function proxyGet() { + + $this->response = null; + + try { + + $this->initCurl(); + + curl_setopt($this->ch, CURLOPT_HTTPGET, true); + + curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true); + + curl_setopt($this->ch, CURLOPT_URL, $this->proxyUrlWithData); + + $this->response = curl_exec($this->ch); + + if(curl_errno($this->ch) > 0 || empty($this->response)) + { + $this->proxyLog->log("Curl error or no response: " . curl_error($this->ch)); + + }else{ + + $this->setProxyHeaders(); + + $this->setResponseBody(); + + } + + curl_close($this->ch); + + $this->ch = null; + + + } catch (Exception $e) { + + $this->proxyLog->log($e->getMessage()); + } + + return; + } + + public function proxyPost($url, $params) + { + $this->response = null; + + $this->headers = null; + + $this->proxyBody = null; + + if(empty($url) || $url == null || empty($params) || $url == $params){ //If no $url or $params passed, defaut to class property values + + $url = $this->proxyUrl; + + $params = $this->proxyData; + + } + + if(is_array($params)){ //If $params is array, convert it to a curl query string like 'image=png&f=json' + + $params = $this->build_http_query($params); + } + + try { + + $this->initCurl(); + + curl_setopt($this->ch, CURLOPT_URL, $url); + + curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true); + + curl_setopt($this->ch, CURLOPT_POST, true); + + curl_setopt($this->ch, CURLOPT_POSTFIELDS, $params); + + $this->response = curl_exec($this->ch); + + } catch (Exception $e) { + + $this->proxyLog->log($e->getMessage()); + } + + if(curl_errno($this->ch) > 0 || empty($this->response)) + { + $this->proxyLog->log("Curl error or no response: " . curl_error($this->ch)); + + }else{ + + $this->setProxyHeaders(); + + $this->setResponseBody(); + } + + curl_close($this->ch); + + $this->ch = null; + + return; + } + + public function proxyFiles() + { + try { + + if (count($this->proxyData)) + { + $query_array = array(); + + foreach ($this->proxyData as $pkey => $pvalue) + { + $query_array[$pkey] = $pvalue; + + foreach ($_FILES as $key => $file) + { + $parts = pathinfo($file["tmp_name"]); + + $this->unlinkPath = $parts["dirname"] . DIRECTORY_SEPARATOR . $file["name"]; + + rename($file["tmp_name"], $this->unlinkPath); + + $this->proxyData[$key] = "@" . $this->unlinkPath; + + $query_array[$key] = "@" . $this->unlinkPath; + + } + + } + + } + + $this->initCurl(); + + curl_setopt($this->ch, CURLOPT_URL, $this->proxyUrl); + + curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true); + + curl_setopt($this->ch, CURLOPT_POST, true); + + curl_setopt($this->ch, CURLOPT_POSTFIELDS, $query_array); + + $this->response = curl_exec($this->ch); + + if(curl_errno($this->ch) > 0 || empty($this->response)) + { + $this->proxyLog->log("Curl error or no response: " . curl_error($this->ch)); + }else{ + + $this->setProxyHeaders(); + + $this->setResponseBody(); + + if($this->isUnauthorized() == true) { + + $this->proxyLog->log("Unlinking: " . $this->unlinkPath); + + unlink($this->unlinkPath); + + } + + } + + curl_close($this->ch); + + $this->ch = null; + + return; + + } catch (Exception $e) { + + $this->proxyLog->log($e->getMessage()); + } + + } + + public function build_http_query($query) //Support for older PHP versions here + { + $query_array = array(); + + foreach($query as $key => $value ){ + + $query_array[] = $key . '=' . $value; + + } + + return implode( '&', $query_array); + + } + + function startsWith($requested, $needed) + { + + return stripos($requested, $needed) === 0; + + } + + function contains($haystack, $needle) + { + + return stripos($haystack, $needle) !== false ? true : false; + + } + + function equals($string, $anotherstring) + { + + return $string == $anotherstring; + + } + + + public function isUserLogin() + { + + if (isset($this->resource['username']) && isset($this->resource['password'])) { + + return true; + } + + return false; + } + + public function isAppLogin() + { + + if (isset ( $this->resource['clientid']) && isset ($this->resource['clientsecret'])) { + + return true; + + } + + return false; + } + + public function exchangePortalTokenForServerToken($portalToken) { + + $this->proxyLog->log("Exchanging portal token for server-specific token for " . $this->resource['url']); + + $pos = strripos($this->resource['oauth2endpoint'], "/oauth2"); + + $exchangeUri = substr($this->resource['oauth2endpoint'],0,$pos) . "/generateToken"; + + $this->proxyPost($exchangeUri, array( + 'token' => $portalToken, + 'serverURL' => $this->resource['url'], + 'f' => 'json' + )); + + $tokenResponse = json_decode($this->proxyBody, true); + + $token = $tokenResponse['token']; + + return $token; + + + } + + function getNewTokenIfCredentialsAreSpecified() { + + $this->sessionUrl = $this->resource['url']; //Store url in local variable b/ later we may tweak url + + $token = null; + + $isUserLogin = $this->isUserLogin(); + + $isAppLogin = $this->isAppLogin(); + + if ($isUserLogin || $isAppLogin) { + + if ($isAppLogin) { + + $token = $this->doAppLogin(); + + } else if($isUserLogin) { + + $token = $this->doUserPasswordLogin(); + } + + }else{ + + $this->proxyLog->log("Can not determine if OAuth or ArcGIS Server means of authentication. Check config for errors."); + } + + return $token; + } + + + public function addTokenToSession($token) { + + $sessonKey = 'token_for_' . $this->sessionUrl; + + $sessonKey = sprintf("'%s'", $sessonKey); + + try { + + $this->proxyLog->log('Adding token to session'); + + $_SESSION[$sessonKey] = $token; + + }catch(Exception $e){ + + $this->proxyLog->log("Error setting session: " . $e); + } + + } + + + public function doUserPasswordLogin() { + + $this->proxyLog->log("Resource using ArcGIS Server security"); + + $tokenServiceUri = $this->getTokenEndpoint(); + + $this->proxyPost($tokenServiceUri, array ( + 'request' => 'getToken', + 'f' => 'json', + 'referer' => $this->referer, + 'expiration' => 60, + 'username' => $this->resource['username'], + 'password' => $this->resource['password'] + )); + + $tokenResponse = json_decode($this->proxyBody, true); + + $token = $tokenResponse['token']; + + return $token; + } + + public function getTokenEndpoint() + { + $position = strripos($this->resource['url'], "/rest"); + + if($position){ + + $infoUrl = substr($this->resource['url'],0,$position) . "/rest/info"; + + }else{ + + $infoUrl = $this->resource['url'] . "/arcgis/rest/info"; + } + + $this->proxyPost($infoUrl,array('f' => 'json')); + + $infoResponse = json_decode($this->proxyBody, true); + + $tokenServiceUri = $infoResponse['authInfo']['tokenServicesUrl']; + + if(!empty($tokenServiceUri)) { + + $this->proxyLog->log("Got token endpoint"); + + }else{ + + $this->proxyLog->log("Unable to get token endpoint"); + } + + return $tokenServiceUri; + } + + + + public function doAppLogin() + { + $this->resource['oauth2endpoint'] = isset($this->resource['oauth2endpoint']) ? $this->resource['oauth2endpoint'] : "https://arcgis.com/sharing/oauth2/"; + + if (substr($this->resource['oauth2endpoint'], -1) != '/') + { + $this->resource['oauth2endpoint'] = $this->resource['oauth2endpoint'] . "/"; + } + + $this->proxyLog->log("Resource using OAuth"); + + $this->proxyPost($this->resource['oauth2endpoint'] . "token", array( + 'client_id' => $this->resource['clientid'], + 'client_secret' => $this->resource['clientsecret'], + 'grant_type' => 'client_credentials', + 'f' => 'json' + )); + + $tokenResponse = json_decode($this->proxyBody, true); + + $token = $tokenResponse['access_token']; + + if (!empty($token)) + { + $token = $this->exchangePortalTokenForServerToken($token); + } + + return $token; + } + + + public function isAllowedApplication() + { + + if(in_array("*",$this->proxyConfig['allowedreferers'])){ + + $this->referer = $_SERVER['SERVER_NAME']; //This is to enable browser testing when * is used + + $isAllowedApplication = true; + + return $isAllowedApplication; + + }else{ + + $this->referer = $_SERVER['HTTP_REFERER']; + + } + + $isAllowedApplication = false; + + $domain = substr($_SERVER['HTTP_REFERER'], strpos($this->referer, '://') + 3); + + $domain = substr($domain, 0, strpos($domain, '/')); + + if (in_array($domain, $this->proxyConfig['allowedreferers'])) { + + $isAllowedApplication = true; + + }else{ + + $this->proxyLog->log("Attempt made to use this proxy from " . $this->referer . " and " . $_SERVER['REMOTE_ADDR']); + + } + + return $isAllowedApplication; + + } + + function __destruct() + { + + } +} + + + + +class ProxyLog { + + public $timeFormat = 'm-d-y H:i:s'; + + public $seperator = ' | '; + + public $eol = "\r\n"; + + public $indent = " "; + + public $proxyConfig; + + + public function __construct($configuration = null) { + + if($configuration != null){ + + $this->proxyConfig = $configuration->proxyConfig; + + $this->addLogLevel(); + + if($this->proxyConfig['loglevel'] != 3){ + + $this->attemptWriteToLog(); + } + + }else{ + + throw new Exception ('Problem creating log.'); + } + + } + + private function addLogLevel() + { + + if(empty($this->proxyConfig['logfile'])) { + + $this->proxyConfig['logfile'] = null; + + $this->proxyConfig['loglevel'] = 3; //Turns off logging + } + + if(!empty($this->proxyConfig['logfile']) && empty($this->proxyConfig['loglevel'])) { + + $this->proxyConfig['loglevel'] = 0; //Turns on logging + + } + + } + + + public function write($m) + { + + if (isset($this->proxyConfig['logfile'])) { + + try { + + $fh = null; + + $fh = fopen($this->proxyConfig['logfile'], (file_exists($this->proxyConfig['logfile'])) ? 'a' : 'w'); + + if (is_writable($this->proxyConfig['logfile'])) { + + fwrite($fh, $this->eol); + + fwrite($fh, $this->getTime()); + + //fwrite($fh, $this->seperator); + + //fwrite($fh, $_SERVER['HTTP_REFERER']); + + fwrite($fh, $this->seperator); + + fwrite($fh, $m); + + }else{ + + header('Status: 200', true, 200); + + header('Content-Type: application/json'); + + $configError = array( + "error" => array("code" => 412, + "details" => array("Detected malformed 'logFile' in configuration. Make sure this app has write permissions to specified log file in configuration. The server does not meet one of the preconditions that the requester put on the request."), + "message" => "Proxy failed due to configuration error." + )); + + echo json_encode($configError); + + exit(); + + } + + fclose($fh); + + } catch (Exception $e) { + + $this->log($e->getMessage()); + + } + } else { + + header('Status: 200', true, 200); + + header('Content-Type: application/json'); + + $configError = array( + "error" => array("code" => 412, + "details" => array("Detected malformed 'logFile' in configuration. Make sure this app has write permissions to specified log file in configuration. The server does not meet one of the preconditions that the requester put on the request."), + "message" => "Proxy failed due to configuration error." + )); + + echo json_encode($configError); + + exit(); + + } + } + + public function attemptWriteToLog() + { + + if ($this->proxyConfig['loglevel'] == 0 || $this->proxyConfig['loglevel'] == 2) { + + if (isset($this->proxyConfig['logfile'])) { + + try { + + $fh = null; + + $fh = fopen($this->proxyConfig['logfile'], (file_exists($this->proxyConfig['logfile'])) ? 'a' : 'w'); + + if (is_writable($this->proxyConfig['logfile'])) { + + fwrite($fh, $this->eol); + + fwrite($fh, ' '); + + }else{ + + header('Status: 200', true, 200); + + header('Content-Type: application/json'); + + $configError = array( + "error" => array("code" => 412, + "details" => array("Detected malformed 'logFile' in configuration. Make sure this app has write permissions to specified log file in configuration. The server does not meet one of the preconditions that the requester put on the request."), + "message" => "Proxy failed due to configuration error." + )); + + echo json_encode($configError); + + exit(); + + } + + fclose($fh); + + } catch (Exception $e) { + + header('Status: 200', true, 200); + + header('Content-Type: application/json'); + + $configError = array( + "error" => array("code" => 412, + "details" => array("Could not write to log. Make sure this app has write permissions to specified log file in configuration. The server does not meet one of the preconditions that the requester put on the request."), + "message" => "Proxy failed due to configuration error." + )); + + echo json_encode($configError); + + exit(); + + } + } else { + + header('Status: 200', true, 200); + + header('Content-Type: application/json'); + + $configError = array( + "error" => array("code" => 412, + "details" => array("Detected malformed 'logFile' in configuration. Make sure this app has write permissions to specified log file in configuration. The server does not meet one of the preconditions that the requester put on the request."), + "message" => "Proxy failed due to configuration error." + )); + + echo json_encode($configError); + + exit(); + + } + + } + + } + + + public function log($message) + { + + if ($this->proxyConfig['loglevel'] == 0) { + + $this->write($message); //Writes messages and errors to logs + + } elseif ($this->proxyConfig['loglevel'] == 1) { + + echo $message; //Show proxy errors and messages in browser console (should only be used when looking for errors) + + } elseif ($this->proxyConfig['loglevel'] == 2) { + + $this->write($message); //Writes messages and errors to logs + + echo $message; //Show proxy errors and messages in browser console (should only be used when looking for errors) + + } elseif ($this->proxyConfig['loglevel'] == 3) { + + return; //No logging + } + + } + + public function getTime() { + + return date($this->timeFormat); + } + + function __destruct() + { + + } + +} + +class RateMeter +{ + /** + * Holds cleanup threshold value + * + * @var string + * @access public + */ + + const CLEAN_RATEMAP_AFTER = 10000; + + /** + * Holds proxy log + * + * @var string + * @access public + */ + public $proxyLog; + + /** + * Holds serverurl property + * + * @var string + * @access public + */ + public $serverUrl; + + /** + * Ip property + * + * @var string + * @access public + */ + public $ip; + + /** + * Time property + * + * @var string + * @access public + */ + + public $time; + + /** + * Stores microtime as property allowing for fractional seconds + * + * @var string + * @access public + */ + + public $microtime; + + /** + * Sqlite database name property + * + * @var string + * @access public + */ + public $dbname; + + /** + * Sqlite connection property + * + * @var PDO + * @access public + */ + public $con; + + /** + * Under meter cap property + * + * @var bool + * @access public + */ + public $underMeterCap; + + + /** + * Meter cap property + * + * @var int + * @access public + */ + + public $countCap; + + /** + * Meter rate property + * + * @var int + * @access public + */ + public $rate; + + /** + * Holds the rate meter count value + * + * @var int + * @access public + */ + public $count = 0; + + /** + * Holds rate meter period found in config + * + * @var int + * @access public + */ + + public $ratelimitperiod; + + /** + * Holds current resource being proxied + * + * @var string + */ + + public $resourceUrl; + + + + public function __construct ($url, $ratelimit, $ratelimitperiod, $log) { + + $this->proxyLog = $log; + + $this->resourceUrl = $url; + + $this->ratelimitperiod = $ratelimitperiod; + + $this->countCap = $ratelimit; + + $this->rate = $ratelimit / $ratelimitperiod / 60; //ratelimitperiod is designed to be in seconds + + $this->ip = $_SERVER['REMOTE_ADDR']; + + $this->dbname = 'proxy.sqlite'; // This string may need to come config + + $this->getRateMeterDatabase(); + + } + + public function getConnection() + { + if($this->con != null) + { + return $this->con; + + }else{ + + $this->proxyLog->log('Cannot get a connection'); + + header('Status: 200', true, 200); + + header('Content-Type: application/json'); + + $serverError = array( + "error" => array("code" => 500, + "details" => array("Cannot make a Sqlite database connection. Check to see if it exists. If it does, consider backing up and then deleting sqlite database."), + "message" => "Proxy failed could not connect to sqlite database." + )); + + echo json_encode($serverError); + + exit(); + } + } + + public function getRateMeterDatabase() + { + try { + + if(file_exists($this->dbname)) + { + $db = new PDO("sqlite:" . $this->dbname); + + $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $this->con = $db; + + return; + } + + } + catch(ErrorException $e) + { + $this->proxyLog->log($e->getMessage()); + + return null; + } + + try { + + $db = new PDO("sqlite:" . $this->dbname); + + $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + chmod($this->dbname,0777); + + if($db != null) + { + $db->beginTransaction(); + + $this->createResourceIpTable($db); + + $this->createClicksTable($db); + + $this->insertClickRecord($db); + + $db->commit(); + + $this->con = $db; + + } + + return; + + } + catch(PDOException $e) + { + $this->proxyLog->log($e->getMessage()); + + return null; + } + } + + public function insertClickRecord($db) + { + + try { + + + $sql = "INSERT INTO clicks (id, total) VALUES (:id,:total)"; + + $q = $db->prepare($sql); + + $q->bindValue(':id', null); + + $q->bindValue(':total', 1); + + $q->execute() or die($this->getDatabaseErrorMessage()); + + } + catch(PDOException $e) + { + + $this->proxyLog->log($e->getMessage()); + } + + } + + public function createClicksTable($db) + { + try { + + $db->exec("CREATE TABLE IF NOT EXISTS clicks ( + id INTEGER PRIMARY KEY, + total INTEGER)"); + + $this->proxyLog->log("clicks table created!"); + } + catch(PDOException $e) + { + $this->proxyLog->log($e->getMessage()); + } + + try { + + $db->exec('CREATE INDEX total ON clicks (total)'); + + } + catch(PDOException $e) + { + $this->proxyLog->log($e->getMessage()); + } + + } + + public function createResourceIpTable($db) + { + try { + + $db->exec("CREATE TABLE IF NOT EXISTS ips ( + id INTEGER PRIMARY KEY, + url VARCHAR(255), + ip VARCHAR(50), + count INTEGER, + rate INTEGER, + time INTEGER)"); + + $this->proxyLog->log("ips table created!"); + + } + catch(PDOException $e) + { + $this->proxyLog->log($e->getMessage()); + } + + try { + + $db->exec('CREATE INDEX url ON ips (url)'); + + $db->exec('CREATE INDEX ip ON ips (ip)'); + + $db->exec('CREATE INDEX count ON ips (count)'); + + $db->exec('CREATE INDEX rate ON ips (rate)'); + + $db->exec('CREATE INDEX time ON ips (time)'); + + } + catch(PDOException $e) + { + $this->proxyLog->log($e->getMessage()); + } + + } + + public function getClickCount() + { + $db = $this->getConnection(); + + try { + + $sth = $db->prepare("SELECT total FROM clicks WHERE id = :id"); + + $sth->execute(array(':id' => 1)) or die($this->getDatabaseErrorMessage()); + + $r = $sth->fetchAll(); + + return $r[0]['total']; + + } + catch(PDOException $e) + { + $this->proxyLog->log($e->getMessage()); + } + } + + public function selectLastRequest() + { + + $db = $this->getConnection(); + + try { + + $sth = $db->prepare("SELECT time, id, count, rate FROM ips WHERE url = :url AND ip = :ip"); + + $sth->execute(array(':url' => $this->resourceUrl, ':ip' => $this->ip)) or die($this->getDatabaseErrorMessage()); + + $r = $sth->fetchAll(); + + return $r[0]; + + } + catch(PDOException $e) + { + $this->proxyLog->log($e->getMessage()); + } + } + + public function updateResourceIp($id) + { + $db = $this->getConnection(); + + try { + + $sth = $db->prepare("UPDATE ips SET id=:id, url=:url, ip=:ip, count=:count, rate=:rate, time=:time WHERE id = :id"); + + $sth->bindValue(':id', $id); + + $sth->bindValue(':url', $this->resourceUrl); + + $sth->bindValue(':ip', $this->ip); + + $sth->bindValue(':count', $this->count); + + $sth->bindValue(':rate', $this->rate); + + $sth->bindValue(':time', $this->microtime); + + $sth->execute() or die($this->getDatabaseErrorMessage()); + } + catch(PDOException $e) + { + $this->proxyLog->log($e->getMessage()); + } + + } + + public function updateClicks($id, $total) + { + + $db = $this->getConnection(); + + try { + + $sth = $db->prepare("UPDATE clicks SET total=:total WHERE id = :id"); + + $sth->bindValue(':id', $id); + + $sth->bindValue(':total', $total); + + $sth->execute() or die($this->getDatabaseErrorMessage()); + } + catch(PDOException $e) + { + + $this->proxyLog->log($e->getMessage()); + } + + } + + public function insertResourceIp() + { + $db = $this->getConnection(); + + try { + + $sql = "INSERT INTO ips (id, url, ip, count, rate, time) VALUES (:id,:url,:ip,:count,:rate,:time)"; + + $q = $db->prepare($sql); + + $q->bindValue(':id', null); + + $q->bindValue(':url', $this->resourceUrl); + + $q->bindValue(':ip', $this->ip); + + $q->bindValue(':count', $this->count); + + $q->bindValue(':rate', $this->rate); + + $q->bindValue(':time', $this->microtime); + + $q->execute() or die($this->getDatabaseErrorMessage()); + + } + catch(PDOException $e) + { + $this->proxyLog->log($e->getMessage()); + } + + } + + public function canBeCleaned($count, $lastTime, $rate) + { + $tsTotalSeconds = $this->getTimeDifferenceInSeconds($lastTime); + + return $count - $tsTotalSeconds * $rate <= 0; + } + + public function fetchAllIps() { + + $this->microtime = microtime(true); + + $db = $this->getConnection(); + + try { + + $sth = $db->prepare("SELECT * FROM ips;"); + + $sth->execute() or die($this->getDatabaseErrorMessage()); + + $r = $sth->fetchAll(); + + return $r; + + } + catch(PDOException $e) + { + $this->proxyLog->log($e->getMessage()); + } + } + + + public function rateMeterCleanup() + { + $db = $this->getConnection(); + + $r = $this->fetchAllIps(); + + $deletes = array(); + + foreach ($r as $item => $value) + { + + if($this->canBeCleaned($value['count'], $value['time'], $value['rate'])){ + + $this->proxyLog->log($value['id'] . "::id"); + + $deletes[] = $value['id']; + + }else{ + + $this->proxyLog->log('Nothing to clean.'); + + } + + } + + $hasDeletes = (boolean)$deletes; + + if($hasDeletes){ + + $placeholders = implode(',', array_fill(0, count($deletes), '?')); + + $stmt = $db->prepare('DELETE FROM ips WHERE id IN(' . $placeholders . ')'); //Using indexed based binding pattern + + foreach ($deletes as $k => $id){ + + $stmt->bindValue(($k+1), $id); + + } + + try { + + $stmt->execute(); + + $this->proxyLog->log('Cleanup occured.'); + + } catch (Exception $e) { + + $this->proxyLog->log($e->getMessage()); + + } + + } + + } + + + public function checkRateMeter() + { + $this->microtime = microtime(true); + + $lastRequest = $this->selectLastRequest(); + + $clickCount = $this->getClickCount(); + + $clickCount = $clickCount + 1; + + $this->updateClicks(1, $clickCount); //Updating the click table so we know when to clean up (aka after 10,000 requests) + + if ($lastRequest != null || count($lastRequest) > 0) { + + $count = $lastRequest['count']; + + $tsTotalSeconds = $this->getTimeDifferenceInSeconds($lastRequest['time']); + + // $this->debugMeterAlgorithm($count, $tsTotalSeconds, $this->rate); + + $count = max(0, $count - $tsTotalSeconds * $this->rate); + + if ($count <= $this->countCap) { + + $this->underMeterCap = true; + + $count = $count + 1; + + $this->count = $count; + + $this->updateResourceIp($lastRequest['id']); + + return true; + + } + + }else{ + + $this->insertResourceIp(); //Add item to ips table. + + $this->underMeterCap = true; + + return; + } + + $this->underMeterCap = false; + + if($clickCount >= RateMeter::CLEAN_RATEMAP_AFTER) + { + $this->proxyLog->log("Click count: " . $clickCount); + + $this->rateMeterCleanup(); + + $this->updateClicks(1, 0); //Set click counter back to zero after cleanup. + + } + + return; + + } + public function debugMeterAlgorithm($count, $tsTotalSeconds, $rate) + { + $debugCountValue = $count - $tsTotalSeconds * $rate; + + $this->proxyLog->log("Count is ( count - timespan x rate ) : " . $count . " - " . $tsTotalSeconds . " x " . $rate . " = " . $debugCountValue); + + $this->proxyLog->log("debugCountValue:::" . $debugCountValue); + + } + + + public function getTimeDifferenceInSeconds($firstTime, $secondTime = null) + { + + + if($firstTime == null) + { + $this->proxyLog->log("No time value was returned from 'ips' table in Sqlite database!"); + + header('Status: 200', true, 200); + + header('Content-Type: application/json'); + + $serverError = array( + "error" => array("code" => 500, + "details" => array("No time value was returned from 'ips' table in Sqlite database. Consider backing up and then deleting sqlite database."), + "message" => "Proxy failed due to missing value in database." + )); + + echo json_encode($serverError); + + exit(); + + } + + if($secondTime == null) + { + $secondTime = microtime(true); + } + + $time = array($firstTime, $secondTime); + + sort($time); + + $diff = $time[1] - $time[0]; + + return $diff; + + } + + + public function underMeterCap() + { + if(empty($this->countCap) || empty($this->ratelimitperiod)) { + + return true; + + } + + $this->checkRateMeter(); + + if($this->underMeterCap) + { + return true; + + }else{ + + return false; + } + } + + public function getDatabaseErrorMessage() + { + $this->proxyLog->log("A database error occured."); + + header('Status: 200', true, 200); + + header('Content-Type: application/json'); + + $dbError = array( + "error" => array("code" => 500, + "details" => array("A database error occured. Consider backing up and then deleting sqlite database."), + "message" => "Proxy failed due to database error." + )); + + return json_encode($dbError); + } + + function __destruct() + { + $this->con = null; + + } + +} + +class ProxyConfig { + + public $proxyConfig; + + public $serverUrls; + + public function __construct() + { + + } + + public function useXML() + { + + $xmlParser = new XmlParser(); + + $proxyconfig = $xmlParser->results[0]['proxyconfig']; + + $proxyconfig = $this->lowercaseArrayKeys($proxyconfig, CASE_LOWER); + + $proxyConfig = $this->setProxyConfig($proxyconfig); + + $serverUrls = $xmlParser->results[0]['childrens'][0]['childrens']; + + $serverUrls = $this->lowercaseArrayKeys($serverUrls, CASE_LOWER); + + $normalizeServerUrls = $this->normalizeServerUrls($serverUrls); + + $this->setServerUrls($normalizeServerUrls); + + $allowedReferers = explode(",", $this->proxyConfig['allowedreferers']); //Change XML allowedreferers from string to array + + $this->proxyConfig['allowedreferers'] = $allowedReferers; //Add above array to the proxyconfig property + + $xmlParser = null; + + } + + function lowercaseArrayKeys($array, $case) + { + $array = array_change_key_case($array, $case); + + foreach ($array as $key => $value) { + + if ( is_array($value) ) { + + $array[$key] = $this->lowercaseArrayKeys($value, $case); + + } + } + + return $array; + } + + + public function normalizeServerUrls($serverUrls) { + + $formatedData = array(); + + foreach ($serverUrls as $key => $item) { + + if(is_array($item)) { + + foreach ($item as $k => $v) { + + if(is_array($v)) { + + $normal = array("serverurl" => array(0=>$v)); + + $formatedData[] = $normal; + + } + + } + } + } + + return $formatedData; + } + + + public function useJSON() + { + try { + + $c = file_get_contents("proxy.config"); + + $configJson = json_decode($c, true); + + $config = $this->lowercaseArrayKeys($configJson, CASE_LOWER); + + $this->setProxyConfig($config['proxyconfig'][0]); + + $this->setServerUrls($config['serverurls']); + + }catch (Exception $e) { + + $this->proxyLog->log($e->getMessage()); + + } + + } + + public function __set($property, $value) + { + $method = 'set' . $value; + + if (!method_exists($this, $method)) + { + + throw new Exception('Error in proxy config.'); + } + + $this->$method($value); + } + + public function __get($property) + { + $method = 'get' . $property; + + if (!method_exists($this, $method)) + { + throw new Exception('Error in proxy config.'); + } + + return $this->$method(); + } + + public function setOptions(array $options) + { + $methods = get_class_methods($this); + + foreach ($options as $key => $value) { + + $method = 'set' . ucfirst($key); + + if (in_array($method, $methods)) + { + $this->$method($value); + } + } + + return $this; + } + + public function getServerUrls() + { + + return $this->serverUrls; + + } + + public function setServerUrls($item) + { + + $this->serverUrls = $item; + + return $this; + + } + + public function getProxyConfig() + { + + return $this->proxyConfig; + + } + + public function setProxyConfig($item) + { + + $this->proxyConfig = $item; + + return $this; + } + + function __destruct() + { + + } + +} + +class XmlParser +{ + public $results = array(); + + public $parser; + + public $xmlString; + + public $file; + + function XmlParser($f = "proxy.config") + { + if(trim($f) != "") { $this->loadFile($f);} + } + + function loadFile($f) + { + $data = file($f); + + $xml = implode("\n", $data); + + return $this->parse($xml); + } + + function parse($xml) + { + $this->parser = xml_parser_create(); + + xml_set_object($this->parser, $this); + + xml_set_element_handler($this->parser, "tagStart", "tagEnd"); + + $this->xmlString = xml_parse($this->parser,$xml); + + if(!$this->xmlString) + { + die(sprintf("Config XML error: %s at line %d", + xml_error_string(xml_get_error_code($this->parser)), + xml_get_current_line_number($this->parser))); //This is before we have the log location + } + + xml_parser_free($this->parser); + + return $this->results; + } + + function tagStart($parser, $name, $attrs) + { + $attrs = array_change_key_case($attrs, CASE_LOWER); + + $tag = array(strtolower($name) => $attrs); + + array_push($this->results, $tag); + } + + + function tagEnd($parser, $name) + { + + //http://www.php.net/manual/en/function.xml-parse.php + + $this->results[count($this->results)-2]['childrens'][] = $this->results[count($this->results)-1]; + + if(count($this->results[count($this->results)-2]['childrens'] ) == 1) + { + $this->results[count($this->results)-2]['firstchild'] =& $this->results[count($this->results)-2]['childrens'][0]; + } + + array_pop($this->results); + } + + function __destruct() + { + + } + +} + + +$proxyConfig = new ProxyConfig(); + +$proxyConfig->useXML(); + +//$proxyConfig->useJSON(); + +$proxyLog = new ProxyLog($proxyConfig); + +$proxyObject = new Proxy($proxyConfig, $proxyLog); + +$proxyObject->getResponse(); + +?> \ No newline at end of file diff --git a/lib/resource-proxy/proxy.sqlite b/lib/resource-proxy/proxy.sqlite new file mode 100755 index 0000000..34ef75a Binary files /dev/null and b/lib/resource-proxy/proxy.sqlite differ diff --git a/lib/resource-proxy/proxy_log.log b/lib/resource-proxy/proxy_log.log new file mode 100644 index 0000000..e9aff7b --- /dev/null +++ b/lib/resource-proxy/proxy_log.log @@ -0,0 +1,5 @@ + + +02-25-14 01:04:21 | FILES detected +02-25-14 01:04:22 | Ok to proxy +02-25-14 01:04:22 | Proxy complete \ No newline at end of file diff --git a/samples/attachments-editor.html b/samples/attachments-editor.html index 5388bc2..bc4c0c8 100644 --- a/samples/attachments-editor.html +++ b/samples/attachments-editor.html @@ -81,8 +81,8 @@ // a proxy page is required to upload attachments // refer to "Using the Proxy Page" for more information: https://developers.arcgis.com/en/javascript/jshelp/ags_proxy.html - esriConfig.defaults.io.proxyUrl = "../lib/proxy.php"; - //esriConfig.defaults.io.proxyUrl = "http://localhost/proxy/proxy.php"; + //esriConfig.defaults.io.proxyUrl = "../lib/proxy.php"; + esriConfig.defaults.io.proxyUrl = "../lib/resource-proxy/proxy.php"; var offlineFeaturesManager = new OfflineFeaturesManager(); offlineFeaturesManager.on(offlineFeaturesManager.events.EDITS_ENQUEUED, updateStatus);