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> → <em>Plugins</em> → <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