You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@netbeans.apache.org by ma...@apache.org on 2021/03/02 18:14:22 UTC

[netbeans-tools] branch master updated: Provide an optimized update.xml.gz generation, that allows choosing the apache mirror to use

This is an automated email from the ASF dual-hosted git repository.

matthiasblaesing pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/netbeans-tools.git


The following commit(s) were added to refs/heads/master by this push:
     new 775f29c  Provide an optimized update.xml.gz generation, that allows choosing the apache mirror to use
     new c12157a  Merge pull request #43 from matthiasblaesing/proxy-chooser
775f29c is described below

commit 775f29ce5cdeccfdca84a5333931a12fb76b82be
Author: Matthias Bläsing <mb...@doppel-helix.eu>
AuthorDate: Mon Apr 13 21:10:38 2020 +0200

    Provide an optimized update.xml.gz generation, that allows choosing the apache mirror to use
---
 .gitignore                                       |    2 +-
 proxy-chooser/.gitignore                         |    3 +
 proxy-chooser/.htaccess                          |   10 +
 proxy-chooser/README.md                          |  111 ++
 proxy-chooser/classes/.htaccess                  |    1 +
 proxy-chooser/classes/ApacheMirrors.php          |  136 ++
 proxy-chooser/classes/CountryCodes.php           |   61 +
 proxy-chooser/classes/IP2Location.php            | 2261 ++++++++++++++++++++++
 proxy-chooser/classes/apache_proxy_countries.csv |   56 +
 proxy-chooser/classes/config.php                 |   17 +
 proxy-chooser/data/.htaccess                     |    1 +
 proxy-chooser/mirror-list.php                    |   89 +
 proxy-chooser/nbproject/project.properties       |    8 +
 proxy-chooser/nbproject/project.xml              |    9 +
 proxy-chooser/tools/.htaccess                    |    1 +
 proxy-chooser/tools/download-ip2location.sh      |   33 +
 proxy-chooser/updates.php                        |  308 +++
 17 files changed, 3106 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index 61bf37e..a0a7bed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,4 +19,4 @@
 /translations/*/dist/
 /translations/*/nbproject/private/
 /translations/*/nbproject/genfiles.properties
-/pp3/module/Application/external/osgi-jar-parser/target
+/pp3/module/Application/external/osgi-jar-parser/target
\ No newline at end of file
diff --git a/proxy-chooser/.gitignore b/proxy-chooser/.gitignore
new file mode 100644
index 0000000..728080b
--- /dev/null
+++ b/proxy-chooser/.gitignore
@@ -0,0 +1,3 @@
+/nbproject/private/
+/data/*
+!/data/.htaccess
\ No newline at end of file
diff --git a/proxy-chooser/.htaccess b/proxy-chooser/.htaccess
new file mode 100644
index 0000000..0e9895c
--- /dev/null
+++ b/proxy-chooser/.htaccess
@@ -0,0 +1,10 @@
+Options -Indexes
+
+# Enable proxy chooser
+RewriteEngine on
+RewriteCond "%{REQUEST_URI}" "/uc-proxy-chooser/[^/]*/updates.xml(.gz)?"
+RewriteRule !/uc-proxy-chooser/[^/]*/updates.xml(.gz)?$ ./updates.php$1 [L]
+
+<Files ".*">
+    Deny from all
+</Files>
\ No newline at end of file
diff --git a/proxy-chooser/README.md b/proxy-chooser/README.md
new file mode 100644
index 0000000..d8b4bf1
--- /dev/null
+++ b/proxy-chooser/README.md
@@ -0,0 +1,111 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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
+
+    https://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 the
+  specific language governing permissions and limitations
+  under the License.
+ -->
+
+Proxy Chooser for Apache NetBeans
+=================================
+
+The updates.xml.gz files that are created at build time from the netbeans build
+system contain relative references to the NBMs. Access to the NBMs is then
+dispatched into the mirror network of apache via
+
+https://www.apache.org/dyn/closer.lua
+
+There are at least two problems with this approach:
+
+- closer.lua is hit many times and can cause blocks because this is detected as
+  abuse of the the download system
+- closer.lua resolves to many different proxies, which collides with company
+  policies, that require whitelisting of external downloads.
+
+This PHP script uses the updates.xml.gz file of the Apache NetBeans releases
+to produce an updates.xml.gz file with absolute links, that all point to the
+same download mirror server and does not cause further closer.lua calls.
+
+In addition the script allows to request a fixed mirror to be used.
+
+Sample invocations:
+
+https://netbeans-vm1.apache.org/uc-proxy-chooser/12.2/updates.xml.gz
+
+This call generates the updates.xml.gz with a mirror that is randomly chosen
+from the list of mirrors associated with the country that is detected based on
+the request IP address.
+
+https://netbeans-vm1.apache.org/uc-proxy-chooser/12.2/updates.xml.gz?mirror=ftp.tudelft.nl
+
+This call generates the updates.xml.gz with the mirror ftp.tudelft.nl. The
+parameter "mirror" takes the hostname of the desired mirror.
+
+https://netbeans-vm1.apache.org/uc-proxy-chooser/12.2/updates.xml.gz?mirror=doesnotexist
+
+If the mirror is not found in the mirror list the ASF provides, a http status
+400 is generated. As body data the list of accepted values is returned.
+
+https://netbeans-vm1.apache.org/uc-proxy-chooser/12.2/updates.xml.gz?mirror=BASE
+
+This forces the delivery of the base file with relative links.
+
+Prerequisites
+-------------
+
+- php-bcmath
+
+Installation
+------------
+
+The base directory is assumed to be `/var/www/html/uc` in that directory for
+each released version of netbeans a subdirectory is found, each of these holds
+a .htaccess like this:
+
+```
+RedirectMatch ^/uc/12.0/((?!(updates\.xml|licenses)).*)(\?.*)?$ https://www.apache.org/dyn/closer.lua?action=download&filename=netbeans/netbeans/12.0/nbms/$1
+```
+
+So the updates.xml and the license files are served from the netbeans-vm, while
+the NBMs are downloaded from the mirror network.
+
+- The code of the proxy-chooser is to be placed into the directory
+  `/var/www/html/uc-proxy-chooser`
+- The directory `/var/www/html/uc-proxy-chooser/data` needs to be writeable by
+  the PHP process
+
+Users wishing to use the new system have to move from
+
+https://netbeans.apache.org/nb/updates/dev/updates.xml.gz
+
+to
+
+https://netbeans.apache.org/uc-proxy-chooser/12.3/updates.xml.gz
+
+Additional work
+---------------
+
+The IP2LOCATION database files need to be manually placed into the data
+directory and kept up-to-date.
+
+The manual procedure:
+
+```
+cd /var/www/html/uc/proxy-chooser/data
+curl https://download.ip2location.com/lite/IP2LOCATION-LITE-DB1.IPV6.BIN.ZIP > IP2LOCATION-LITE-DB1.IPV6.BIN.ZIP
+curl https://download.ip2location.com/lite/IP2LOCATION-LITE-DB1.BIN.ZIP > IP2LOCATION-LITE-DB1.BIN.ZIP
+unzip -o IP2LOCATION-LITE-DB1.IPV6.BIN.ZIP IP2LOCATION-LITE-DB1.IPV6.BIN
+unzip -o IP2LOCATION-LITE-DB1.BIN.ZIP IP2LOCATION-LITE-DB1.BIN
+rm *.zip
+```
\ No newline at end of file
diff --git a/proxy-chooser/classes/.htaccess b/proxy-chooser/classes/.htaccess
new file mode 100644
index 0000000..14249c5
--- /dev/null
+++ b/proxy-chooser/classes/.htaccess
@@ -0,0 +1 @@
+Deny from all
\ No newline at end of file
diff --git a/proxy-chooser/classes/ApacheMirrors.php b/proxy-chooser/classes/ApacheMirrors.php
new file mode 100644
index 0000000..58438d4
--- /dev/null
+++ b/proxy-chooser/classes/ApacheMirrors.php
@@ -0,0 +1,136 @@
+<?php
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *   https://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 the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+class ApacheMirrors {
+    private $cacheFile;
+    private $mirrorUrl;
+    private $mirrors = array();
+    private $countryMirrors = array();
+
+    public function __construct($mirrorUrl, $cacheFile) {
+        $this->cacheFile = $cacheFile;
+        $this->mirrorUrl = $mirrorUrl;
+        $this->ensureMirrorListIsCurrent();
+        $this->readMirrorList();
+    }
+
+    /**
+     * Ensure that the locally cached list of apache mirrors is current. This
+     * function checks the age of the locally cached file and if it is to old, a
+     * new list is fetched from the original location.
+     */
+    private function ensureMirrorListIsCurrent() {
+        if (file_exists($this->cacheFile)) {
+            $age = filemtime($this->cacheFile);
+        } else {
+            $age = 0;
+        }
+        $now = time();
+        // Cache the mirror list for one hour
+        if ((!$age) || ($age < ($now - (1 * 60 * 60)))) {
+            $mirrorlistInput = file_get_contents($this->mirrorUrl);
+            if ($mirrorlistInput) {
+                $cacheDir = dirname($this->cacheFile);
+                $tmp = tempnam($cacheDir, "mirors");
+                $fid = fopen($tmp, "w");
+                fwrite($fid, $mirrorlistInput);
+                fclose($fid);
+                if (!rename($tmp, $this->cacheFile)) {
+                    throw new Exception("Failed to rename mirror list");
+                }
+            }
+        }
+    }
+
+    /**
+     * Read the cached mirror list and return the list of http mirrors.
+     *
+     * @return map hostname to full url
+     */
+    private function readMirrorList() {
+        $mirrorFid = fopen($this->cacheFile, "r");
+        $this->mirrors = array();
+        $this->countryMirrors = array();
+        while ($row = fgetcsv($mirrorFid, 0, " ")) {
+            if (count($row) >= 3 && ($row[0] == "http")) {
+                $country = strtolower($row[1]);
+                $hostname = parse_url($row[2], PHP_URL_HOST);
+                if (!isset($this->countryMirrors[$country])) {
+                    $this->countryMirrors[$country] = [];
+                }
+                $this->countryMirrors[$country][] = $hostname;
+                $this->mirrors[$hostname] = $row[2];
+            }
+        }
+        fclose($mirrorFid);
+    }
+
+    public function getProxyList() {
+        return $this->mirrors;
+    }
+
+    public function proxyExists($hostname) {
+        return array_key_exists($hostname, $this->mirrors);
+    }
+
+    public function proxyExistsCountry($countryCode) {
+        return array_key_exists(strtolower($countryCode), $this->countryMirrors);
+    }
+
+    public function getProxy($hostname) {
+        return $this->mirrors[$hostname];
+    }
+
+    public function getProxyCountryKey($countryCode) {
+        if(! $this->proxyExistsCountry($countryCode)) {
+            return null;
+        }
+        $candidates = $this->countryMirrors[strtolower($countryCode)];
+        if(count($candidates) > 0) {
+            $rand = rand(0, count($candidates) - 1);
+            return $candidates[$rand];
+        } else {
+            return null;
+        }
+    }
+
+    public function getFallbackProxyKey() {
+        $candidates = $this->countryMirrors[strtolower('backup')];
+        if (count($candidates) > 0) {
+            $rand = rand(0, count($candidates) - 1);
+            return $candidates[$rand];
+        } else {
+            return null;
+        }
+    }
+
+    public function getCountryCodes() {
+        return array_keys($this->countryMirrors);
+    }
+
+    public function getMirrorsForCountry($countryCode) {
+        if(array_key_exists($countryCode, $this->countryMirrors)) {
+            return $this->countryMirrors[$countryCode];
+        } else {
+            return [];
+        }
+    }
+}
\ No newline at end of file
diff --git a/proxy-chooser/classes/CountryCodes.php b/proxy-chooser/classes/CountryCodes.php
new file mode 100644
index 0000000..9815bba
--- /dev/null
+++ b/proxy-chooser/classes/CountryCodes.php
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *   https://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 the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+class CountryCodes {
+    private $data = [];
+
+    public function __construct() {
+        $fid = fopen(__DIR__ . "/apache_proxy_countries.csv", "r");
+        while($row = fgetcsv($fid)) {
+            $this->data[] = [
+                "apacheCode" => strtolower($row[0]),
+                "ip2countryCode" => strtolower($row[1]),
+                "name" => $row[2]
+            ];
+        }
+        $this->data[] = [
+            "apacheCode" => "backup",
+            "ip2countryCode" => "-",
+            "name" => "Fallback Mirror"
+        ];
+        fclose($fid);
+    }
+
+    public function resolveNameApache($code) {
+        $normalized = strtolower($code);
+        foreach($this->data as $element) {
+            if($element['apacheCode'] == $normalized) {
+                return $element['name'];
+            }
+        }
+        return "-";
+    }
+
+    public function resolveApacheCodeFromIP2Country($code) {
+        $normalized = strtolower($code);
+        foreach($this->data as $element) {
+            if($element['ip2countryCode'] == $normalized) {
+                return $element['apacheCode'];
+            }
+        }
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/proxy-chooser/classes/IP2Location.php b/proxy-chooser/classes/IP2Location.php
new file mode 100644
index 0000000..9ec2297
--- /dev/null
+++ b/proxy-chooser/classes/IP2Location.php
@@ -0,0 +1,2261 @@
+<?php
+
+/*
+ * Copyright (C) 2005-2020 IP2Location.com
+ * All Rights Reserved
+ *
+ * This library is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace IP2Location;
+
+/**
+ * IP2Location database class.
+ */
+class Database
+{
+	/**
+	 * Current module's version.
+	 *
+	 * @var string
+	 */
+	public const VERSION = '8.2.2';
+
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//  Error field constants  ///////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	/**
+	 * Unsupported field message.
+	 *
+	 * @var string
+	 */
+	public const FIELD_NOT_SUPPORTED = 'This parameter is unavailable in selected .BIN data file. Please upgrade.';
+
+	/**
+	 * Unknown field message.
+	 *
+	 * @var string
+	 */
+	public const FIELD_NOT_KNOWN = 'This parameter is inexistent. Please verify.';
+
+	/**
+	 * Invalid IP address message.
+	 *
+	 * @var string
+	 */
+	public const INVALID_IP_ADDRESS = 'Invalid IP address.';
+
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//  Field selection constants  ///////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	/**
+	 * Country code (ISO 3166-1 Alpha 2).
+	 *
+	 * @var int
+	 */
+	public const COUNTRY_CODE = 1;
+
+	/**
+	 * Country name.
+	 *
+	 * @var int
+	 */
+	public const COUNTRY_NAME = 2;
+
+	/**
+	 * Region name.
+	 *
+	 * @var int
+	 */
+	public const REGION_NAME = 3;
+
+	/**
+	 * City name.
+	 *
+	 * @var int
+	 */
+	public const CITY_NAME = 4;
+
+	/**
+	 * Latitude.
+	 *
+	 * @var int
+	 */
+	public const LATITUDE = 5;
+
+	/**
+	 * Longitude.
+	 *
+	 * @var int
+	 */
+	public const LONGITUDE = 6;
+
+	/**
+	 * ISP name.
+	 *
+	 * @var int
+	 */
+	public const ISP = 7;
+
+	/**
+	 * Domain name.
+	 *
+	 * @var int
+	 */
+	public const DOMAIN_NAME = 8;
+
+	/**
+	 * Zip code.
+	 *
+	 * @var int
+	 */
+	public const ZIP_CODE = 9;
+
+	/**
+	 * Time zone.
+	 *
+	 * @var int
+	 */
+	public const TIME_ZONE = 10;
+
+	/**
+	 * Net speed.
+	 *
+	 * @var int
+	 */
+	public const NET_SPEED = 11;
+
+	/**
+	 * IDD code.
+	 *
+	 * @var int
+	 */
+	public const IDD_CODE = 12;
+
+	/**
+	 * Area code.
+	 *
+	 * @var int
+	 */
+	public const AREA_CODE = 13;
+
+	/**
+	 * Weather station code.
+	 *
+	 * @var int
+	 */
+	public const WEATHER_STATION_CODE = 14;
+
+	/**
+	 * Weather station name.
+	 *
+	 * @var int
+	 */
+	public const WEATHER_STATION_NAME = 15;
+
+	/**
+	 * Mobile Country Code.
+	 *
+	 * @var int
+	 */
+	public const MCC = 16;
+
+	/**
+	 * Mobile Network Code.
+	 *
+	 * @var int
+	 */
+	public const MNC = 17;
+
+	/**
+	 * Mobile carrier name.
+	 *
+	 * @var int
+	 */
+	public const MOBILE_CARRIER_NAME = 18;
+
+	/**
+	 * Elevation.
+	 *
+	 * @var int
+	 */
+	public const ELEVATION = 19;
+
+	/**
+	 * Usage type.
+	 *
+	 * @var int
+	 */
+	public const USAGE_TYPE = 20;
+
+	/**
+	 * Country name and code.
+	 *
+	 * @var int
+	 */
+	public const COUNTRY = 101;
+
+	/**
+	 * Latitude and Longitude.
+	 *
+	 * @var int
+	 */
+	public const COORDINATES = 102;
+
+	/**
+	 * IDD and area codes.
+	 *
+	 * @var int
+	 */
+	public const IDD_AREA = 103;
+
+	/**
+	 * Weather station name and code.
+	 *
+	 * @var int
+	 */
+	public const WEATHER_STATION = 104;
+
+	/**
+	 * MCC, MNC, and mobile carrier name.
+	 *
+	 * @var int
+	 */
+	public const MCC_MNC_MOBILE_CARRIER_NAME = 105;
+
+	/**
+	 * All fields at once.
+	 *
+	 * @var int
+	 */
+	public const ALL = 1001;
+
+	/**
+	 * Include the IP address of the looked up IP address.
+	 *
+	 * @var int
+	 */
+	public const IP_ADDRESS = 1002;
+
+	/**
+	 * Include the IP version of the looked up IP address.
+	 *
+	 * @var int
+	 */
+	public const IP_VERSION = 1003;
+
+	/**
+	 * Include the IP number of the looked up IP address.
+	 *
+	 * @var int
+	 */
+	public const IP_NUMBER = 1004;
+
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//  Exception code constants  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	/**
+	 * Generic exception code.
+	 *
+	 * @var int
+	 */
+	public const EXCEPTION = 10000;
+
+	/**
+	 * No shmop extension found.
+	 *
+	 * @var int
+	 */
+	public const EXCEPTION_NO_SHMOP = 10001;
+
+	/**
+	 * Failed to open shmop memory segment for reading.
+	 *
+	 * @var int
+	 */
+	public const EXCEPTION_SHMOP_READING_FAILED = 10002;
+
+	/**
+	 * Failed to open shmop memory segment for writing.
+	 *
+	 * @var int
+	 */
+	public const EXCEPTION_SHMOP_WRITING_FAILED = 10003;
+
+	/**
+	 * Failed to create shmop memory segment.
+	 *
+	 * @var int
+	 */
+	public const EXCEPTION_SHMOP_CREATE_FAILED = 10004;
+
+	/**
+	 * The specified database file was not found.
+	 *
+	 * @var int
+	 */
+	public const EXCEPTION_DBFILE_NOT_FOUND = 10005;
+
+	/**
+	 * Not enough memory to load database file.
+	 *
+	 * @var int
+	 */
+	public const EXCEPTION_NO_MEMORY = 10006;
+
+	/**
+	 * No candidate databse files found.
+	 *
+	 * @var int
+	 */
+	public const EXCEPTION_NO_CANDIDATES = 10007;
+
+	/**
+	 * Failed to open database file.
+	 *
+	 * @var int
+	 */
+	public const EXCEPTION_FILE_OPEN_FAILED = 10008;
+
+	/**
+	 * Failed to determine the current path.
+	 *
+	 * @var int
+	 */
+	public const EXCEPTION_NO_PATH = 10009;
+
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//  Caching method constants  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	/**
+	 * Directly read from the databse file.
+	 *
+	 * @var int
+	 */
+	public const FILE_IO = 100001;
+
+	/**
+	 * Read the whole database into a variable for caching.
+	 *
+	 * @var int
+	 */
+	public const MEMORY_CACHE = 100002;
+
+	/**
+	 * Use shared memory objects for caching.
+	 *
+	 * @var int
+	 */
+	public const SHARED_MEMORY = 100003;
+
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//  Shared memory constants  /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	/**
+	 * Share memory segment's permissions (for creation).
+	 *
+	 * @var int
+	 */
+	public const SHM_PERMS = 0600;
+
+	/**
+	 * Number of bytes to read/write at a time in order to load the shared memory cache (512k).
+	 *
+	 * @var int
+	 */
+	public const SHM_CHUNK_SIZE = 524288;
+
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//  Static data  /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	/**
+	 * Column offset mapping.
+	 *
+	 * Each entry contains an array mapping databse version (0--23) to offset within a record.
+	 * A value of 0 means the column is not present in the given database version.
+	 *
+	 * @static
+	 *
+	 * @var array
+	 */
+	private static $columns = [
+		self::COUNTRY_CODE         => [8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8],
+		self::COUNTRY_NAME         => [8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8],
+		self::REGION_NAME          => [0, 0, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12],
+		self::CITY_NAME            => [0, 0, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16],
+		self::LATITUDE             => [0, 0, 0, 0, 20, 20, 0, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20],
+		self::LONGITUDE            => [0, 0, 0, 0, 24, 24, 0, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24],
+		self::ISP                  => [0, 12, 0, 20, 0, 28, 20, 28, 0, 32, 0, 36, 0, 36, 0, 36, 0, 36, 28, 36, 0, 36, 28, 36],
+		self::DOMAIN_NAME          => [0, 0, 0, 0, 0, 0, 24, 32, 0, 36, 0, 40, 0, 40, 0, 40, 0, 40, 32, 40, 0, 40, 32, 40],
+		self::ZIP_CODE             => [0, 0, 0, 0, 0, 0, 0, 0, 28, 28, 28, 28, 0, 28, 28, 28, 0, 28, 0, 28, 28, 28, 0, 28],
+		self::TIME_ZONE            => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 32, 28, 32, 32, 32, 28, 32, 0, 32, 32, 32, 0, 32],
+		self::NET_SPEED            => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 44, 0, 44, 32, 44, 0, 44, 0, 44, 0, 44],
+		self::IDD_CODE             => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 48, 0, 48, 0, 48, 36, 48, 0, 48],
+		self::AREA_CODE            => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 52, 0, 52, 0, 52, 40, 52, 0, 52],
+		self::WEATHER_STATION_CODE => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 56, 0, 56, 0, 56, 0, 56],
+		self::WEATHER_STATION_NAME => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 60, 0, 60, 0, 60, 0, 60],
+		self::MCC                  => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 64, 0, 64, 36, 64],
+		self::MNC                  => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 40, 68, 0, 68, 40, 68],
+		self::MOBILE_CARRIER_NAME  => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 72, 0, 72, 44, 72],
+		self::ELEVATION            => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 44, 76, 0, 76],
+		self::USAGE_TYPE           => [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 80],
+	];
+
+	/**
+	 * Column name mapping.
+	 *
+	 * @static
+	 *
+	 * @var array
+	 */
+	private static $names = [
+		self::COUNTRY_CODE         => 'countryCode',
+		self::COUNTRY_NAME         => 'countryName',
+		self::REGION_NAME          => 'regionName',
+		self::CITY_NAME            => 'cityName',
+		self::LATITUDE             => 'latitude',
+		self::LONGITUDE            => 'longitude',
+		self::ISP                  => 'isp',
+		self::DOMAIN_NAME          => 'domainName',
+		self::ZIP_CODE             => 'zipCode',
+		self::TIME_ZONE            => 'timeZone',
+		self::NET_SPEED            => 'netSpeed',
+		self::IDD_CODE             => 'iddCode',
+		self::AREA_CODE            => 'areaCode',
+		self::WEATHER_STATION_CODE => 'weatherStationCode',
+		self::WEATHER_STATION_NAME => 'weatherStationName',
+		self::MCC                  => 'mcc',
+		self::MNC                  => 'mnc',
+		self::MOBILE_CARRIER_NAME  => 'mobileCarrierName',
+		self::ELEVATION            => 'elevation',
+		self::USAGE_TYPE           => 'usageType',
+		self::IP_ADDRESS           => 'ipAddress',
+		self::IP_VERSION           => 'ipVersion',
+		self::IP_NUMBER            => 'ipNumber',
+	];
+
+	/**
+	 * Database names, in order of preference for file lookup.
+	 *
+	 * @var array
+	 */
+	private static $databases = [
+		// IPv4 databases
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED-AREACODE-WEATHER-MOBILE-ELEVATION-USAGETYPE',
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ISP-DOMAIN-MOBILE-USAGETYPE',
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED-AREACODE-WEATHER-MOBILE-ELEVATION',
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-AREACODE-ELEVATION',
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED-AREACODE-WEATHER-MOBILE',
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ISP-DOMAIN-MOBILE',
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED-AREACODE-WEATHER',
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-NETSPEED-WEATHER',
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED-AREACODE',
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-AREACODE',
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED',
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-TIMEZONE-NETSPEED',
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN',
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE',
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-ISP-DOMAIN',
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE',
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ISP-DOMAIN',
+		'IP-COUNTRY-REGION-CITY-ISP-DOMAIN',
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ISP',
+		'IP-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE',
+		'IP-COUNTRY-REGION-CITY-ISP',
+		'IP-COUNTRY-REGION-CITY',
+		'IP-COUNTRY-ISP',
+		'IP-COUNTRY',
+		// IPv6 databases
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED-AREACODE-WEATHER-MOBILE-ELEVATION-USAGETYPE',
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ISP-DOMAIN-MOBILE-USAGETYPE',
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED-AREACODE-WEATHER-MOBILE-ELEVATION',
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-AREACODE-ELEVATION',
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED-AREACODE-WEATHER-MOBILE',
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ISP-DOMAIN-MOBILE',
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED-AREACODE-WEATHER',
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-NETSPEED-WEATHER',
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED-AREACODE',
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-AREACODE',
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN-NETSPEED',
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-TIMEZONE-NETSPEED',
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE-ISP-DOMAIN',
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-TIMEZONE',
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE-ISP-DOMAIN',
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ZIPCODE',
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ISP-DOMAIN',
+		'IPV6-COUNTRY-REGION-CITY-ISP-DOMAIN',
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE-ISP',
+		'IPV6-COUNTRY-REGION-CITY-LATITUDE-LONGITUDE',
+		'IPV6-COUNTRY-REGION-CITY-ISP',
+		'IPV6-COUNTRY-REGION-CITY',
+		'IPV6-COUNTRY-ISP',
+		'IPV6-COUNTRY',
+	];
+
+	/**
+	 * Static memory buffer to use for MEMORY_CACHE mode, the keys will be BIN filenames and the values their contents.
+	 *
+	 * @static
+	 *
+	 * @var array
+	 */
+	private static $buffer = [];
+
+	/**
+	 * The machine's float size.
+	 *
+	 * @static
+	 *
+	 * @var int
+	 */
+	private static $floatSize = null;
+
+	/**
+	 * The configured memory limit.
+	 *
+	 * @static
+	 *
+	 * @var int
+	 */
+	private static $memoryLimit = null;
+
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//  Caching backend controls  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	/**
+	 * Caching mode to use (one of FILE_IO, MEMORY_CACHE, or SHARED_MEMORY).
+	 *
+	 * @var int
+	 */
+	private $mode;
+
+	/**
+	 * File pointer to use for FILE_IO mode, BIN filename for MEMORY_CACHE mode, or shared memory id to use for SHARED_MEMORY mode.
+	 *
+	 * @var false|int|resource
+	 */
+	private $resource = false;
+
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//  Database metadata  ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	/**
+	 * Database's compilation date.
+	 *
+	 * @var int
+	 */
+	private $date;
+
+	/**
+	 * Database's type (0--23).
+	 *
+	 * @var int
+	 */
+	private $type;
+
+	/**
+	 * Database's register width (as an array mapping 4 to IPv4 width, and 6 to IPv6 width).
+	 *
+	 * @var array
+	 */
+	private $columnWidth = [];
+
+	/**
+	 * Database's pointer offset (as an array mapping 4 to IPv4 offset, and 6 to IPv6 offset).
+	 *
+	 * @var array
+	 */
+	private $offset = [];
+
+	/**
+	 * Amount of IP address ranges the database contains (as an array mapping 4 to IPv4 count, and 6 to IPv6 count).
+	 *
+	 * @var array
+	 */
+	private $ipCount = [];
+
+	/**
+	 * Offset withing the database where IP data begins (as an array mapping 4 to IPv4 base, and 6 to IPv6 base).
+	 *
+	 * @var array
+	 */
+	private $ipBase = [];
+
+	/**
+	 * Base index address.
+	 *
+	 * @var array
+	 */
+	private $indexBaseAddr = [];
+
+	/**
+	 * The year of the database is released.
+	 *
+	 * @var string
+	 */
+	private $year;
+
+	/**
+	 * The month of the database is released.
+	 *
+	 * @var string
+	 */
+	private $month;
+
+	/**
+	 * The day of the database is released.
+	 *
+	 * @var string
+	 */
+	private $day;
+
+	/**
+	 * The raw row of columns's positions.
+	 *
+	 * @var string
+	 */
+	private $rawPositionsRow;
+
+	/**
+	 * IP2Location web service API key.
+	 *
+	 * @var string
+	 */
+	private $apiKey;
+
+	/**
+	 * Web service package.
+	 *
+	 * @var string
+	 */
+	private $package;
+
+	/**
+	 * Either use HTTPS or HTTP.
+	 *
+	 * @var bool
+	 */
+	private $useSsl;
+
+	/**
+	 * Add ons used by the web service.
+	 *
+	 * @var array
+	 */
+	private $addOns = [];
+
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//  Default fields  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	/**
+	 * Default fields to return during lookup.
+	 *
+	 * @var array|int
+	 */
+	private $defaultFields = self::ALL;
+
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//  Administrative public interface  /////////////////////////////////////////////////////////////////////////////////////////////////////
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	/**
+	 * Constructor.
+	 *
+	 * @param string $file          Filename of the BIN database to load
+	 * @param int    $mode          Caching mode (one of FILE_IO, MEMORY_CACHE, or SHARED_MEMORY)
+	 * @param mixed  $defaultFields
+	 *
+	 * @throws \Exception
+	 */
+	public function __construct($file = null, $mode = self::FILE_IO, $defaultFields = self::ALL)
+	{
+		// find the referred file and its size
+		$rfile = self::findFile($file);
+		$size = filesize($rfile);
+
+		// initialize caching backend
+		switch ($mode) {
+		case self::SHARED_MEMORY:
+		// verify the shmop extension is loaded
+		if (!\extension_loaded('shmop')) {
+			throw new \Exception(__CLASS__ . ": Please make sure your PHP setup has the 'shmop' extension enabled.", self::EXCEPTION_NO_SHMOP);
+		}
+
+		$limit = self::getMemoryLimit();
+		if ($limit !== false && $size > $limit) {
+			throw new \Exception(__CLASS__ . ": Insufficient memory to load file '{$rfile}'.", self::EXCEPTION_NO_MEMORY);
+		}
+
+		$this->mode = self::SHARED_MEMORY;
+		$shmKey = self::getShmKey($rfile);
+
+		// try to open the shared memory segment
+		$this->resource = @shmop_open($shmKey, 'a', 0, 0);
+		if ($this->resource === false) {
+			// the segment did not exist, create it and load the database into it
+			$fp = fopen($rfile, 'r');
+			if ($fp === false) {
+				throw new \Exception(__CLASS__ . ": Unable to open file '{$rfile}'.", self::EXCEPTION_FILE_OPEN_FAILED);
+			}
+
+			// try to open the memory segment for exclusive access
+			$shmId = @shmop_open($shmKey, 'n', self::SHM_PERMS, $size);
+			if ($shmId === false) {
+				throw new \Exception(__CLASS__ . ": Unable to create shared memory block '{$shmKey}'.", self::EXCEPTION_SHMOP_CREATE_FAILED);
+			}
+
+			// load SHM_CHUNK_SIZE bytes at a time
+			$pointer = 0;
+			while ($pointer < $size) {
+				$buf = fread($fp, self::SHM_CHUNK_SIZE);
+				shmop_write($shmId, $buf, $pointer);
+				$pointer += self::SHM_CHUNK_SIZE;
+			}
+			shmop_close($shmId);
+			fclose($fp);
+
+			// now open the memory segment for readonly access
+			$this->resource = @shmop_open($shmKey, 'a', 0, 0);
+			if ($this->resource === false) {
+				throw new \Exception(__CLASS__ . ": Unable to access shared memory block '{$shmKey}' for reading.", self::EXCEPTION_SHMOP_READING_FAILED);
+			}
+		}
+		break;
+
+		case self::FILE_IO:
+		$this->mode = self::FILE_IO;
+		$this->resource = @fopen($rfile, 'r');
+		if ($this->resource === false) {
+			throw new \Exception(__CLASS__ . ": Unable to open file '{$rfile}'.", self::EXCEPTION_FILE_OPEN_FAILED);
+		}
+		break;
+
+		case self::MEMORY_CACHE:
+		$this->mode = self::MEMORY_CACHE;
+		$this->resource = $rfile;
+		if (!\array_key_exists($rfile, self::$buffer)) {
+			$limit = self::getMemoryLimit();
+			if ($limit !== false && $size > $limit) {
+				throw new \Exception(__CLASS__ . ": Insufficient memory to load file '{$rfile}'.", self::EXCEPTION_NO_MEMORY);
+			}
+
+			self::$buffer[$rfile] = @file_get_contents($rfile);
+			if (self::$buffer[$rfile] === false) {
+				throw new \Exception(__CLASS__ . ": Unable to open file '{$rfile}'.", self::EXCEPTION_FILE_OPEN_FAILED);
+			}
+		}
+		break;
+
+		default:
+	}
+
+		// determine the platform's float size
+		//
+		// NB: this should be a constant instead, and some unpack / typebanging magic
+		//     should be used to accomodate different float sizes, but, as the libreary
+		//     is written, this is the sanest thing to do anyway
+		//
+		if (self::$floatSize === null) {
+			self::$floatSize = \strlen(pack('f', M_PI));
+		}
+
+		// set default fields to retrieve
+		$this->defaultFields = $defaultFields;
+
+		// extract database metadata
+		$this->type = $this->readByte(1) - 1;
+		$this->columnWidth[4] = $this->readByte(2) * 4;
+		$this->columnWidth[6] = $this->columnWidth[4] + 12;
+		$this->offset[4] = -4;
+		$this->offset[6] = 8;
+
+		$this->year = 2000 + $this->readByte(3);
+		$this->month = $this->readByte(4);
+		$this->day = $this->readByte(5);
+		$this->date = date('Y-m-d', strtotime("{$this->year}-{$this->month}-{$this->day}"));
+
+		$this->ipCount[4] = $this->readWord(6);
+		$this->ipBase[4] = $this->readWord(10);		//hjlim readword
+		$this->ipCount[6] = $this->readWord(14);
+		$this->ipBase[6] = $this->readWord(18);
+		$this->indexBaseAddr[4] = $this->readWord(22);		//hjlim
+	$this->indexBaseAddr[6] = $this->readWord(26);		//hjlim
+	}
+
+	/**
+	 * Destructor.
+	 */
+	public function __destruct()
+	{
+		switch ($this->mode) {
+		case self::FILE_IO:
+		// free the file pointer
+		if ($this->resource !== false) {
+			fclose($this->resource);
+			$this->resource = false;
+		}
+		break;
+		case self::SHARED_MEMORY:
+		// detach from the memory segment
+		if ($this->resource !== false) {
+			shmop_close($this->resource);
+			$this->resource = false;
+		}
+		break;
+	}
+	}
+
+	/**
+	 * Tear down a shared memory segment created for the given file.
+	 *
+	 * @static
+	 *
+	 * @param string $file Filename of the BIN database whise segment must be deleted
+	 *
+	 * @throws \Exception
+	 */
+	public static function shmTeardown($file)
+	{
+		// verify the shmop extension is loaded
+		if (!\extension_loaded('shmop')) {
+			throw new \Exception(__CLASS__ . ": Please make sure your PHP setup has the 'shmop' extension enabled.", self::EXCEPTION_NO_SHMOP);
+		}
+
+		// Get actual file path
+		$rfile = realpath($file);
+
+		// If the file cannot be found, except away
+		if ($rfile === false) {
+			throw new \Exception(__CLASS__ . ": Database file '{$file}' does not seem to exist.", self::EXCEPTION_DBFILE_NOT_FOUND);
+		}
+
+		$shmKey = self::getShmKey($rfile);
+
+		// Try to open the memory segment for writing
+		$shmId = @shmop_open($shmKey, 'w', 0, 0);
+		if ($shmId === false) {
+			throw new \Exception(__CLASS__ . ": Unable to access shared memory block '{$shmKey}' for writing.", self::EXCEPTION_SHMOP_WRITING_FAILED);
+		}
+
+		// Delete and close the descriptor
+		shmop_delete($shmId);
+		shmop_close($shmId);
+	}
+
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//  Public interface  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	/**
+	 * Get the database's compilation date as a string of the form 'YYYY-MM-DD'.
+	 *
+	 * @return string
+	 */
+	public function getDate()
+	{
+		return $this->date;
+	}
+
+	/**
+	 * Get the database's type (1--24).
+	 *
+	 * @return int
+	 */
+	public function getType()
+	{
+		return $this->type + 1;
+	}
+
+	/**
+	 * Return this database's available fields.
+	 *
+	 * @param bool $asNames Whether to return the mapped names intead of numbered constants
+	 *
+	 * @return array
+	 */
+	public function getFields($asNames = false)
+	{
+		$result = array_keys(array_filter(self::$columns, function ($field) {
+			return $field[$this->type] !== 0;
+		}));
+		if ($asNames) {
+			$return = [];
+			foreach ($result as $field) {
+				$return[] = self::$names[$field];
+			}
+
+			return $return;
+		}
+
+		return $result;
+	}
+
+	/**
+	 * Return the version of module.
+	 */
+	public function getModuleVersion()
+	{
+		return self::VERSION;
+	}
+
+	/**
+	 * Return the version of module.
+	 */
+	public function getDatabaseVersion()
+	{
+		return $this->year . '.' . $this->month . '.' . $this->day;
+	}
+
+	/**
+	 * This function will look the given IP address up in the database and return the result(s) asked for.
+	 *
+	 * If a single, SINGULAR, field is specified, only its mapped value is returned.
+	 * If many fields are given (as an array) or a MULTIPLE field is specified, an
+	 * array whith the returned singular field names as keys and their corresponding
+	 * values is returned.
+	 *
+	 * @param string    $ip      IP address to look up
+	 * @param array|int $fields  Field(s) to return
+	 * @param bool      $asNamed Whether to return an associative array instead
+	 *
+	 * @return array|bool|mixed
+	 */
+	public function lookup($ip, $fields = null, $asNamed = true)
+	{
+		// extract IP version and number
+		list($ipVersion, $ipNumber) = self::ipVersionAndNumber($ip);
+		// perform the binary search proper (if the IP address was invalid, binSearch will return false)
+		$pointer = $this->binSearch($ipVersion, $ipNumber);
+		if (empty($pointer)) {
+			return false;
+		}
+
+		// apply defaults if needed
+		if ($fields === null) {
+			$fields = $this->defaultFields;
+		}
+
+		// Get the entire row based on the pointer value
+		// The length of the row differs based on the IP version
+		if ($ipVersion === 4) {
+			$this->rawPositionsRow = $this->read($pointer - 1, $this->columnWidth[4] + 4);
+		} elseif ($ipVersion === 6) {
+			$this->rawPositionsRow = $this->read($pointer - 1, $this->columnWidth[6]);
+		}
+
+		// turn fields into an array in case it wasn't already
+		$ifields = (array) $fields;
+
+		// add fields if needed
+		if (\in_array(self::ALL, $ifields)) {
+			$ifields[] = self::REGION_NAME;
+			$ifields[] = self::CITY_NAME;
+			$ifields[] = self::ISP;
+			$ifields[] = self::DOMAIN_NAME;
+			$ifields[] = self::ZIP_CODE;
+			$ifields[] = self::TIME_ZONE;
+			$ifields[] = self::NET_SPEED;
+			$ifields[] = self::ELEVATION;
+			$ifields[] = self::USAGE_TYPE;
+
+			$ifields[] = self::COUNTRY;
+			$ifields[] = self::COORDINATES;
+			$ifields[] = self::IDD_AREA;
+			$ifields[] = self::WEATHER_STATION;
+			$ifields[] = self::MCC_MNC_MOBILE_CARRIER_NAME;
+
+			$ifields[] = self::IP_ADDRESS;
+			$ifields[] = self::IP_VERSION;
+			$ifields[] = self::IP_NUMBER;
+		}
+		// turn into a uniquely-valued array the fast way
+		// (see: http://php.net/manual/en/function.array-unique.php#77743)
+		$afields = array_keys(array_flip($ifields));
+		// sorting them in reverse order warrants that by the time we get to
+		// SINGULAR fields, its MULTIPLE counterparts, if at all present, have
+		// already been retrieved
+		rsort($afields);
+
+		// maintain a list of already retrieved fields to avoid doing it twice
+		$done = [
+		self::COUNTRY_CODE         => false,
+		self::COUNTRY_NAME         => false,
+		self::REGION_NAME          => false,
+		self::CITY_NAME            => false,
+		self::LATITUDE             => false,
+		self::LONGITUDE            => false,
+		self::ISP                  => false,
+		self::DOMAIN_NAME          => false,
+		self::ZIP_CODE             => false,
+		self::TIME_ZONE            => false,
+		self::NET_SPEED            => false,
+		self::IDD_CODE             => false,
+		self::AREA_CODE            => false,
+		self::WEATHER_STATION_CODE => false,
+		self::WEATHER_STATION_NAME => false,
+		self::MCC                  => false,
+		self::MNC                  => false,
+		self::MOBILE_CARRIER_NAME  => false,
+		self::ELEVATION            => false,
+		self::USAGE_TYPE           => false,
+
+		self::COUNTRY                     => false,
+		self::COORDINATES                 => false,
+		self::IDD_AREA                    => false,
+		self::WEATHER_STATION             => false,
+		self::MCC_MNC_MOBILE_CARRIER_NAME => false,
+
+		self::IP_ADDRESS => false,
+		self::IP_VERSION => false,
+		self::IP_NUMBER  => false,
+	];
+		// results are empty to begin with
+		$results = [];
+
+		// treat each field in turn
+		foreach ($afields as $afield) {
+			switch ($afield) {
+		// purposefully ignore self::ALL, we already dealt with it
+		case self::ALL: break;
+
+		case self::COUNTRY:
+			if (!$done[self::COUNTRY]) {
+				list($results[self::COUNTRY_NAME], $results[self::COUNTRY_CODE]) = $this->readCountryNameAndCode($pointer);
+				$done[self::COUNTRY] = true;
+				$done[self::COUNTRY_CODE] = true;
+				$done[self::COUNTRY_NAME] = true;
+			}
+			break;
+		case self::COORDINATES:
+			if (!$done[self::COORDINATES]) {
+				list($results[self::LATITUDE], $results[self::LONGITUDE]) = $this->readLatitudeAndLongitude($pointer);
+				$done[self::COORDINATES] = true;
+				$done[self::LATITUDE] = true;
+				$done[self::LONGITUDE] = true;
+			}
+			break;
+		case self::IDD_AREA:
+			if (!$done[self::IDD_AREA]) {
+				list($results[self::IDD_CODE], $results[self::AREA_CODE]) = $this->readIddAndAreaCodes($pointer);
+				$done[self::IDD_AREA] = true;
+				$done[self::IDD_CODE] = true;
+				$done[self::AREA_CODE] = true;
+			}
+			break;
+		case self::WEATHER_STATION:
+			if (!$done[self::WEATHER_STATION]) {
+				list($results[self::WEATHER_STATION_NAME], $results[self::WEATHER_STATION_CODE]) = $this->readWeatherStationNameAndCode($pointer);
+				$done[self::WEATHER_STATION] = true;
+				$done[self::WEATHER_STATION_NAME] = true;
+				$done[self::WEATHER_STATION_CODE] = true;
+			}
+			break;
+		case self::MCC_MNC_MOBILE_CARRIER_NAME:
+			if (!$done[self::MCC_MNC_MOBILE_CARRIER_NAME]) {
+				list($results[self::MCC], $results[self::MNC], $results[self::MOBILE_CARRIER_NAME]) = $this->readMccMncAndMobileCarrierName($pointer);
+				$done[self::MCC_MNC_MOBILE_CARRIER_NAME] = true;
+				$done[self::MCC] = true;
+				$done[self::MNC] = true;
+				$done[self::MOBILE_CARRIER_NAME] = true;
+			}
+			break;
+
+		case self::COUNTRY_CODE:
+			if (!$done[self::COUNTRY_CODE]) {
+				$results[self::COUNTRY_CODE] = $this->readCountryNameAndCode($pointer)[1];
+				$done[self::COUNTRY_CODE] = true;
+			}
+			break;
+		case self::COUNTRY_NAME:
+			if (!$done[self::COUNTRY_NAME]) {
+				$results[self::COUNTRY_NAME] = $this->readCountryNameAndCode($pointer)[0];
+				$done[self::COUNTRY_NAME] = true;
+			}
+			break;
+		case self::REGION_NAME:
+			if (!$done[self::REGION_NAME]) {
+				$results[self::REGION_NAME] = $this->readRegionName($pointer);
+				$done[self::REGION_NAME] = true;
+			}
+			break;
+		case self::CITY_NAME:
+			if (!$done[self::CITY_NAME]) {
+				$results[self::CITY_NAME] = $this->readCityName($pointer);
+				$done[self::CITY_NAME] = true;
+			}
+			break;
+		case self::LATITUDE:
+			if (!$done[self::LATITUDE]) {
+				$results[self::LATITUDE] = $this->readLatitudeAndLongitude($pointer)[0];
+				$done[self::LATITUDE] = true;
+			}
+			break;
+		case self::LONGITUDE:
+			if (!$done[self::LONGITUDE]) {
+				$results[self::LONGITUDE] = $this->readLatitudeAndLongitude($pointer)[1];
+				$done[self::LONGITUDE] = true;
+			}
+			break;
+		case self::ISP:
+			if (!$done[self::ISP]) {
+				$results[self::ISP] = $this->readIsp($pointer);
+				$done[self::ISP] = true;
+			}
+			break;
+		case self::DOMAIN_NAME:
+			if (!$done[self::DOMAIN_NAME]) {
+				$results[self::DOMAIN_NAME] = $this->readDomainName($pointer);
+				$done[self::DOMAIN_NAME] = true;
+			}
+			break;
+		case self::ZIP_CODE:
+			if (!$done[self::ZIP_CODE]) {
+				$results[self::ZIP_CODE] = $this->readZipCode($pointer);
+				$done[self::ZIP_CODE] = true;
+			}
+			break;
+		case self::TIME_ZONE:
+			if (!$done[self::TIME_ZONE]) {
+				$results[self::TIME_ZONE] = $this->readTimeZone($pointer);
+				$done[self::TIME_ZONE] = true;
+			}
+			break;
+		case self::NET_SPEED:
+			if (!$done[self::NET_SPEED]) {
+				$results[self::NET_SPEED] = $this->readNetSpeed($pointer);
+				$done[self::NET_SPEED] = true;
+			}
+			break;
+		case self::IDD_CODE:
+			if (!$done[self::IDD_CODE]) {
+				$results[self::IDD_CODE] = $this->readIddAndAreaCodes($pointer)[0];
+				$done[self::IDD_CODE] = true;
+			}
+			break;
+		case self::AREA_CODE:
+			if (!$done[self::AREA_CODE]) {
+				$results[self::AREA_CODE] = $this->readIddAndAreaCodes($pointer)[1];
+				$done[self::AREA_CODE] = true;
+			}
+			break;
+		case self::WEATHER_STATION_CODE:
+			if (!$done[self::WEATHER_STATION_CODE]) {
+				$results[self::WEATHER_STATION_CODE] = $this->readWeatherStationNameAndCode($pointer)[1];
+				$done[self::WEATHER_STATION_CODE] = true;
+			}
+			break;
+		case self::WEATHER_STATION_NAME:
+			if (!$done[self::WEATHER_STATION_NAME]) {
+				$results[self::WEATHER_STATION_NAME] = $this->readWeatherStationNameAndCode($pointer)[0];
+				$done[self::WEATHER_STATION_NAME] = true;
+			}
+			break;
+		case self::MCC:
+			if (!$done[self::MCC]) {
+				$results[self::MCC] = $this->readMccMncAndMobileCarrierName($pointer)[0];
+				$done[self::MCC] = true;
+			}
+			break;
+		case self::MNC:
+			if (!$done[self::MNC]) {
+				$results[self::MNC] = $this->readMccMncAndMobileCarrierName($pointer)[1];
+				$done[self::MNC] = true;
+			}
+			break;
+		case self::MOBILE_CARRIER_NAME:
+			if (!$done[self::MOBILE_CARRIER_NAME]) {
+				$results[self::MOBILE_CARRIER_NAME] = $this->readMccMncAndMobileCarrierName($pointer)[2];
+				$done[self::MOBILE_CARRIER_NAME] = true;
+			}
+			break;
+		case self::ELEVATION:
+			if (!$done[self::ELEVATION]) {
+				$results[self::ELEVATION] = $this->readElevation($pointer);
+				$done[self::ELEVATION] = true;
+			}
+			break;
+		case self::USAGE_TYPE:
+			if (!$done[self::USAGE_TYPE]) {
+				$results[self::USAGE_TYPE] = $this->readUsageType($pointer);
+				$done[self::USAGE_TYPE] = true;
+			}
+			break;
+
+		case self::IP_ADDRESS:
+			if (!$done[self::IP_ADDRESS]) {
+				$results[self::IP_ADDRESS] = $ip;
+				$done[self::IP_ADDRESS] = true;
+			}
+			break;
+		case self::IP_VERSION:
+			if (!$done[self::IP_VERSION]) {
+				$results[self::IP_VERSION] = $ipVersion;
+				$done[self::IP_VERSION] = true;
+			}
+			break;
+		case self::IP_NUMBER:
+			if (!$done[self::IP_NUMBER]) {
+				$results[self::IP_NUMBER] = $ipNumber;
+				$done[self::IP_NUMBER] = true;
+			}
+			break;
+
+		default:
+			$results[$afield] = self::FIELD_NOT_KNOWN;
+		}
+		}
+
+		// If we were asked for an array, or we have multiple results to return...
+		if (\is_array($fields) || \count($results) > 1) {
+			// return array
+			if ($asNamed) {
+				// apply translations if needed
+				$return = [];
+				foreach ($results as $key => $val) {
+					if (\array_key_exists($key, static::$names)) {
+						$return[static::$names[$key]] = $val;
+					} else {
+						$return[$key] = $val;
+					}
+				}
+
+				return $return;
+			}
+
+			return $results;
+		}
+		// return a single value
+		return array_values($results)[0];
+	}
+
+	/**
+	 * For a given IP address, returns the cidr of his sub-network.
+	 *
+	 * @param string $ip
+	 *
+	 * @return array
+	 * */
+	public function getCidr($ip)
+	{
+		// Extract IP version and number
+		list($ipVersion, $ipNumber) = self::ipVersionAndNumber($ip);
+
+		// Perform the binary search proper (if the IP address was invalid, binSearch will return false)
+		$records = $this->binSearch($ipVersion, $ipNumber, true);
+		if (!empty($records)) {
+			$result = [];
+
+			list($ipFrom, $ipTo) = $records;
+
+			$ipTo -= 1;
+
+			while ($ipTo >= $ipFrom) {
+				$maxSize = self::getMaxSize($ipFrom, 32);
+				$x = log($ipTo - $ipFrom + 1) / log(2);
+				$maxDiff = floor(32 - floor($x));
+
+				$ip = long2ip($ipFrom);
+
+				if ($maxSize < $maxDiff) {
+					$maxSize = $maxDiff;
+				}
+
+				$result[] = $ip . '/' . $maxSize;
+				$ipFrom += pow(2, (32 - $maxSize));
+			}
+
+			return $result;
+		}
+
+		return false;
+	}
+
+	/**
+	 * For a given IP address, returns the cidr of his sub-network. (This function will be deprecated in next version).
+	 *
+	 * For example, calling get_cidr('91.200.12.233') returns '91.200.0.0/13'.
+	 * Useful to setup "Deny From 91.200.0.0/13" in .htaccess file for Apache2
+	 * server against spam.
+	 *
+	 * @param mixed $ip
+	 * */
+	public function get_cidr($ip)
+	{
+		// Extract IP version and number
+		list($ipVersion, $ipNumber) = self::ipVersionAndNumber($ip);
+
+		// Perform the binary search proper (if the IP address was invalid, binSearch will return false)
+
+		$resp = $this->binSearch($ipVersion, $ipNumber, true);
+		if (!empty($resp)) {
+			list($ipFrom, $ipTo) = $resp;
+			$i = 32;
+			$mask = 1;
+			while (($ipTo & $mask) == 0) {
+				$mask *= 2;
+				--$i;
+			}
+			$ip = long2ip($ipFrom);
+
+			return "$ip/$i";
+		}
+
+		return false;
+	}
+
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//  Static tools  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	/**
+	 * Rerturn maxinum size of a net block.
+	 *
+	 * @param int $base the base number
+	 * @param int $bit  the bit number
+	 * @static
+	 *
+	 * @return bool|int
+	 */
+	private static function getMaxSize($base, $bit)
+	{
+		while ($bit > 0) {
+			$decimal = hexdec(base_convert((pow(2, 32) - pow(2, (32 - ($bit - 1)))), 10, 16));
+
+			if (($base & $decimal) != $base) {
+				break;
+			}
+			--$bit;
+		}
+
+		return $bit;
+	}
+
+	/**
+	 * Get memory limit from the current PHP settings (return false if no memory limit set).
+	 *
+	 * @static
+	 *
+	 * @return bool|int
+	 */
+	private static function getMemoryLimit()
+	{
+		// Get values if no cache
+		if (self::$memoryLimit === null) {
+			$limit = ini_get('memory_limit');
+
+			// Feal with defaults
+			if ((string) $limit === '') {
+				$limit = '128M';
+			}
+
+			$value = (int) $limit;
+
+			// Deal with "no-limit"
+			if ($value < 0) {
+				$value = false;
+			} else {
+				// Deal with shorthand bytes
+				switch (strtoupper(substr($limit, -1))) {
+			case 'G': $value *= 1024;
+			// no break
+			case 'M': $value *= 1024;
+			// no break
+			case 'K': $value *= 1024;
+		}
+			}
+			self::$memoryLimit = $value;
+		}
+
+		return self::$memoryLimit;
+	}
+
+	/**
+	 * Return the realpath of the given file or look for the first matching database option.
+	 *
+	 * @param string $file File to try to find, or null to try the databases in turn on the current file's path
+	 *
+	 * @throws \Exception
+	 *
+	 * @return string
+	 */
+	private static function findFile($file = null)
+	{
+		if ($file !== null) {
+			// Get actual file path
+			$rfile = realpath($file);
+
+			// If the file cannot be found, except away
+			if ($rfile === false) {
+				throw new \Exception(__CLASS__ . ": Database file '{$file}' does not seem to exist.", self::EXCEPTION_DBFILE_NOT_FOUND);
+			}
+
+			return $rfile;
+		}
+		// Try to get current path
+		$current = realpath(__DIR__);
+
+		if ($current === false) {
+			throw new \Exception(__CLASS__ . ': Cannot determine current path.', self::EXCEPTION_NO_PATH);
+		}
+		// Try each database in turn
+		foreach (self::$databases as $database) {
+			$rfile = realpath("{$current}/{$database}.BIN");
+			if ($rfile !== false) {
+				return $rfile;
+			}
+		}
+		// No candidates found
+		throw new \Exception(__CLASS__ . ': No candidate database files found.', self::EXCEPTION_NO_CANDIDATES);
+	}
+
+	/**
+	 * Make the given number positive by wrapping it to 8 bit values.
+	 *
+	 * @static
+	 *
+	 * @param int $x Number to wrap
+	 *
+	 * @return int
+	 */
+	private static function wrap8($x)
+	{
+		return $x + ($x < 0 ? 256 : 0);
+	}
+
+	/**
+	 * Make the given number positive by wrapping it to 32 bit values.
+	 *
+	 * @static
+	 *
+	 * @param int $x Number to wrap
+	 *
+	 * @return int
+	 */
+	private static function wrap32($x)
+	{
+		return $x + ($x < 0 ? 4294967296 : 0);
+	}
+
+	/**
+	 * Generate a unique and repeatable shared memory key for each instance to use.
+	 *
+	 * @static
+	 *
+	 * @param string $filename Filename of the BIN file
+	 *
+	 * @return int
+	 */
+	private static function getShmKey($filename)
+	{
+		// This will create a shared memory key that deterministically depends only on
+		// the current file's path and the BIN file's path
+		return (int) sprintf('%u', self::wrap32(crc32(__FILE__ . ':' . $filename)));
+	}
+
+	/**
+	 * Determine whether the given IP number of the given version lies between the given bounds.
+	 *
+	 * This function will return 0 if the given ip number falls within the given bounds
+	 * for the given version, -1 if it falls below, and 1 if it falls above.
+	 *
+	 * @static
+	 *
+	 * @param int        $version IP version to use (either 4 or 6)
+	 * @param int|string $ip      IP number to check (int for IPv4, string for IPv6)
+	 * @param int|string $low     Lower bound (int for IPv4, string for IPv6)
+	 * @param int|string $high    Uppoer bound (int for IPv4, string for IPv6)
+	 *
+	 * @return int
+	 */
+	private static function ipBetween($version, $ip, $low, $high)
+	{
+		if ($version === 4) {
+			// Use normal PHP ints
+			if ($low <= $ip) {
+				if ($ip < $high) {
+					return 0;
+				}
+
+				return 1;
+			}
+
+			return -1;
+		}
+		// Use BCMath
+		if (bccomp($low, $ip, 0) <= 0) {
+			if (bccomp($ip, $high, 0) <= -1) {
+				return 0;
+			}
+
+			return 1;
+		}
+
+		return -1;
+	}
+
+	/**
+	 * Get the IP version and number of the given IP address.
+	 *
+	 * This method will return an array, whose components will be:
+	 * - first: 4 if the given IP address is an IPv4 one, 6 if it's an IPv6 one,
+	 *          or false if it's neither.
+	 * - second: the IP address' number if its version is 4, the number string if
+	 *           its version is 6, false otherwise.
+	 *
+	 * @static
+	 *
+	 * @param string $ip IP address to extract the version and number for
+	 *
+	 * @return array
+	 */
+	private static function ipVersionAndNumber($ip)
+	{
+		if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
+			return [4, sprintf('%u', ip2long($ip))];
+		} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
+			$result = 0;
+
+			foreach (str_split(bin2hex(inet_pton($ip)), 8) as $word) {
+				$result = bcadd(bcmul($result, '4294967296', 0), self::wrap32(hexdec($word)), 0);
+			}
+
+			return [6, $result];
+		}
+		// Invalid IP address, return falses
+		return [false, false];
+	}
+
+	/**
+	 * Return the decimal string representing the binary data given.
+	 *
+	 * @static
+	 *
+	 * @param string $data Binary data to parse
+	 *
+	 * @return string
+	 */
+	private static function bcBin2Dec($data)
+	{
+		$parts = [
+		unpack('V', substr($data, 12, 4)),
+		unpack('V', substr($data, 8, 4)),
+		unpack('V', substr($data, 4, 4)),
+		unpack('V', substr($data, 0, 4)),
+	];
+
+		foreach ($parts as &$part) {
+			if ($part[1] < 0) {
+				$part[1] += 4294967296;
+			}
+		}
+
+		$result = bcadd(bcadd(bcmul($parts[0][1], bcpow(4294967296, 3)), bcmul($parts[1][1], bcpow(4294967296, 2))), bcadd(bcmul($parts[2][1], 4294967296), $parts[3][1]));
+
+		return $result;
+	}
+
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//  Caching backend abstraction  /////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	/**
+	 * Low level read function to abstract away the caching mode being used.
+	 *
+	 * @param int $pos Position from where to start reading
+	 * @param int $len Read this many bytes
+	 *
+	 * @return string
+	 */
+	private function read($pos, $len)
+	{
+		switch ($this->mode) {
+		case self::SHARED_MEMORY:
+		return shmop_read($this->resource, $pos, $len);
+
+		case self::MEMORY_CACHE:
+		return $data = substr(self::$buffer[$this->resource], $pos, $len);
+
+		default:
+		fseek($this->resource, $pos, SEEK_SET);
+
+		return fread($this->resource, $len);
+	}
+	}
+
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//  Low-level read functions  ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	/**
+	 * Low level function to fetch a string from the caching backend.
+	 *
+	 * @param int $pos        Position to read from
+	 * @param int $additional Additional offset to apply
+	 *
+	 * @return string
+	 */
+	private function readString($pos, $additional = 0)
+	{
+		// Get the actual pointer to the string's head by extract from the raw row
+		$spos = unpack('V', substr($this->rawPositionsRow, $pos, 4))[1] + $additional;
+
+		// Read as much as the length (first "string" byte) indicates
+		return $this->read($spos + 1, $this->readByte($spos + 1));
+	}
+
+	/**
+	 * Low level function to fetch a float from the caching backend.
+	 *
+	 * @param int $pos Position to read from
+	 *
+	 * @return float
+	 */
+	private function readFloat($pos)
+	{
+		// Unpack a float's size worth of data
+		return unpack('f', substr($this->rawPositionsRow, $pos, self::$floatSize))[1];
+	}
+
+	/**
+	 * Low level function to fetch a quadword (128 bits) from the caching backend.
+	 *
+	 * @param int $pos Position to read from
+	 *
+	 * @return string
+	 */
+	private function readQuad($pos)
+	{
+		// Use BCMath ints to get a quad's (128-bit) value
+		return self::bcBin2Dec($this->read($pos - 1, 16));
+	}
+
+	/**
+	 * Low level function to fetch a word (32 bits) from the caching backend.
+	 *
+	 * @param int $pos Position to read from
+	 *
+	 * @return int
+	 */
+	private function readWord($pos)
+	{
+		// Unpack a long's worth of data
+		return self::wrap32(unpack('V', $this->read($pos - 1, 4))[1]);
+	}
+
+	/**
+	 * Low level function to fetch a byte from the caching backend.
+	 *
+	 * @param int $pos Position to read from
+	 *
+	 * @return string
+	 */
+	private function readByte($pos)
+	{
+		// Unpack a byte's worth of data
+		return self::wrap8(unpack('C', $this->read($pos - 1, 1))[1]);
+	}
+
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//  High-level read functions  ///////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	/**
+	 * High level function to fetch the country name and code.
+	 *
+	 * @param bool|int $pointer Position to read from, if false, return self::INVALID_IP_ADDRESS
+	 *
+	 * @return array
+	 */
+	private function readCountryNameAndCode($pointer)
+	{
+		if ($pointer === false) {
+			// Deal with invalid IPs
+			$countryCode = self::INVALID_IP_ADDRESS;
+			$countryName = self::INVALID_IP_ADDRESS;
+		} elseif (self::$columns[self::COUNTRY_CODE][$this->type] === 0) {
+			// If the field is not suported, return accordingly
+			$countryCode = self::FIELD_NOT_SUPPORTED;
+			$countryName = self::FIELD_NOT_SUPPORTED;
+		} else {
+			// Read the country code and name (the name shares the country's pointer,
+			// but it must be artificially displaced 3 bytes ahead: 2 for the country code, one
+			// for the country name's length)
+			$countryCode = $this->readString(self::$columns[self::COUNTRY_CODE][$this->type]);
+			$countryName = $this->readString(self::$columns[self::COUNTRY_NAME][$this->type], 3);
+		}
+
+		return [$countryName, $countryCode];
+	}
+
+	/**
+	 * High level function to fetch the region name.
+	 *
+	 * @param int $pointer Position to read from, if false, return self::INVALID_IP_ADDRESS
+	 *
+	 * @return string
+	 */
+	private function readRegionName($pointer)
+	{
+		if ($pointer === false) {
+			// Deal with invalid IPs
+			$regionName = self::INVALID_IP_ADDRESS;
+		} elseif (self::$columns[self::REGION_NAME][$this->type] === 0) {
+			// If the field is not suported, return accordingly
+			$regionName = self::FIELD_NOT_SUPPORTED;
+		} else {
+			// Read the region name
+			$regionName = $this->readString(self::$columns[self::REGION_NAME][$this->type]);
+		}
+
+		return $regionName;
+	}
+
+	/**
+	 * High level function to fetch the city name.
+	 *
+	 * @param int $pointer Position to read from, if false, return self::INVALID_IP_ADDRESS
+	 *
+	 * @return string
+	 */
+	private function readCityName($pointer)
+	{
+		if ($pointer === false) {
+			// Deal with invalid IPs
+			$cityName = self::INVALID_IP_ADDRESS;
+		} elseif (self::$columns[self::CITY_NAME][$this->type] === 0) {
+			// If the field is not suported, return accordingly
+			$cityName = self::FIELD_NOT_SUPPORTED;
+		} else {
+			// Read the city name
+			$cityName = $this->readString(self::$columns[self::CITY_NAME][$this->type]);
+		}
+
+		return $cityName;
+	}
+
+	/**
+	 * High level function to fetch the latitude and longitude.
+	 *
+	 * @param int $pointer Position to read from, if false, return self::INVALID_IP_ADDRESS
+	 *
+	 * @return array
+	 */
+	private function readLatitudeAndLongitude($pointer)
+	{
+		if ($pointer === false) {
+			// Deal with invalid IPs
+			$latitude = self::INVALID_IP_ADDRESS;
+			$longitude = self::INVALID_IP_ADDRESS;
+		} elseif (self::$columns[self::LATITUDE][$this->type] === 0) {
+			// If the field is not suported, return accordingly
+			$latitude = self::FIELD_NOT_SUPPORTED;
+			$longitude = self::FIELD_NOT_SUPPORTED;
+		} else {
+			// Read latitude and longitude
+			$latitude = round($this->readFloat(self::$columns[self::LATITUDE][$this->type]), 6);
+			$longitude = round($this->readFloat(self::$columns[self::LONGITUDE][$this->type]), 6);
+		}
+
+		return [$latitude, $longitude];
+	}
+
+	/**
+	 * High level function to fetch the ISP name.
+	 *
+	 * @param int $pointer Position to read from, if false, return self::INVALID_IP_ADDRESS
+	 *
+	 * @return string
+	 */
+	private function readIsp($pointer)
+	{
+		if ($pointer === false) {
+			// Deal with invalid IPs
+			$isp = self::INVALID_IP_ADDRESS;
+		} elseif (self::$columns[self::ISP][$this->type] === 0) {
+			// If the field is not suported, return accordingly
+			$isp = self::FIELD_NOT_SUPPORTED;
+		} else {
+			// Read isp name
+			$isp = $this->readString(self::$columns[self::ISP][$this->type]);
+		}
+
+		return $isp;
+	}
+
+	/**
+	 * High level function to fetch the domain name.
+	 *
+	 * @param int $pointer Position to read from, if false, return self::INVALID_IP_ADDRESS
+	 *
+	 * @return string
+	 */
+	private function readDomainName($pointer)
+	{
+		if ($pointer === false) {
+			// Deal with invalid IPs
+			$domainName = self::INVALID_IP_ADDRESS;
+		} elseif (self::$columns[self::DOMAIN_NAME][$this->type] === 0) {
+			// If the field is not suported, return accordingly
+			$domainName = self::FIELD_NOT_SUPPORTED;
+		} else {
+			// Read the domain name
+			$domainName = $this->readString(self::$columns[self::DOMAIN_NAME][$this->type]);
+		}
+
+		return $domainName;
+	}
+
+	/**
+	 * High level function to fetch the zip code.
+	 *
+	 * @param int $pointer Position to read from, if false, return self::INVALID_IP_ADDRESS
+	 *
+	 * @return string
+	 */
+	private function readZipCode($pointer)
+	{
+		if ($pointer === false) {
+			// Deal with invalid IPs
+			$zipCode = self::INVALID_IP_ADDRESS;
+		} elseif (self::$columns[self::ZIP_CODE][$this->type] === 0) {
+			// If the field is not suported, return accordingly
+			$zipCode = self::FIELD_NOT_SUPPORTED;
+		} else {
+			// Read the zip code
+			$zipCode = $this->readString(self::$columns[self::ZIP_CODE][$this->type]);
+		}
+
+		return $zipCode;
+	}
+
+	/**
+	 * High level function to fetch the time zone.
+	 *
+	 * @param int $pointer Position to read from, if false, return self::INVALID_IP_ADDRESS
+	 *
+	 * @return string
+	 */
+	private function readTimeZone($pointer)
+	{
+		if ($pointer === false) {
+			// Deal with invalid IPs
+			$timeZone = self::INVALID_IP_ADDRESS;
+		} elseif (self::$columns[self::TIME_ZONE][$this->type] === 0) {
+			// If the field is not suported, return accordingly
+			$timeZone = self::FIELD_NOT_SUPPORTED;
+		} else {
+			// Read the time zone
+			$timeZone = $this->readString(self::$columns[self::TIME_ZONE][$this->type]);
+		}
+
+		return $timeZone;
+	}
+
+	/**
+	 * High level function to fetch the net speed.
+	 *
+	 * @param int $pointer Position to read from, if false, return self::INVALID_IP_ADDRESS
+	 *
+	 * @return string
+	 */
+	private function readNetSpeed($pointer)
+	{
+		if ($pointer === false) {
+			// Deal with invalid IPs
+			$netSpeed = self::INVALID_IP_ADDRESS;
+		} elseif (self::$columns[self::NET_SPEED][$this->type] === 0) {
+			// If the field is not suported, return accordingly
+			$netSpeed = self::FIELD_NOT_SUPPORTED;
+		} else {
+			// Read the net speed
+			$netSpeed = $this->readString(self::$columns[self::NET_SPEED][$this->type]);
+		}
+
+		return $netSpeed;
+	}
+
+	/**
+	 * High level function to fetch the IDD and area codes.
+	 *
+	 * @param int $pointer Position to read from, if false, return self::INVALID_IP_ADDRESS
+	 *
+	 * @return array
+	 */
+	private function readIddAndAreaCodes($pointer)
+	{
+		if ($pointer === false) {
+			// Deal with invalid IPs
+			$iddCode = self::INVALID_IP_ADDRESS;
+			$areaCode = self::INVALID_IP_ADDRESS;
+		} elseif (self::$columns[self::IDD_CODE][$this->type] === 0) {
+			// If the field is not suported, return accordingly
+			$iddCode = self::FIELD_NOT_SUPPORTED;
+			$areaCode = self::FIELD_NOT_SUPPORTED;
+		} else {
+			// Read IDD and area codes
+			$iddCode = $this->readString(self::$columns[self::IDD_CODE][$this->type]);
+			$areaCode = $this->readString(self::$columns[self::AREA_CODE][$this->type]);
+		}
+
+		return [$iddCode, $areaCode];
+	}
+
+	/**
+	 * High level function to fetch the weather station name and code.
+	 *
+	 * @param int $pointer Position to read from, if false, return self::INVALID_IP_ADDRESS
+	 *
+	 * @return array
+	 */
+	private function readWeatherStationNameAndCode($pointer)
+	{
+		if ($pointer === false) {
+			// Deal with invalid IPs
+			$weatherStationName = self::INVALID_IP_ADDRESS;
+			$weatherStationCode = self::INVALID_IP_ADDRESS;
+		} elseif (self::$columns[self::WEATHER_STATION_NAME][$this->type] === 0) {
+			// If the field is not suported, return accordingly
+			$weatherStationName = self::FIELD_NOT_SUPPORTED;
+			$weatherStationCode = self::FIELD_NOT_SUPPORTED;
+		} else {
+			// Read weather station name and code
+			$weatherStationName = $this->readString(self::$columns[self::WEATHER_STATION_NAME][$this->type]);
+			$weatherStationCode = $this->readString(self::$columns[self::WEATHER_STATION_CODE][$this->type]);
+		}
+
+		return [$weatherStationName, $weatherStationCode];
+	}
+
+	/**
+	 * High level function to fetch the MCC, MNC, and mobile carrier name.
+	 *
+	 * @param int $pointer Position to read from, if false, return self::INVALID_IP_ADDRESS
+	 *
+	 * @return array
+	 */
+	private function readMccMncAndMobileCarrierName($pointer)
+	{
+		if ($pointer === false) {
+			// Deal with invalid IPs
+			$mcc = self::INVALID_IP_ADDRESS;
+			$mnc = self::INVALID_IP_ADDRESS;
+			$mobileCarrierName = self::INVALID_IP_ADDRESS;
+		} elseif (self::$columns[self::MCC][$this->type] === 0) {
+			// If the field is not suported, return accordingly
+			$mcc = self::FIELD_NOT_SUPPORTED;
+			$mnc = self::FIELD_NOT_SUPPORTED;
+			$mobileCarrierName = self::FIELD_NOT_SUPPORTED;
+		} else {
+			// Read MCC, MNC, and mobile carrier name
+			$mcc = $this->readString(self::$columns[self::MCC][$this->type]);
+			$mnc = $this->readString(self::$columns[self::MNC][$this->type]);
+			$mobileCarrierName = $this->readString(self::$columns[self::MOBILE_CARRIER_NAME][$this->type]);
+		}
+
+		return [$mcc, $mnc, $mobileCarrierName];
+	}
+
+	/**
+	 * High level function to fetch the elevation.
+	 *
+	 * @param int $pointer Position to read from, if false, return self::INVALID_IP_ADDRESS
+	 *
+	 * @return string
+	 */
+	private function readElevation($pointer)
+	{
+		if ($pointer === false) {
+			// Deal with invalid IPs
+			$elevation = self::INVALID_IP_ADDRESS;
+		} elseif (self::$columns[self::ELEVATION][$this->type] === 0) {
+			// If the field is not suported, return accordingly
+			$elevation = self::FIELD_NOT_SUPPORTED;
+		} else {
+			// Read the elevation
+			$elevation = $this->readString(self::$columns[self::ELEVATION][$this->type]);
+		}
+
+		return $elevation;
+	}
+
+	/**
+	 * High level function to fetch the usage type.
+	 *
+	 * @param int $pointer Position to read from, if false, return self::INVALID_IP_ADDRESS
+	 *
+	 * @return string
+	 */
+	private function readUsageType($pointer)
+	{
+		if ($pointer === false) {
+			// Deal with invalid IPs
+			$usageType = self::INVALID_IP_ADDRESS;
+		} elseif (self::$columns[self::USAGE_TYPE][$this->type] === 0) {
+			// If the field is not suported, return accordingly
+			$usageType = self::FIELD_NOT_SUPPORTED;
+		} else {
+			$usageType = $this->readString(self::$columns[self::USAGE_TYPE][$this->type]);
+		}
+
+		return $usageType;
+	}
+
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+	//  Binary search and support functions  /////////////////////////////////////////////////////////////////////////////////////////////////
+	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+	/**
+	 * High level fucntion to read an IP address of the given version.
+	 *
+	 * @param int $version IP version to read (either 4 or 6, returns false on anything else)
+	 * @param int $pos     Position to read from
+	 *
+	 * @return bool|int|string
+	 */
+	private function readIp($version, $pos)
+	{
+		if ($version === 4) {
+			// Read a standard PHP int
+			return self::wrap32($this->readWord($pos));
+		} elseif ($version === 6) {
+			// Read as BCMath int (quad)
+			return $this->readQuad($pos);
+		}
+		// unrecognized
+		return false;
+	}
+
+	/**
+	 * Perform a binary search on the given IP number and return a pointer to its record.
+	 *
+	 * @param int   $version  IP version to use for searching
+	 * @param int   $ipNumber IP number to look for
+	 * @param mixed $cidr
+	 *
+	 * @return bool|int
+	 */
+	private function binSearch($version, $ipNumber, $cidr = false)
+	{
+		if ($version === false) {
+			// unrecognized version
+			return false;
+		}
+
+		// initialize fields
+		$base = $this->ipBase[$version];
+		$offset = $this->offset[$version];
+		$width = $this->columnWidth[$version];
+		$high = $this->ipCount[$version];
+		$low = 0;
+
+		//hjlim
+		$indexBaseStart = $this->indexBaseAddr[$version];
+		if ($indexBaseStart > 0) {
+			$indexPos = 0;
+			switch ($version) {
+			case 4:
+				$ipNum1_2 = (int) ($ipNumber / 65536);
+				$indexPos = $indexBaseStart + ($ipNum1_2 << 3);
+
+				break;
+
+			case 6:
+				$ipNum1 = (int) (bcdiv($ipNumber, bcpow('2', '112')));
+				$indexPos = $indexBaseStart + ($ipNum1 << 3);
+
+				break;
+
+			default:
+				return false;
+		}
+
+			$low = $this->readWord($indexPos);
+			$high = $this->readWord($indexPos + 4);
+		}
+
+		// as long as we can narrow down the search...
+		while ($low <= $high) {
+			$mid = (int) ($low + (($high - $low) >> 1));
+
+			// Read IP ranges to get boundaries
+			$ip_from = $this->readIp($version, $base + $width * $mid);
+			$ip_to = $this->readIp($version, $base + $width * ($mid + 1));
+
+			// determine whether to return, repeat on the lower half, or repeat on the upper half
+			switch (self::ipBetween($version, $ipNumber, $ip_from, $ip_to)) {
+		case 0:
+			return ($cidr) ? [$ip_from, $ip_to] : $base + $offset + $mid * $width;
+		case -1:
+			$high = $mid - 1;
+			break;
+		case 1:
+			$low = $mid + 1;
+			break;
+	}
+		}
+
+		// nothing found
+		return false;
+	}
+}
+
+/**
+ * IP2Location web service class.
+ */
+class WebService
+{
+	/**
+	 * No cURL extension found.
+	 *
+	 * @var int
+	 */
+	public const EXCEPTION_NO_CURL = 10001;
+
+	/**
+	 * Invalid API key format.
+	 *
+	 * @var int
+	 */
+	public const EXCEPTION_INVALID_API_KEY = 10002;
+
+	/**
+	 * Web service error.
+	 *
+	 * @var int
+	 */
+	public const EXCEPTION_WEB_SERVICE_ERROR = 10003;
+
+	/**
+	 * Constructor.
+	 *
+	 * @param string $apiKey  API key of your IP2Location web service
+	 * @param string $package Supported IP2Location package from WS1 to WS24
+	 * @param bool   $useSsl  Enable or disabled HTTPS connection. HTTP is faster but less secure.
+	 *
+	 * @throws \Exception
+	 */
+	public function __construct($apiKey, $package = 'WS1', $useSsl = false)
+	{
+		if (!\extension_loaded('curl')) {
+			throw new \Exception(__CLASS__ . ": Please make sure your PHP setup has the 'curl' extension enabled.", self::EXCEPTION_NO_CURL);
+		}
+
+		if (!preg_match('/^[0-9A-Z]{10}$/', $apiKey) && $apiKey != 'demo') {
+			throw new \Exception(__CLASS__ . ': Please provide a valid IP2Location web service API key.', self::EXCEPTION_INVALID_API_KEY);
+		}
+
+		if (!preg_match('/^WS[0-9]+$/', $package)) {
+			$package = 'WS1';
+		}
+
+		$this->apiKey = $apiKey;
+		$this->package = $package;
+		$this->useSsl = $useSsl;
+	}
+
+	/**
+	 * This function will look the given IP address up in IP2Location web service.
+	 *
+	 * @param string $ip       IP address to look up
+	 * @param array  $addOns   Extra fields to return. Please refer to https://www.ip2location.com/web-service/ip2location
+	 * @param string $language the translation for continent, country, region and city name for the addon package
+	 *
+	 * @throws \Exception
+	 *
+	 * @return array|false
+	 */
+	public function lookup($ip, $addOns = [], $language = 'en')
+	{
+		$response = $this->httpRequest('http://api.ip2location.com/v2/?' . http_build_query([
+			'key'     => $this->apiKey,
+			'ip'      => $ip,
+			'package' => $this->package,
+			'addon'   => implode(',', $addOns),
+			'lang'    => $language,
+		]));
+
+		if (($data = json_decode($response, true)) === null) {
+			return false;
+		}
+
+		if (isset($data['response'])) {
+			throw new \Exception(__CLASS__ . ': ' . $data['response'], self::EXCEPTION_WEB_SERVICE_ERROR);
+		}
+
+		return $data;
+	}
+
+	/**
+	 * Get the remaing credit in your IP2Location web service account.
+	 *
+	 * @return int
+	 */
+	public function getCredit()
+	{
+		$response = $this->httpRequest('http://api.ip2location.com/v2/?' . http_build_query([
+			'key'   => $this->apiKey,
+			'check' => true,
+		]));
+
+		if (($data = json_decode($response, true)) === null) {
+			return 0;
+		}
+
+		if (!isset($data['response'])) {
+			return 0;
+		}
+
+		return $data['response'];
+	}
+
+	/**
+	 * Open a remote web address.
+	 *
+	 * @param string $url Website URL
+	 *
+	 * @return bool|string
+	 */
+	private function httpRequest($url)
+	{
+		$ch = curl_init();
+		curl_setopt($ch, CURLOPT_URL, $url);
+		curl_setopt($ch, CURLOPT_FAILONERROR, 1);
+		curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
+		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
+		curl_setopt($ch, CURLOPT_TIMEOUT, 30);
+
+		$response = curl_exec($ch);
+
+		if (!curl_errno($ch)) {
+			curl_close($ch);
+
+			return $response;
+		}
+
+		curl_close($ch);
+
+		return false;
+	}
+}
diff --git a/proxy-chooser/classes/apache_proxy_countries.csv b/proxy-chooser/classes/apache_proxy_countries.csv
new file mode 100644
index 0000000..c749a0d
--- /dev/null
+++ b/proxy-chooser/classes/apache_proxy_countries.csv
@@ -0,0 +1,56 @@
+Proxy Country Code,IP2Location Code,Name
+ar,AR,Argentina
+at,AT,Austria
+au,AU,Australia
+ba,BA,Bosnia and Herzegovina
+be,BE,Belgium
+bg,BG,Bulgaria
+br,BR,Brazil
+bw,BW,Botswana
+by,BY,Belarus
+ca,CA,Canada
+ch,CH,Switzerland
+cn,CN,China
+cr,CR,Costa Rica
+cy,CY,Cyprus
+cz,CZ,Czech Republic
+de,DE,Germany
+dk,DK,Denmark
+ec,EC,Ecuador
+es,ES,Spain
+fi,FI,Finland
+fr,FR,France
+gr,GR,Greece
+hk,HK,Hong Kong
+hr,HR,Croatia
+hu,HU,Hungary
+ie,IE,Ireland
+il,IL,Israel
+in,IN,India
+it,IT,Italy
+jp,JP,Japan
+kr,KR,Korea
+lt,LT,Lithuania
+nc,NC,New Caledonia
+nl,NL,Netherlands
+no,NO,Norway
+nz,NZ,New Zealand
+om,OM,Oman
+ph,PH,Philippines
+pl,PL,Poland
+pt,PT,Portugal
+ro,RO,Romania
+ru,RU,Russian Federation
+se,SE,Sweden
+sg,SG,Singapore
+si,SI,Slovenia
+sk,SK,Slowakia
+tr,TR,Turkey
+tw,TW,Taiwan
+ua,UA,Ukraine
+ug,UG,Uganda
+uk,GB,United Kingdom of Great Britain and Northern Ireland
+us,US,United States of America
+uy,UY,Uruguay
+vn,VN,Viet Nam
+za,ZA,South Africa
diff --git a/proxy-chooser/classes/config.php b/proxy-chooser/classes/config.php
new file mode 100644
index 0000000..27aaffd
--- /dev/null
+++ b/proxy-chooser/classes/config.php
@@ -0,0 +1,17 @@
+<?php
+
+$CONFIG = array(
+    // mirrorListUrl holds the URL where the list of mirrors can be found
+    "mirrorListUrl" => "https://www.apache.org/mirrors/mirrors.list",
+    // mirrorCacheFile holds a local copy of the contents of mirrorListUrl
+    "mirrorCacheFile" => __DIR__ . "/../data/mirrors.list",
+    // Path to the updates.xml - will be passed through sprintf, first parameter
+    // is the netbeans version. It is expected, that updates.xml
+    "updatesPattern" => __DIR__ . '/../../uc/%1$s/updates.xml.gz',
+    // Location where the generated updates files are cached (first parameter
+    // is netbeans version, second parameter is the chosen proxy)
+    "cachePattern" => __DIR__ . '/../data/%1$s/%2$s/updates.xml.gz',
+    // Pattern for Update Center URL, parameter 1 is the request host,
+    // parameter 2 is the chosen mirror
+    "updateCenterUrl" => 'https://%1$s/uc-proxy-chooser/<strong>version</strong>/updates.xml.gz?mirror=%2$s'
+);
diff --git a/proxy-chooser/data/.htaccess b/proxy-chooser/data/.htaccess
new file mode 100644
index 0000000..14249c5
--- /dev/null
+++ b/proxy-chooser/data/.htaccess
@@ -0,0 +1 @@
+Deny from all
\ No newline at end of file
diff --git a/proxy-chooser/mirror-list.php b/proxy-chooser/mirror-list.php
new file mode 100644
index 0000000..41cb40c
--- /dev/null
+++ b/proxy-chooser/mirror-list.php
@@ -0,0 +1,89 @@
+<?php
+
+require("classes/ApacheMirrors.php");
+require("classes/IP2Location.php");
+require("classes/CountryCodes.php");
+require("classes/config.php");
+
+header("X-Attribution: This site or product includes IP2Proxy LITE data available from https://lite.ip2location.com");
+
+// Initialize mirror list
+$mirrors = new ApacheMirrors($CONFIG['mirrorListUrl'], $CONFIG['mirrorCacheFile']);
+$countryCodes = new CountryCodes();
+
+if(isset($_REQUEST['countryCode'])) {
+    $countryCode = $_REQUEST['countryCode'];
+} else {
+    $database = new IP2Location\Database(__DIR__ . "/data/IP2LOCATION-LITE-DB1.BIN");
+    $database2 = new IP2Location\Database(__DIR__ . "/data/IP2LOCATION-LITE-DB1.IPV6.BIN");
+
+    $ip4result = $database->lookup($_SERVER['REMOTE_ADDR']);
+    $ip6result = $database2->lookup($_SERVER['REMOTE_ADDR']);
+
+    $detectedCountryCode = "";
+    if ($ip6result) {
+        $detectedCountryCode = $ip6result['countryCode'];
+    } else if ($ip4result) {
+        $detectedCountryCode = $ip4result['countryCode'];
+    }
+
+    $countryCode = $countryCodes->resolveApacheCodeFromIP2Country($detectedCountryCode);
+}
+
+$countryName = $countryCodes->resolveNameApache($countryCode);
+
+?>
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>Update-Center Proxy Chooser</title>
+        <style>
+            * {
+                font-family: Arial, sans-serif;
+            }
+        </style>
+    </head>
+    <body>
+        <h1>Update-Center Proxy Chooser</h1>
+        <p>This site allows you to create customized update center URLs for the NetBeans IDE. By default the update center for the NetBeans IDE will use a random apache mirror based on your country.</p>
+        <p><form action="" method="GET">
+            Mirrors for:
+            <select name="countryCode">
+                <?php foreach($mirrors->getCountryCodes() as $code) { 
+                    printf('<option %s value="%s">%s</option>'
+                            , $code == $countryCode ? 'selected="selected"' : ""
+                            , htmlspecialchars($code, ENT_COMPAT, "UTF-8")
+                            , htmlspecialchars($countryCodes->resolveNameApache($code), ENT_COMPAT, "UTF-8")
+                            );
+                } ?>
+            </select>
+            <input type="submit" value="Update" />
+        </form></p>
+        <h2>Usage</h2>
+        <ol>
+            <li>Check if the correct country was detected, if not choose the correct country above and update.</li>
+            <li>Choose one of the available mirrors and copy the url to the clipboard.</li>
+            <li>Open NetBeans and go to <em>Tools</em> &#x2192; <em>Plugins</em> &#x2192; <em>Settings</em>.</li>
+            <li>Add a new entry:<ul>
+                    <li>Set name to "NetBeans Distribution (Mirrored)".</li>
+                    <li>Ensure "Check of updates automatically" is checked.</li>
+                    <li>Insert the copied URL and replace "version" in the URL with your NetBeans version.</li>
+                    <li>Ensure "Trust update center fully and and allow automatic installations" is checked.</li>
+                </ul>
+            </li>
+            <li>Switch to the "Updates" tab and choose "Check for updates".</li>
+        </ol>
+        <h2>Mirrors for <?php echo htmlspecialchars($countryName . " - " . $countryCode, ENT_COMPAT, "UTF-8"); ?></h2>
+        <ul>
+            <?php foreach($mirrors->getMirrorsForCountry($countryCode) as $mirror) {
+                $url = sprintf($CONFIG['updateCenterUrl']
+                        , htmlspecialchars($_SERVER['HTTP_HOST'], ENT_COMPAT, "UTF-8")
+                        , htmlspecialchars($mirror, ENT_COMPAT, "UTF-8"));
+                printf("<li>%s:<br><em>%s</em></li>",
+                        htmlspecialchars($mirror, ENT_COMPAT, "UTF-8"),
+                        $url);
+            }?>
+        </ul>
+        <p style="font-size: 80%">This site or product includes IP2Proxy LITE data available from <a href="https://lite.ip2location.com">https://lite.ip2location.com</a></p>
+    </body>
+</html>
\ No newline at end of file
diff --git a/proxy-chooser/nbproject/project.properties b/proxy-chooser/nbproject/project.properties
new file mode 100644
index 0000000..d017777
--- /dev/null
+++ b/proxy-chooser/nbproject/project.properties
@@ -0,0 +1,8 @@
+browser.reload.on.save=true
+include.path=${php.global.include.path}
+php.version=PHP_71
+source.encoding=UTF-8
+src.dir=.
+tags.asp=false
+tags.short=false
+web.root=.
diff --git a/proxy-chooser/nbproject/project.xml b/proxy-chooser/nbproject/project.xml
new file mode 100644
index 0000000..9dda7b4
--- /dev/null
+++ b/proxy-chooser/nbproject/project.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://www.netbeans.org/ns/project/1">
+    <type>org.netbeans.modules.php.project</type>
+    <configuration>
+        <data xmlns="http://www.netbeans.org/ns/php-project/1">
+            <name>proxy-chooser</name>
+        </data>
+    </configuration>
+</project>
diff --git a/proxy-chooser/tools/.htaccess b/proxy-chooser/tools/.htaccess
new file mode 100644
index 0000000..14249c5
--- /dev/null
+++ b/proxy-chooser/tools/.htaccess
@@ -0,0 +1 @@
+Deny from all
\ No newline at end of file
diff --git a/proxy-chooser/tools/download-ip2location.sh b/proxy-chooser/tools/download-ip2location.sh
new file mode 100755
index 0000000..69359ed
--- /dev/null
+++ b/proxy-chooser/tools/download-ip2location.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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
+#
+#   https://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 the
+# specific language governing permissions and limitations
+# under the License.
+
+# Helper script to download the IP2LOCATION data required for
+# geolocation based proxy selection
+
+PWD=`pwd`
+BIN_DIR=`dirname $0`
+ABS_BIN_DIR=`readlink -f $BIN_DIR`
+DATA_DIR="$ABS_BIN_DIR/../data"
+cd $DATA_DIR
+curl https://download.ip2location.com/lite/IP2LOCATION-LITE-DB1.IPV6.BIN.ZIP > IP2LOCATION-LITE-DB1.IPV6.BIN.ZIP
+curl https://download.ip2location.com/lite/IP2LOCATION-LITE-DB1.BIN.ZIP > IP2LOCATION-LITE-DB1.BIN.ZIP
+unzip -o IP2LOCATION-LITE-DB1.IPV6.BIN.ZIP IP2LOCATION-LITE-DB1.IPV6.BIN
+unzip -o IP2LOCATION-LITE-DB1.BIN.ZIP IP2LOCATION-LITE-DB1.BIN
+rm *.ZIP
+cd $PWD
diff --git a/proxy-chooser/updates.php b/proxy-chooser/updates.php
new file mode 100644
index 0000000..72458d1
--- /dev/null
+++ b/proxy-chooser/updates.php
@@ -0,0 +1,308 @@
+<?php
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *   https://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 the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+require("classes/ApacheMirrors.php");
+require("classes/IP2Location.php");
+require("classes/CountryCodes.php");
+require("classes/config.php");
+
+header("X-Attribution: This site or product includes IP2Proxy LITE data available from https://lite.ip2location.com");
+
+const MIRROR_ARCHIVE = "ARCHIVE";
+const MIRROR_CI = "CI";
+
+$requestUri = parse_url($_SERVER['REQUEST_URI']);
+if(! preg_match('!.*updates.xml.gz$!', $requestUri['path'])) {
+    sendError(404, "Only compressed (updates.xml.gz) version supported");
+    exit(0);
+}
+
+// Initialize mirror list
+$mirrors = new ApacheMirrors($CONFIG['mirrorListUrl'], $CONFIG['mirrorCacheFile']);
+$countryCodes = new CountryCodes();
+
+/**
+ * Create a http error response. After the message is created, excecution is
+ * stopped.
+ *
+ * @param type $httpStatus the http status to report
+ * @param type $httpMessage status line message
+ * @param type $userMessage body message, if not provided, $httpMessage will
+ * be reused
+ */
+function sendError($httpStatus, $httpMessage, $userMessage = false) {
+    header("HTTP/1.0 $httpStatus $httpMessage");
+    header("Content-Type: text/plain");
+    if($userMessage) {
+        echo $userMessage;
+    } else {
+        echo $httpMessage . "\n";
+    }
+    exit(1);
+}
+
+/**
+ * Generate a http status 400 and return a list of supported mirrors.
+ *
+ * @param string[] $mirrors
+ */
+function generateErrorList(ApacheMirrors $mirrors) {
+    $userMessage = "Please choose one of these mirrors:\n\n";
+    foreach ($mirrors->getProxyList() as $host => $url) {
+        $userMessage .= sprintf("%s - %s\n", $host, $url);
+    }
+    sendError("400", "Invalid mirror", $userMessage);
+}
+
+/**
+ * Stream the raw updates.xml file and stop execution. The contents is send
+ * unmodified.
+ *
+ * @param string $updatesXML
+ */
+function streamRawUpdate($updatesXML) {
+    header("Content-Type: application/x-gzip");
+    $fid = fopen($updatesXML, "r");
+    $output = fopen("php://output", "r");
+    stream_copy_to_stream($fid, $output);
+    fclose($fid);
+    fclose($output);
+    exit(0);
+}
+
+/**
+ * Create an absolute url from the supplied url. If the url is already absolute,
+ * it will be returned unmodified, unless it is a reference to the 'closer.lua'
+ * service of the apache foundation. If it is a reference to 'closer.lua' the
+ * referenced file will be resolved using the supplied mirror.
+ *
+ * @param string $url
+ * @param string $baseHref
+ * @param string $mirrorKey
+ * @param string $mirror
+ * @return string
+ */
+function createAbsoluteUrl(string $url, string $baseHref, string $mirrorKey, string $mirror) {
+    $urlParts = parse_url($url);
+    if($mirrorKey != MIRROR_CI && strpos($urlParts['path'], "/dyn/closer.lua") === 0) {
+        // This rewrites calls to closer.lua into direct absolute paths with a
+        // fixed name. This makes the assumption, that the update.xml.gz has
+        // the same status as the referenced parts.
+        //
+        // I.e. if we determine, that the distribution was moved to archive
+        // (updates.xml.gz is found on archive.apache.org), it is expected, that
+        // all resources referenced through closer.lua are also to be found
+        // in the archive. The same holds true for the "regular distribution"
+        // case.
+        $queryParts = [];
+        parse_str($urlParts['query'], $queryParts);
+        return $mirror . $queryParts['filename'];
+    } else if(strpos($urlParts['path'], '/') === 0) {
+        return $url;
+    } else {
+        return $baseHref . $url;
+    }
+}
+
+/**
+ * Read the supplied updates.xml and prepend the supplied mirror.
+ *
+ * @param string $updatesXML
+ * @param string $mirror
+ * @param string $nbVersion
+ */
+function streamUpdatesWithMirror($updatesXML, $mirrorKey, $mirror, $nbVersion) {
+    global $CONFIG;
+
+    $bufferFile = sprintf($CONFIG['cachePattern'], $nbVersion, $mirrorKey);
+
+    if ((!file_exists($bufferFile)) || filemtime($updatesXML) > filemtime($bufferFile)) {
+        mkdir(dirname($bufferFile), 0774, TRUE);
+        if($mirror) {
+            $baseHref = $mirror . "netbeans/netbeans/$nbVersion/nbms/";
+            if($mirrorKey == 'CI') {
+                $baseHref = $mirror;
+            }
+            $data = file_get_contents($updatesXML);
+            $doc = new DOMDocument();
+            $doc->loadXML(gzdecode($data));
+            foreach ($doc->getElementsByTagName("module") as $mod) {
+                $distribution = $mod->getAttribute("distribution");
+                $mod->setAttribute("distribution", createAbsoluteUrl($distribution, $baseHref, $mirrorKey, $mirror));
+            }
+            foreach ($doc->getElementsByTagName("license") as $li) {
+                if($li->hasAttribute("url")) {
+                    $url = $li->getAttribute("url");
+                    $li->setAttribute("url", createAbsoluteUrl($url, $baseHref, $mirrorKey, $mirror));
+                }
+            }
+            $xml = gzencode($doc->saveXML());
+        } else {
+            $data = file_get_contents($updatesXML);
+            $xml = gzencode($data);
+        }
+        file_put_contents($bufferFile, $xml);
+    }
+
+    $etag = '"' . md5($bufferFile . filemtime($bufferFile)) . '"';
+
+    if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == $etag) {
+        header("HTTP/1.0 304 Not modified");
+        exit(0);
+    }
+
+    header("Etag: $etag");
+
+    streamRawUpdate($bufferFile);
+}
+
+/**
+ * Based on the requsted URI extract the target netbeans version.
+ *
+ * @param string $request_uri
+ * @return string
+ */
+function extractNbVersion($request_uri) {
+    $matches = array();
+    if (preg_match('!.*/uc-proxy-chooser/([^/]+)/updates.xml.gz!', $request_uri, $matches)) {
+        return $matches[1];
+    } else {
+        sendError("400", "Invalid URL");
+    }
+}
+
+/**
+ * Check if the requested netbeans version is distributed via the mirror network
+ *
+ * @param type $nbVersion
+ * @return type
+ */
+function isOnMirrors($nbVersion) {
+    $primaryServerUrl = "https://downloads.apache.org/netbeans/netbeans/$nbVersion/nbms/updates.xml";
+    return urlHasContent($primaryServerUrl);
+}
+
+/**
+ * Check if the requested netbeans version is archived
+ *
+ * @param type $nbVersion
+ * @return type
+ */
+function isInArchive($nbVersion) {
+    $primaryServerUrl = "https://archive.apache.org/dist/netbeans/netbeans/$nbVersion/nbms/updates.xml.gz";
+    return urlHasContent($primaryServerUrl);
+}
+
+function ciUrl($version) {
+    $pathVersion = preg_replace("!\D!", "", $version);
+    return "https://ci-builds.apache.org/job/Netbeans/job/netbeans-TLP/job/netbeans/job/release$pathVersion/lastSuccessfulBuild/artifact/dist/netbeans/nbms/";
+}
+
+/**
+ * Check if the requested netbeans version is distributed via the mirror network
+ * or only accessible as a nightly build.
+ *
+ * @param type $nbVersion
+ * @return type
+ */
+function isCIBuild($nbVersion) {
+    $ciUrl = ciUrl($nbVersion) . "updates.xml.gz";
+    return urlHasContent($ciUrl);
+}
+
+/**
+ * Check if the supplied url return a non 404 response.
+ *
+ * @param string $url
+ * @return boolean
+ */
+function urlHasContent($url) {
+    $ch = curl_init();
+    curl_setopt($ch, CURLOPT_URL, $url);
+    curl_setopt($ch, CURLOPT_NOBODY, true);
+    curl_exec($ch);
+    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+    curl_close($ch);
+    return $code != 404;
+}
+
+// Extract the requested netbeans version from the request URI
+$nbVersion = extractNbVersion($requestUri['path']);
+
+// build the path to the updates.xml
+$updatesXML = sprintf($CONFIG['updatesPattern'], $nbVersion);
+
+// Check that the requested netbeans version is available
+if(!file_exists($updatesXML)) {
+    error_log("Failed to open updates.xml: " . $updatesXML);
+    sendError("500", "Internal server error");
+}
+
+// Check the mirror parameter and check if a valid mirror was selected
+$mirror = FALSE;
+$mirrorKey = null;
+if(array_key_exists("mirror", $_GET)) {
+    $mirrorKey = $_GET['mirror'];
+    if($mirrorKey == 'BASE') {
+        // Request wants to fetch base file
+    } else if(! $mirrors->proxyExists($mirrorKey)) {
+        generateErrorList($mirrors);
+    } else {
+        $mirror = $mirrors->getProxy($mirrorKey);
+    }
+} else {
+    $database = new IP2Location\Database(__DIR__ . "/data/IP2LOCATION-LITE-DB1.BIN");
+    $database2 = new IP2Location\Database(__DIR__ . "/data/IP2LOCATION-LITE-DB1.IPV6.BIN");
+
+    $res = $database->lookup($_SERVER['REMOTE_ADDR']);
+    $res2 = $database2->lookup($_SERVER['REMOTE_ADDR']);
+
+    $proxyKey = null;
+
+    if($res && (! $mirrorKey)) {
+        $mirrorKey = $mirrors->getProxyCountryKey($countryCodes->resolveApacheCodeFromIP2Country($res['countryCode']));
+    }
+    if($res2 && (! $mirrorKey)) {
+        $mirrorKey = $mirrors->getProxyCountryKey($countryCodes->resolveApacheCodeFromIP2Country($res2['countryCode']));
+    }
+    if($mirrorKey == null) {
+        $mirrorKey = $mirrors->getFallbackProxyKey();
+    }
+    $mirror = $mirrors->getProxy($mirrorKey);
+}
+
+if (!isOnMirrors($nbVersion)) {
+    if (isInArchive($nbVersion)) {
+        // If version of netbeans is already archived, the nbms need to be
+        // fetched from the apache archive
+        $mirror = "https://archive.apache.org/dist/";
+        $mirrorKey = MIRROR_ARCHIVE;
+    } else if (isCIBuild($nbVersion)) {
+        // If version of netbeans is still in prerelease state, the nbms need
+        // to be fetched from the CI build system
+        $mirror = ciUrl($nbVersion);
+        $updatesXML = $mirror . "updates.xml.gz";
+        $mirrorKey = MIRROR_CI;
+    } else {
+        sendError(404, "Not found");
+    }
+}
+
+streamUpdatesWithMirror($updatesXML, $mirrorKey, $mirror, $nbVersion);


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@netbeans.apache.org
For additional commands, e-mail: commits-help@netbeans.apache.org

For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists