#!/usr/bin/perl -w -T
#
# Project    : ipv6calc/ip6calcweb
# File       : ipv6calcweb.cgi
# Version    : $Id: 1dcada8d6d0cdf3280d11aa2af8d78ce164aec78 $
# Copyright  : 2002-2021 by Peter Bieringer <pb (at) bieringer.de>
# License    : GPL, but copyright always has to be displayed in output
#
# Simple Perl web interface and security wrapper
#  It's too dangerous to call the binary 'ipv6calc' directly...
#
# Note: potentially SELinux will block access to databases and block also maximum process check
#
# Todo: more functions by query string...

### Uses environment variables:
## Created by http server before invoking CGI:
#  REMOTE_ADDR    : remote client address
#  REMOTE_HOST    : remote client name (DNS resolved)
#  HTTP_USER_AGENT: user agent string
#  SERVER_ADDR    : local server address
#  SERVER_NAME    : local server name (by http server config)
#  SERVER_PROTOCOL: check for INCLUDED (called by SSI)
#  HTTP_X_FORWARDED_FOR: added by passing proxies (if not disabled on proxy)
#  HTTP_VIA            : added by passing proxies (if not disabled on proxy)
#  QUERY_STRING   : for language setting
#    Currently supported:
#      Always:
#	"lang=$lang" with $lang in @supported_languages
#	"format=$format" with $format in @supported_formats
#      Mode=mixed/form
#	"input=$input"
#	"token=$token"

## program controls by httpd's setenv
#  HTTP_IPV6CALCWEB_MODE                : info (default), form (display a form for user input), mixed (both)
#  HTTP_IPV6CALCWEB_INFO_SERVER         : 0 (default), 1 (show information about server)
#  HTTP_IPV6CALCWEB_DEBUG               : 0 (default)
#  HTTP_IPV6CALCWEB_BIN_IPV6CALC        : path to ipv6calc binary (optional)
#  HTTP_IPV6CALCWEB_LIB_IPV6CALC        : path to ipv6calc library (optional)
#  HTTP_IPV6CALCWEB_DEBUG_IPV6CALC      : debug value for ipv6calc binary (optional)
#
#  HTTP_IPV6CALCWEB_DB_MMDB_LIB         : MaxMindDB library name (optional in dyn-load support)
#  HTTP_IPV6CALCWEB_DB_IP2LOCATION_DIR  : IP2Location database directory (optional)
#  HTTP_IPV6CALCWEB_DB_IP2LOCATION_LIB  : IP2Location library name (optional in dyn-load support)
#  HTTP_IPV6CALCWEB_DB_GEOIP_DIR        : GeoIP/GeoIP2 database directory (optional)
#  HTTP_IPV6CALCWEB_DB_DBIP_DIR         : db-ip.com database directory (optional)
#  HTTP_IPV6CALCWEB_DB_EXTERNAL_DIR     : external database directory (optional)
#  HTTP_IPV6CALCWEB_OUTPUT_FORMAT       : html (default if SERVER_PROTOCOL=INCLUDED), htmlfull (default), text, textkeyvalue
#  HTTP_IPV6CALCWEB_OUTPUT_FORMAT_HTML_DB: sequential (default), subcolumns
#
#  HTTP_IPV6CALCWEB_ANTIDOS_LOAD1       : System load 1 limit to trigger DoS protection (default below)
#  HTTP_IPV6CALCWEB_ANTIDOS_LOAD5       : System load 5 limit to trigger DoS protection (default below)
#  HTTP_IPV6CALCWEB_ANTIDOS_LOAD15      : System load 15 limit to trigger DoS protection (default below)
#  HTTP_IPV6CALCWEB_ANTIDOS_PROCMAX     : Max CGI processes to trigger DoS protection (default 0), keep 0 in case of SELinux is active to avoid many entries in audit log
#
#  HTTP_IPV6CALCWEB_DB_GEOIP2		: 1 (enable, default), 0 (disable)
#  HTTP_IPV6CALCWEB_DB_DBIP2		: 1 (enable, default), 0 (disable)
#  HTTP_IPV6CALCWEB_DB_IP2LOCATION	: 1 (enable, default), 0 (disable)
#  HTTP_IPV6CALCWEB_DB_EXTERNAL		: 1 (enable)         , 1 (disable, default)
#
#  HTTP_IPV6CALCWEB_USERNAME_GEONAMESORG: user name for geonames.org API (default: "demo")

## special output format "textkeyvalue"
#  INFO__OUTPUTVERSION="1"			: current output format version
#  (more to be documented)

## general
use strict;
use URI::Escape;
use HTML::Entities;
use POSIX;
use warnings;

## Defines
# Program information
my $program_name = "ipv6calcweb.cgi";
my $program_copyright = "(P) & (C) 2002-@COPYRIGHT_YEAR@ by Peter Bieringer";
my $program_version = "@PACKAGE_VERSION@";

# required output version of ipv6calc (introduced in 0.60.0)
my $program_required_ipv6calc_output_version = 2;


## Prototyping
sub logging($$);

## defaults for Anti-DoS (generic) (load/proc is normalized by number of CPU cores below)
my $load1_max = 2;
my $load5_max = 1.5;
my $load15_max = 1;
my $proc_max = 0;
my $proc_pattern = "ipv6calcweb";

## defines for Anti-DoS (form or mixed mode only)
my $tokenhash;
my $tokenhash_verify;
my $tokentime;
my $tokenhash_found = 0;
my $tokentime_found = 0;
my $salt;

my $time_range_valid = 300; # seconds
my $time_range_min   = 5; # seconds
my $mask_remote_address = 0; # disabled (0), enabled (1)

my $returncode = 0;
my $error;
my $error_flag = 0;

## defines for layout
my $mode_db_entries_in_subcolumns = 0;
my $db_columns = 0;
my $font_size_1 = "85%";
my $font_size_2 = "70%";

## database enabled toggles and defaults
my %db_enabled;
$db_enabled{'GEOIP2'} = 1;
$db_enabled{'IP2LOCATION'} = 1;
$db_enabled{'DBIP2'} = 1;
$db_enabled{'EXTERNAL'} = 0;

## Debug value
my $debug = 0;
#$debug |= 0x02;
#$debug |= 0x10;
#$debug = 0xffff;
#$debug = 0x01;

# debug | 0x0001: print ipv6calc command
# debug | 0x0002: print result of find_file to stderr
# debug | 0x0004: print result of find_dir to stderr
# debug | 0x0008: print environment handling to stderr
# debug | 0x0010: print raw ipv6calc output to stderr
# debug | 0x0020: db info parser
# debug | 0x0040: print form input/token to stderr
# debug | 0x0080: print environment to stderr
# debug | 0x0100: print Anti-DoS token infos to stderr
# debug | 0x1000: skip Anti-DoS sleeps
# debug | 0x2000: print lat/long URL infos to stderr
# debug | 0x4000: skip system health checks (load)
# debug | 0x8000: print feature token hash

if ( defined $ENV{'HTTP_IPV6CALCWEB_DEBUG'} ) {
	if ($ENV{'HTTP_IPV6CALCWEB_DEBUG'} =~ /^[0-9]+$/) {
		$debug = $ENV{'HTTP_IPV6CALCWEB_DEBUG'};
	} elsif ($ENV{'HTTP_IPV6CALCWEB_DEBUG'} =~ /^0x[0-9a-f]+$/oi) {
		$debug = hex($ENV{'HTTP_IPV6CALCWEB_DEBUG'});
	};
};

## debug output
if ($debug & 0x80) {
	foreach my $key (sort keys %ENV) {
		logging("DEBUG", "ENV: " . $key . "=" . $ENV{$key});
	};
}

## Program mode
my $mode = "info"; # default

if ( defined $ENV{'HTTP_IPV6CALCWEB_MODE'} ) {
	if ($ENV{'HTTP_IPV6CALCWEB_MODE'} eq "form") {
		$mode = "form";
	} elsif ($ENV{'HTTP_IPV6CALCWEB_MODE'} eq "mixed") {
		$mode = "mixed";
	};
};

if ($mode eq "form" || $mode eq "mixed") {
	use Digest::SHA1;
	use Digest::MD5;
};

# List of default location of files (first existing one would be choosen)
my @list_bin_ipv6calc = (
	"../ipv6calc/ipv6calc",
	"/usr/bin/ipv6calc",
	"/bin/ipv6calc",
);

my @list_bin_ps = (
	"/usr/bin/ps",
	"/bin/ps",
);

my @list_database_ip2location_dir = (
	"/var/local/share/IP2Location",
	"/usr/share/IP2Location",
);

my @list_database_geoip_dir = (
	"/var/local/share/GeoIP",
	"/usr/share/GeoIP",
);

my @list_database_dbip_dir = (
	"/var/local/share/DBIP",
	"/usr/share/DBIP",
);

my @list_database_external_dir = (
	"/var/local/share/ipv6calc/db",
	"/usr/share/ipv6calc/db",
);

sub find_file(@) {
	foreach my $file (@_) {
		if (-e $file) {
			logging("DEBUG", "find_file selected: $file") if ($debug & 0x02);
			return ($file);
		};
	};
	return undef;
};

sub find_dir(@) {
	foreach my $dir (@_) {
		if (-d $dir) {
			logging("DEBUG", "find_dir selected: $dir") if ($debug & 0x04);
			return ($dir);
		};
	};
	return undef;
};

# URLs
my %urls = (
	"IP2LOCATION" => "https://www.ip2location.com",
	"GEOIP2" => "https://www.maxmind.com",
	"DBIP2"  => "https://www.db-ip.com",
	"GEONAMES"  => "https://www.geonames.org",
	"IPV6CALC"  => "https://www.deepspace6.net/projects/ipv6calc.html",
);

# Base URL for RFCs
my $url_rfc = "https://tools.ietf.org/html/";

# Base URL for LISP
my $url_lisp = "https://lisp4.net.cba.upc.edu/lisp-site/";

# Base URL for GeoName service
my $username_geonamesorg = "demo";
my $url_geoname_by_id = "https://secure.geonames.org/get?geonameId=#GEONAME_ID#&username=#USERNAME#";
my $api_used_geonamesorg = 0;

# Base URL for Location Service
my $url_openstreetmap_by_lat_lon = "https://www.openstreetmap.org/?mlat=#LATITUDE#&mlon=#LONGITUDE#&zoom=5";

# Base URL for Weather Station Service
my $url_weather_com_by_station = "https://weather.com/weather/tenday/l/#WEATHERSTATIONCODE#:1:GM";

# Whois server urls
my %url_whoisservers = (
	'RIPENCC' => {
		'ipv4'	=> "https://www.ripe.net/perl/whois?searchtext=",
		'ipv6'	=> "https://www.ripe.net/perl/whois?searchtext=",
	},
	'ARIN'	=> {
		'ipv4'	=> "https://whois.arin.net/rest/ip/",
		'ipv6'	=> "https://whois.arin.net/rest/ip/",
	},
	'APNIC'	=> {
		'ipv4'	=> "https://wq.apnic.net/static/search.html?query=",
		'ipv6'	=> "https://wq.apnic.net/static/search.html?query=",
	},
	'LACNIC'	=> {
		'ipv4'	=> "https://lacnic.net/cgi-bin/lacnic/whois?query=",
		'ipv6'	=> "https://lacnic.net/cgi-bin/lacnic/whois?query=",
	},
	'AFRINIC'	=> { # currently only supporting POST, e.g. curl --data "key=<IP>" "https://www.afrinic.net/whois-web/public/?"
		'ipv4'	=> "https://www.afrinic.net/whois-web/public/?key=",
		'ipv6'	=> "https://www.afrinic.net/whois-web/public/?key=",
	},
	'IANA'	=> {
		'ipv4'	=> "",
		'ipv6'	=> "",
	},
	'unknown'	=> {
		'ipv4'	=> "",
		'ipv6'	=> "",
	}
);

#my $lang_default = "de";
my $lang_default = "en";

my $lang = $lang_default;

## Output format: text, html, htmlfull
#my $outputformat = "text";
#my $outputformat = "html";
my $outputformat = "htmlfull"; # switched to "html", if called by SSI

## Output format for databases: sequential, subcolumns
my $outputformat_html_db = "sequential"; # default

## Output type
# full = with description
# simple = without description
my $outputtype = "full";
#my $outputtype = "simple";

## Output format "textkeyvalue" version
my $outputversion_textkeyvalue = "1";

## Select output
# skip server = 1
my $skip_server = 1;

if ( defined $ENV{'HTTP_IPV6CALCWEB_INFO_SERVER'} ) {
	if ($ENV{'HTTP_IPV6CALCWEB_INFO_SERVER'} eq "1") {
		$skip_server = 0;
	};
};

## Text
# Language
my @supported_languages = ( "de", "en");

# Format
my @supported_formats = ( "text", "html", "textkeyvalue");

# Tokens to be formatted using <TT>
my @format_tt = ( "EUI48", "EUI64", "IPV6", "IPV4", "SLA", "IID", "IPV4_6TO4", "IPV4_ANON", "IPV6_ANON", "HTTP_X_FORWARDED_FOR" );

my %text_geodb = (
	'IP2LOCATION' => 'IP2Location',
	'GEOIP2'      => 'GeoIP(MaxMindDB)',
	'DBIP2'       => 'db-ip.com(MaxMindDB)'
);

my %text_extra = (
	'_REGISTRY' => {
		'de' => "Verweis auf REGISTRY",
		'en' => "link to REGISTRY",
	},
	'_GEONAME_ID' => {
		'de' => "Verweis auf geonames.org",
		'en' => "link to geonames.org",
	},
	'_GEONAME_ID_COUNTRY' => {
		'de' => "Verweis auf geonames.org",
		'en' => "link to geonames.org",
	},
	'_GEONAME_ID_CONTINENT' => {
		'de' => "Verweis auf geonames.org",
		'en' => "link to geonames.org",
	},
	'_GEONAME_ID_REGION' => {
		'de' => "Verweis auf geonames.org",
		'en' => "link to geonames.org",
	},
	'_GEONAME_ID_DISTRICT' => {
		'de' => "Verweis auf geonames.org",
		'en' => "link to geonames.org",
	},
	'_LATITUDE' => {
		'de' => "Verweis auf openstreetmap.org",
		'en' => "link to openstreetmap.org",
	},
	'_LONGITUDE' => {
		'de' => "Verweis auf openstreetmap.org",
		'en' => "link to openstreetmap.org",
	},
	'_WEATHERSTATIONCODE' => {
		'de' => "Verweis auf weather.com",
		'en' => "link to weather.com",
	}
);

my %text = (
	'EUI48' => {
		'de' => "EUI-48 Identifizierungsnummer (MAC Adresse)",
		'en' => "EUI-48 identifier (MAC address)",
	},
	'EUI48_SCOPE' => {
		'de' => "EUI-48 Art",
		'en' => "EUI-48 scope",
	},
	'EUI48_TYPE' => {
		'de' => "EUI-48 Adresstyp",
		'en' => "EUI-48 address type",
	},
	'EUI64' => {
		'de' => "EUI-64 Identifizierungsnummer",
		'en' => "EUI-64 identifier",
	},
	'EUI64_SCOPE' => {
		'de' => "EUI-64 Art",
		'en' => "EUI-64 scope",
	},
	'IPV6' => {
		'de' => "IPv6 Adresse",
		'en' => "IPv6 address",
	},
	'IPV6_ANON' => {
		'de' => "Anonymisierte IPv6 Adresse",
		'en' => "Anonymized IPv6 address",
	},
	'IPV6_COUNTRYCODE' => {
		'de' => "Ländercode der IPv6 Adresse",
		'en' => "Country Code of IPv6 address",
	},
	'IPV6_COUNTRYCODE_SOURCE' => {
		'de' => "Quelle des Ländercode der IPv6 Adresse",
		'en' => "Source of Country Code of IPv6 address",
	},
	'IPV6_AS_NUM' => {
		'de' => "Autonomous System Nummer der IPv6 Adresse",
		'en' => "Autonomous System Number of IPv6 address",
	},
	'IPV6_AS_TEXT' => {
		'de' => "Autonomous System der IPv6 Adresse",
		'en' => "Autonomous System of IPv6 address",
	},
	'IPV6_AS_SOURCE' => {
		'de' => "Quelle der Autonomous System der IPv6 Adresse",
		'en' => "Source of Autonomous System of IPv6 address",
	},
	'IPV6_AS_NUM_SOURCE' => {
		'de' => "Quelle der Autonomous System Nummer der IPv6 Adresse",
		'en' => "Source of Autonomous System Number of IPv6 address",
	},
	'IPV4' => {
		'de' => "IPv4 Adresse",
		'en' => "IPv4 address",
	},
	'IPV4_ANON' => {
		'de' => "Anonymisierte IPv4 Adresse",
		'en' => "Anonymized IPv4 address",
	},
	'IPV4_REGISTRY' => {
		'de' => "Registry der IPv4 Adresse",
		'en' => "Registry of IPv4 address",
	},
	'IPV6_REGISTRY' => {
		'de' => "Registry der IPv6 Adresse",
		'en' => "Registry of IPv6 address",
	},
	'IPV4_SOURCE' => {
		'de' => "Quelle der IPv4 Adresse",
		'en' => "Source of IPv4 address",
	},
	'IPV4_COUNTRYCODE' => {
		'de' => "Ländercode der IPv4 Adresse",
		'en' => "Country Code of IPv4 address",
	},
	'IPV4_COUNTRYCODE_SOURCE' => {
		'de' => "Quelle des Ländercode der IPv4 Adresse",
		'en' => "Source of Country Code of IPv4 address",
	},
	'IPV4_AS_NUM' => {
		'de' => "Autonomous System Nummer der IPv4 Adresse",
		'en' => "Autonomous System Number of IPv4 address",
	},
	'IPV4_AS_TEXT' => {
		'de' => "Autonomous System der IPv4 Adresse",
		'en' => "Autonomous System of IPv4 address",
	},
	'IPV4_AS_SOURCE' => {
		'de' => "Quelle der Autonomous System der IPv4 Adresse",
		'en' => "Source of Autonomous System of IPv4 address",
	},
	'TEREDO_PORT_CLIENT' => {
		'de' => "UDP-Port des Teredo Clients (nach NAT)",
		'en' => "UDP port of Teredo client (outside NAT)",
	},
	'TEREDO_IPV4_SERVER' => {
		'de' => "IPv4-Adresse des Teredo Servers",
		'en' => "IPv4 address of Teredo Server",
	},
	'OUI' => {
		'de' => "Hersteller-Identifizierung der Netzwerkarte",
		'en' => "Vendor identification of network interface card",
	},
	'INPUT' => {
		'de' => "Ihre Eingabe",
		'en' => "Your input",
	},
	'REMOTE' => {
		'de' => "Ihr Client",
		'en' => "Your client",
	},
	'SERVER' => {
		'de' => "Dieser Server",
		'en' => "This server",
	},
	'SLA' => {
		'de' => "Subnetz ID",
		'en' => "Subnet ID",
	},
	'IIDSCOPE' => {
		'de' => "Art der Interface-Identifierungsnummer",
		'en' => "Scope of interface identifier",
	},
	'IID' => {
		'de' => "Interface-Identifierungsnummer",
		'en' => "Interface identifier",
	},
	'TYPE' => {
		'de' => "Adresstyp",
		'en' => "Address type",
	},
	'IPV6_TYPE' => {
		'de' => "IPv6 Adresstyp",
		'en' => "IPv6 Address type",
	},
	'IPV4_TYPE' => {
		'de' => "IPv4 Adresstyp",
		'en' => "IPv4 Address type",
	},
	'NAME' => {
		'de' => "Reverse DNS Aufloesung",
		'en' => "Reverse DNS resolution",
	},
	'RESULT' => {
		'de' => "Ergebnis",
		'en' => "Result",
	},
	'IPV6_PREFIXLENGTH' => {
		'de' => "IPv6 Praefixlaenge",
		'en' => "IPv6 prefix length",
	},
	'IPV4_PREFIXLENGTH' => {
		'de' => "IPv4 Praefixlaenge",
		'en' => "IPv4 prefix length",
	},
	'title' => {
		'de' => "Adresstyp Information",
		'en' => "Addresstype information",
	},
	'nodata' => {
		'de' => "Keine Daten verfuegbar",
		'en' => "No data availabe",
	},
	'generated' => {
		'de' => "Generiert durch",
		'en' => "Generated by",
	},
	'at' => {
		'de' => "um",
		'en' => "at",
	},
	'powered' => {
		'de' => "Unterstuetzt durch",
		'en' => "Powered by",
	},
	'credits' => {
		'de' => "Danksagung an",
		'en' => "Credits to",
	},
	'entries' => {
		'de' => "Eintraege",
		'en' => "entries",
	},
	'database' => {
		'de' => "Datenbank",
		'en' => "database",
	},
	'version' => {
		'de' => "Version",
		'en' => "version",
	},
	'clear' => {
		'de' => "loeschen",
		'en' => "clear",
	},
	'cancel' => {
		'de' => "abbrechen",
		'en' => "cancel",
	},
	'Address' => {
		'de' => "Adresse",
		'en' => "Address",
	},
	'send' => {
		'de' => "absenden",
		'en' => "send",
	},
	'HTTP_USER_AGENT' => {
		'de' => "Browseridentifikation",
		'en' => "User agent identification",
	},
	'HTTP_X_FORWARDED_FOR' => {
		'de' => "Client hinter Proxy",
		'en' => "Client behind proxy",
	},
	'HTTP_VIA' => {
		'de' => "Passierender Proxy",
		'en' => "Passing proxy",
	},
	'PROXY' => {
		'de' => "Passierender Proxy",
		'en' => "Passing proxy",
	},
	'PROXY_INFO' => {
		'de' => "Proxy Information",
		'en' => "Proxy Information",
	},
	'EXTERNAL_COUNTRY_SHORT' => {
		'de' => "External DB Laenderkennung",
		'en' => "External DB country code",
	},
	'EXTERNAL_DATABASE_INFO' => {
		'de' => "External Datenbank-Information",
		'en' => "External database information",
	},
	'BUILTIN_DATABASE_INFO' => {
		'de' => "Eingebaute Datenbank-Information",
		'en' => "BuiltIn database information",
	},
	'SETTINGS_ANON' => {
		'de' => "Einstellungen der Anonymisierung",
		'en' => "Anoymizer settings",
	},
	'_COUNTRY_SHORT' => {
		'de' => "Laenderkennung",
		'en' => "Country Code",
	},
	'_COUNTRY_LONG' => {
		'de' => "Land",
		'en' => "Country",
	},
	'_CONTINENT_LONG' => {
		'de' => "Kontinent",
		'en' => "Continent",
	},
	'_CONTINENT_SHORT' => {
		'de' => "Kontinentkennung",
		'en' => "Continent Code",
	},
	'_REGION' => {
		'de' => "Region",
		'en' => "Region",
	},
	'_RADIUS' => {
		'de' => "Radius",
		'en' => "Radius",
	},
	'_DISTRICT' => {
		'de' => "Bezirk",
		'en' => "District",
	},
	'_CITY' => {
		'de' => "Stadt",
		'en' => "City",
	},
	'_ISP' => {
		'de' => "Internet Service Provider",
		'en' => "Internet Service Provider",
	},
	'_LATITUDE' => {
		'de' => "IP2Location Breitengrad",
		'en' => "Latitude",
	},
	'_LONGITUDE' => {
		'de' => "Laengengrad",
		'en' => "Longitude",
	},
	'_DOMAIN' => {
		'de' => "Domain",
		'en' => "Domain",
	},
	'_ZIPCODE' => {
		'de' => "Postleitzahl",
		'en' => "ZIP code",
	},
	'_GEONAME_ID' => {
		'de' => "GeoName ID des Ortes",
		'en' => "GeoName ID of Location",
	},
	'_GEONAME_ID_COUNTRY' => {
		'de' => "GeoName ID des Landes",
		'en' => "GeoName ID of Country",
	},
	'_GEONAME_ID_CONTINENT' => {
		'de' => "GeoName ID des Kontinents",
		'en' => "GeoName ID of Continent",
	},
	'_GEONAME_ID_REGION' => {
		'de' => "GeoName ID der Region",
		'en' => "GeoName ID of Region",
	},
	'_GEONAME_ID_DISTRICT' => {
		'de' => "GeoName ID des Bezirks",
		'en' => "GeoName ID of District",
	},
	'IPV6_GEONAME_ID' => {
		'de' => "GeoName ID des Ortes der IPv6 Adresse",
		'en' => "GeoName ID of Location of IPv6 Address",
	},
	'IPV4_GEONAME_ID' => {
		'de' => "GeoName ID des Ortes der IPv4 Adresse",
		'en' => "GeoName ID of Location of IPv4 Address",
	},
	'IPV6_GEONAME_ID_TYPE' => {
		'de' => "Typ der GeoName ID der IPv6 Adresse",
		'en' => "Type of GeoName ID of IPv6 Address",
	},
	'IPV4_GEONAME_ID_TYPE' => {
		'de' => "Typ der GeoName ID der IPv4 Adresse",
		'en' => "Type of GeoName ID of IPv4 Address",
	},
	'IPV6_GEONAME_ID_SOURCE' => {
		'de' => "Quelle der GeoName ID der IPv6 Adresse",
		'en' => "Source of GeoName ID of IPv6 Address",
	},
	'IPV4_GEONAME_ID_SOURCE' => {
		'de' => "Quelle der GeoName ID der IPv4 Adresse",
		'en' => "Source of GeoName ID of IPv4 Address",
	},
	'_ORGNAME' => {
		'de' => "Autonomous System Organisation",
		'en' => "Autonomous System Organization",
	},
	'_AS_ORGNAME' => {
		'de' => "Autonomous System Organisation",
		'en' => "Autonomous System Organization",
	},
	'_AS_NUM' => {
		'de' => "Autonomous System Nummer",
		'en' => "Autonomous System Number",
	},
	'_AS_TEXT' => {
		'de' => "Autonomous System",
		'en' => "Autonomous System",
	},
	'_CONTINENTCODE' => {
		'de' => "Kontinent-Code",
		'en' => "Continent Code",
	},
	'_AREACODE' => {
		'de' => "Area-Code",
		'en' => "Area Code",
	},
	'_DMACODE' => {
		'de' => "DMA-Code",
		'en' => "DMA Code",
	},
	'_TIMEZONE' => {
		'de' => "Zeitzone",
		'en' => "Time Zone",
	},
	'_TIMEZONE_NAME' => {
		'de' => "Zeitzonen Name",
		'en' => "Time Zone Name",
	},
	'_NETSPEED' => {
		'de' => "Netzgeschwindigkeit",
		'en' => "Net Speed",
	},
	'_WEATHERSTATIONNAME' => {
		'de' => "Name der Wetterstation",
		'en' => "Weather Station Name",
	},
	'_WEATHERSTATIONCODE' => {
		'de' => "Code der Wetterstation",
		'en' => "Weather Station Code",
	},
	'_IDDCODE' => {
		'de' => "Landesvorwahl",
		'en' => "Internation Direct Dialing",
	},
	'_ELEVATION' => {
		'de' => "Hoehe ueber Meeresspiegel [m]",
		'en' => "High above Sealevel [m]",
	},
	'_USAGETYPE' => {
		'de' => "Art der Benutzung",
		'en' => "Usage Type Classification",
	},
	'_MOBILEBRAND' => {
		'de' => "Mobile Brand",
		'en' => "Mobile Brand",
	},
	'_MNC' => {
		'de' => "Mobile Network Code",
		'en' => "Mobile Network Code",
	},
	'_MCC' => {
		'de' => "Mobile Country Code",
		'en' => "Mobile Country Code",
	},
);


# Location of binary
my $bin_ipv6calc;
if (defined $ENV{'HTTP_IPV6CALCWEB_BIN_IPV6CALC'}) {
	if ($ENV{'HTTP_IPV6CALCWEB_BIN_IPV6CALC'} =~ /^([[:alnum:]\.\-\/]+)$/o) {
		$bin_ipv6calc = $1;
	} else {
		logging("ERROR", "HTTP_IPV6CALCWEB_BIN_IPV6CALC found, but not containing proper chars");
		&print_error();
	};
} else {
	$bin_ipv6calc = find_file(@list_bin_ipv6calc);
};

# Location of library
my $lib_ipv6calc;
if (defined $ENV{'HTTP_IPV6CALCWEB_LIB_IPV6CALC'}) {
	if ($ENV{'HTTP_IPV6CALCWEB_LIB_IPV6CALC'} =~ /^([[:alnum:]\.\-\/]+)$/o) {
		$lib_ipv6calc = $1;
	} else {
		logging("ERROR", "HTTP_IPV6CALCWEB_LIB_IPV6CALC found, but not containing proper chars");
		&print_error();
	};
};

my $bin_ps;
$bin_ps = find_file(@list_bin_ps);

my $options_ipv6calc = "-m -i -q";

## add optional ipv6calc options
if (defined $ENV{'HTTP_IPV6CALCWEB_DEBUG_IPV6CALC'}) {
	if ($ENV{'HTTP_IPV6CALCWEB_DEBUG_IPV6CALC'} =~ /^(0x[[:digit:]abcdef]+|-?[[:digit:]]+)$/oi) {
		$options_ipv6calc .= " -d " . $1;
	} else {
		logging("ERROR", "HTTP_IPV6CALCWEB_DEBUG_IPV6CALC found, but not containing proper value: " . $ENV{'HTTP_IPV6CALCWEB_DEBUG_IPV6CALC'});
		&print_error();
	};
};

## simple DoS protection
if (defined $ENV{'HTTP_IPV6CALCWEB_ANTIDOS_LOAD1'}) {
	if ($ENV{'HTTP_IPV6CALCWEB_ANTIDOS_LOAD1'} =~ /^(-?[[:digit:].]+)$/o) {
		$load1_max = $1; # overwrite default from above
	} else {
		logging("ERROR", "HTTP_IPV6CALCWEB_ANTIDOS_LOAD1 found, but not containing proper number");
		&print_error();
	};
};

if (defined $ENV{'HTTP_IPV6CALCWEB_ANTIDOS_LOAD5'}) {
	if ($ENV{'HTTP_IPV6CALCWEB_ANTIDOS_LOAD5'} =~ /^(-?[[:digit:].]+)$/o) {
		$load5_max = $1; # overwrite default from above
	} else {
		logging("ERROR", "HTTP_IPV6CALCWEB_ANTIDOS_LOAD5 found, but not containing proper number");
		&print_error();
	};
};

if (defined $ENV{'HTTP_IPV6CALCWEB_ANTIDOS_LOAD15'}) {
	if ($ENV{'HTTP_IPV6CALCWEB_ANTIDOS_LOAD15'} =~ /^(-?[[:digit:].]+)$/o) {
		$load15_max = $1; # overwrite default from above
	} else {
		logging("ERROR", "HTTP_IPV6CALCWEB_ANTIDOS_LOAD15 found, but not containing proper number");
		&print_error();
	};
};

if (defined $ENV{'HTTP_IPV6CALCWEB_ANTIDOS_PROCMAX'}) {
	if ($ENV{'HTTP_IPV6CALCWEB_ANTIDOS_PROCMAX'} =~ /^(-?[[:digit:]]+)$/o) {
		$proc_max = $1; # overwrite default from above
	} else {
		logging("ERROR", "HTTP_IPV6CALCWEB_ANTIDOS_PROCMAX found, but not containing proper number");
		&print_error();
	};
};


## external APIs
if (defined $ENV{'HTTP_IPV6CALCWEB_USERNAME_GEONAMESORG'}) {
	if ($ENV{'HTTP_IPV6CALCWEB_USERNAME_GEONAMESORG'} =~ /^([[:alnum:]\.\-]+)$/o) {
		$username_geonamesorg = $1; # overwrite default from above
	} else {
		logging("ERROR", "HTTP_IPV6CALCWEB_USERNAME_GEONAMESORG found, but not containing proper chars");
		&print_error();
	};
};

## Database toggles
for my $db (sort keys %db_enabled) {
	my $db_env = "HTTP_IPV6CALCWEB_DB_" . $db;
	if (defined $ENV{$db_env}) {
		if ($ENV{$db_env} =~ /^(0|no|disabled|off)$/) {
			$db_enabled{$db} = 0;
		} elsif ($ENV{$db_env} =~ /^(1|yes|enabled|on)$/) {
			$db_enabled{$db} = 1;
		} else {
			logging("ERROR", "$db_env found, but not containing proper value (0|1)");
			&print_error();
		};
	};
};

## Location of database and library files

# MaxMindDB
my $database_mmdb_lib;
if (defined $ENV{'HTTP_IPV6CALCWEB_DB_MMDB_LIB'}) {
	if ($ENV{'HTTP_IPV6CALCWEB_DB_MMDB_LIB'} =~ /^([[:alnum:]\.\-\/]+)$/) {
		$database_mmdb_lib = $1;
	} else {
		logging("ERROR", "HTTP_IPV6CALCWEB_DB_MMDB_LIB found, but not containing proper chars");
		&print_error();
	};
};

# IP2Location
my $options_ipv6calc_ip2location = "";
my %db_info_ip2location;
my $database_ip2location_dir;
my $database_ip2location_lib;

if (defined $ENV{'HTTP_IPV6CALCWEB_DB_IP2LOCATION_DIR'}) {
	if ($ENV{'HTTP_IPV6CALCWEB_DB_IP2LOCATION_DIR'} =~ /^([[:alnum:]\.\-\/]+)$/) {
		$database_ip2location_dir = $1;
	} else {
		logging("ERROR", "HTTP_IPV6CALCWEB_DB_IP2LOCATION_DIR found, but not containing proper chars");
		&print_error();
	};
} else {
	$database_ip2location_dir = find_dir(@list_database_ip2location_dir);
};

if (defined $ENV{'HTTP_IPV6CALCWEB_DB_IP2LOCATION_LIB'}) {
	if ($ENV{'HTTP_IPV6CALCWEB_DB_IP2LOCATION_LIB'} =~ /^([[:alnum:]\.\-\/]+)$/) {
		$database_ip2location_lib = $1;
	} else {
		logging("ERROR", "HTTP_IPV6CALCWEB_DB_IP2LOCATION_LIB found, but not containing proper chars");
		&print_error();
	};
};

# GeoIP/GeoIP2
my $options_ipv6calc_geoip = "";
my $options_ipv6calc_geoip2 = "";
my %db_info_geoip;
my %db_info_geoip2;
my $info_geoip_string;
my $info_geoip2_string;
my $database_geoip_dir;
my $database_geoip2_dir;
my $database_geoip_lib;

if (defined $ENV{'HTTP_IPV6CALCWEB_DB_GEOIP_DIR'}) {
	if ($ENV{'HTTP_IPV6CALCWEB_DB_GEOIP_DIR'} =~ /^([[:alnum:]\.\-\/]+)$/) {
		$database_geoip_dir = $1;
		$database_geoip2_dir = $1;
	} else {
		logging("ERROR", "HTTP_IPV6CALCWEB_DB_GEOIP_DIR found, but not containing proper chars");
		&print_error();
	};
} else {
	$database_geoip_dir = find_dir(@list_database_geoip_dir);
};

# db-ip.com
my $options_ipv6calc_dbip = "";
my $options_ipv6calc_dbip2 = "";
my %db_info_dbip;
my %db_info_dbip2;
my $database_dbip_dir;
my $database_dbip2_dir;
my $info_dbip_string;
my $info_dbip2_string;

if (defined $ENV{'HTTP_IPV6CALCWEB_DB_DBIP_DIR'}) {
	if ($ENV{'HTTP_IPV6CALCWEB_DB_DBIP_DIR'} =~ /^([[:alnum:]\.\-\/]+)$/) {
		$database_dbip_dir = $1;
		$database_dbip2_dir = $1;
	} else {
		logging("ERROR", "HTTP_IPV6CALCWEB_DB_DBIP_DIR found, but not containing proper chars");
		&print_error();
	};
} else {
	$database_dbip_dir = find_dir(@list_database_dbip_dir);
};

# External DB
my $options_ipv6calc_external = "";
my %db_info_external;
my $database_external_dir;
my $info_external_string;

if (defined $ENV{'HTTP_IPV6CALCWEB_DB_EXTERNAL_DIR'}) {
	if ($ENV{'HTTP_IPV6CALCWEB_DB_EXTERNAL_DIR'} =~ /^([[:alnum:]\.\-\/]+)$/) {
		$database_external_dir = $1;
	} else {
		logging("ERROR", "HTTP_IPV6CALCWEB_DB_EXTERNAL_DIR found, but not containing proper chars");
		&print_error();
	};
} else {
	$database_external_dir = find_dir(@list_database_external_dir);
};

# BuiltIn DB
my $options_ipv6calc_builtin = "";
my %db_info_builtin;
my $database_builtin_dir;
my $info_builtin_string;


## external database ipv6calc option handling
# MaxMindDB
if (defined $database_mmdb_lib) {
	$options_ipv6calc_geoip2 .= " --db-mmdb-lib $database_mmdb_lib";
	$options_ipv6calc_dbip2 .= " --db-mmdb-lib $database_mmdb_lib";
};

# IP2Location
if (defined $database_ip2location_dir) {
	$options_ipv6calc_ip2location .= " --db-ip2location-dir $database_ip2location_dir";
};

if (defined $database_ip2location_lib) {
	$options_ipv6calc_ip2location .= " --db-ip2location-lib $database_ip2location_lib";
};

# GeoIP
if (defined $database_geoip_dir) {
	$options_ipv6calc_geoip .= " --db-geoip-dir $database_geoip_dir";
};

if (defined $database_geoip_lib) {
	$options_ipv6calc_geoip .= " --db-geoip-lib $database_geoip_lib";
};

# GeoIP2
if (defined $database_geoip2_dir) {
	$options_ipv6calc_geoip2 .= " --db-geoip2-dir $database_geoip2_dir";
};

# DBIP
if (defined $database_dbip_dir) {
	$options_ipv6calc_dbip .= " --db-dbip-dir $database_dbip_dir";
};

# DBIP2
if (defined $database_dbip2_dir) {
	$options_ipv6calc_dbip2 .= " --db-dbip2-dir $database_dbip2_dir";
};

# External DB
if (defined $database_external_dir) {
	$options_ipv6calc_external .= " --db-external-dir $database_external_dir";
};

###### Normally nothing to change here

## Cleanup environment
# Please report, if more cleanup is needed on other systems

# Hardwire path to well known
if ( defined $ENV{'PATH'} ) { $ENV{'PATH'}="/bin:/usr/bin:/usr/local/bin"; };
# Clear shell environment
if ( defined $ENV{'BASH_ENV'} ) { $ENV{'BASH_ENV'}=""; };

## Fallbacks
if (! defined $outputformat) { $outputformat = "text" };
if (! defined $outputtype) { $outputtype = "simple" };
if (! defined $lang_default) { $lang_default = "en"};
if (! defined $lang) { $lang = $lang_default};

## Variables
my $addr_remote;
my $name_remote;
my $user_agent;
my $x_forwarded_for;
my $via;
my $addr_server;
my $name_server;

my @proxy_addresses;
my @proxy_infos;

my (%infoh_remote, @sort_remote);
my (%infoh_server, @sort_server);
my (%infoh_proxy, @sort_proxy);
my (%infoh_input, @sort_input);
my (%infoh_localhost4, @sort_localhost4);

my $length_max_key = 0;
my $length_max_description = 0;
my $query_string;
my $script_name;
my $uri = "";
my $error_insert_input = 0;

my $maxenvlength = 256;

my $input;
my $input_default;
my $token;

my %key_db_tokens;

# default values
my $ipv6calc_version = "";
my $ipv6calc_copyright = "(P) & (C) by Peter Bieringer";
my $ipv6calc_name = "ipv6calc";
my $ipv6calc_features = "";
my %ipv6calc_feature_hash;
my $ipv6calc_capabilities = "";
my %ipv6calc_capability_hash;
my $ipv6calc_settings = "";
my %ipv6calc_settings_hash;

my $chr_quote = ""; # default

############### Functions

## Logging
sub logging($$) {
	if (defined $_[0] && defined $_[1]) {
		printf STDERR "%s %-6s: %s\n", $program_name, $_[0], $_[1];
	};
};


## Split by " / "
sub split_store($$) {
	my $p_hash = $_[0];

	printf STDERR "DEBUG : need to split: %s\n", $_[1] if ($debug & 0x20);

	for my $entry (split / \/ /, $_[1]) {
		printf STDERR "DEBUG : split part: %s\n", $entry if ($debug & 0x20);
		$$p_hash{$entry} = 1;
	};
};

## Error message
sub print_error ($) {
	my $message = shift;
	if (defined $message) {
		printf STDERR $message . "\n";
	};
	goto("OUTPUT_BEGIN_ERROR");
};

## Print conditional html
sub print_tagoutput ($) {
	my $text = shift;
	if ( defined $text ) {
		if ($outputformat =~ /^html/o) {
			print $text;
		};
	};
};

sub print_textonly ($) {
	my $text = shift;
	if ( defined $text ) {
		if ($outputformat =~ /^text/o) {
			print $text;
		};
	};
};

## Print one table part
sub print_infohash ($$$;$) {
	my $phash = $_[0];
	my $parray = $_[1];
	my $colormode = $_[2];
	my $counter = $_[3];

	my %key_values_db_tokens;

	if (! defined $phash) { return; };

	my ($flag_tt, $flag_whoisurl, $whois_registry, $whois_type);
	my $last_key_embedded = "";
	my $count_key_embedded = 0;

	if ( ! defined \$phash ) {
		&print_tagoutput ( "      <tr>\n" );
		&print_tagoutput ( "        <td colspan=\"" . ($db_columns + 3) . "\">" );
		print $text{'nodata'}->{$lang};
		&print_textonly ("\n");
		&print_tagoutput ( "        </td>\n" );
		&print_tagoutput ( "      </tr>\n" );
		return;
	};

	for my $key (@$parray) {
		if ($key eq "IP2LOCATION_DATABASE_INFO") {
			split_store(\%db_info_ip2location, $$phash{$key});
			# skipped, will be shown in footer
			next;
		};

		if ($key eq "IP2LOCATION_DATABASE_INFO_IPV4") {
			split_store(\%db_info_ip2location, $$phash{$key});
			# skipped, will be shown in footer
			next;
		};

		if ($key eq "IP2LOCATION_DATABASE_INFO_IPV6") {
			split_store(\%db_info_ip2location, $$phash{$key});
			# skipped, will be shown in footer
			next;
		};

		if ($key eq "GEOIP2_DATABASE_INFO") {
			$info_geoip2_string = $$phash{$key};
			# strip non-ascii chars
			$info_geoip2_string =~ s/[\200-\377]//g;
			split_store(\%db_info_geoip2, $info_geoip2_string);
			# skipped, will be shown in footer
			next;
		};
		if ($key eq "DBIP2_DATABASE_INFO") {
			$info_dbip2_string = $$phash{$key};
			# strip non-ascii chars
			$info_dbip2_string =~ s/[\200-\377]//g;
			split_store(\%db_info_dbip2, $info_dbip2_string);
			# skipped, will be shown in footer
			next;
		};

		if ($key eq "EXTERNAL_DATABASE_INFO") {
			$info_external_string = $$phash{$key};
			# strip non-ascii chars
			$info_external_string =~ s/[\200-\377]//g;
			split_store(\%db_info_external, $info_external_string);
			# skipped, will be shown in footer
			next;
		};

		if ($key eq "BUILTIN_DATABASE_INFO") {
			$info_builtin_string = $$phash{$key};
			# strip non-ascii chars
			$info_builtin_string =~ s/[\200-\377]//g;
			split_store(\%db_info_builtin, $info_builtin_string);
			# skipped, will be shown in footer
			next;
		};

		# catch internal keys
		if ( $key =~ /^IPV6CALC_/ ) {
			# skipped, will be shown in footer
			next;
		};

		$flag_tt = 0;
		$flag_whoisurl = 0;

		# extract lookup key
		$key =~ /^([^[]+)(\[[^]]*\])?$/;
		my $key_lookup = $1;
		my $key_embedded = "";
		my $embedded_changed = 0;
		if (defined $2) {
			$key_embedded = $2;
			if ($last_key_embedded eq "") {
				$last_key_embedded = $key_embedded;
				$count_key_embedded++;
				$embedded_changed = 1;
			} elsif ( $last_key_embedded ne $key_embedded) {
				$last_key_embedded = $key_embedded;
				$count_key_embedded++;
				$embedded_changed = 1;
			};
			if (($outputformat eq "html") || ($outputformat eq "htmlfull")) {
				if ($embedded_changed == 1) {
					if ($count_key_embedded & 1) {
						&print_tagoutput ( "      <tr style=\"background-color:rgb(240,240,240);\">\n" );
					} else {
						&print_tagoutput ( "      <tr style=\"background-color:rgb(216,216,216);\">\n" );
					};
					my $entry = "Embedded IPv4 address " . $key_embedded;
					$entry =~ s/[\[\]]//g;
					&print_tagoutput ( "        <th align=\"left\" colspan=\"" . ($db_columns + 3) . "\" style=\"font-size:$font_size_1;\">" . $entry . "</th>\n" );
					&print_tagoutput ( "      </tr>\n");
				};
			};
		} else {
			$count_key_embedded = 0;
			$last_key_embedded = "";
		};

		if (grep(/^$key_lookup$/, @format_tt)) {
			$flag_tt = 1;
		};

		if ($mode_db_entries_in_subcolumns == 1) {
			# suppress db based keys, store them in hash for later printout
			$key =~ /^([A-Z0-9]+)_(.*)$/o;
			if (grep(/^$1/i, keys %key_db_tokens)) {
				my $dbprovider = $1;
				my $subkey = $2;
				$key_db_tokens{$dbprovider}++;

				# check for embedded information
				$subkey =~ /^([^[]+)(\[[^]]*\])?$/;
				my $subkey_stripped = $1;
				my $embedded = $2;
				if (! defined $embedded) { $embedded = "default"; };
				$key_values_db_tokens{$embedded}->{$subkey_stripped} = $dbprovider;
				next;
			};
		};

		# print key
		if ($count_key_embedded > 0) {
			if ($count_key_embedded & 1) {
				&print_tagoutput ( "      <tr style=\"background-color:rgb(240,240,240);\">\n" );
			} else {
				&print_tagoutput ( "      <tr style=\"background-color:rgb(216,216,216);\">\n" );
			};
		} else {
			&print_tagoutput ( "      <tr>\n" );
		};
		&print_tagoutput ( "        <th align=\"left\"><b>" );
		&print_tagoutput ( "<font size=\"-1\">" ) if ($count_key_embedded > 0);
		print uc($colormode) . "__" if ($outputformat eq "textkeyvalue");
		# reduce duplicate information
		my $key_print = $key;
		$key_print =~ s/^($colormode)_//i if ($outputformat eq "textkeyvalue");
		if ((($outputformat eq "html") || ($outputformat eq "htmlfull")) && ($key_embedded ne "")) {
			$key_print =~ /^([^[]+)(\[[^]]*\])?$/;
			$key_print = $1;
		};

		# print url where available
		if ((($outputformat eq "html") || ($outputformat eq "htmlfull")) && ($key =~ /^([A-Z0-9]+)_(.+)$/o)) {
			if (defined $urls{$1}) {
				# add URL
				&print_tagoutput("<a target=\"_blank\" href=\"" . $urls{$1} . "\">" . $key_print . "</a>");
			} else {
				&print_tagoutput($key_print);
			};
		} else {
			print $key_print;
		};

		&print_textonly (' ' x ($length_max_key - length($key)) ) if ($outputformat ne "textkeyvalue");
		&print_textonly (" | ") if ($outputformat ne "textkeyvalue");
		&print_textonly ("[" . $counter . "]") if (($outputformat eq "textkeyvalue") && (defined $counter));
		&print_textonly ("=") if ($outputformat eq "textkeyvalue");
		&print_tagoutput ( "</font>" ) if ($count_key_embedded > 0);
		&print_tagoutput ( "</b></th>\n" );

		# registry detection
		if ($key_lookup eq "IPV4" || $key_lookup eq "IPV4_REGISTRY") {
			if ( defined $$phash{'IPV4_REGISTRY' . $key_embedded} ) {
				$whois_registry = $$phash{'IPV4_REGISTRY' . $key_embedded};
				$whois_type = "ipv4";
				$flag_whoisurl = 1;
			} else {
				# Temporary workaround
				$whois_registry = 'unknown';
				$whois_type = "ipv4";
				$flag_whoisurl = 1;
			};
		} elsif ($key_lookup eq "IPV6" || $key_lookup eq "IPV6_REGISTRY") {
			if ( defined $$phash{'IPV6_REGISTRY' . $key_embedded} ) {
				$whois_registry = $$phash{'IPV6_REGISTRY' . $key_embedded};
				$whois_type = "ipv6";
				$flag_whoisurl = 1;
			};
		};

		# print description
		if ($outputtype ne "simple") {
			&print_tagoutput ( "        <td>" );
			&print_tagoutput ( "<font size=\"-1\">" ) if ($count_key_embedded > 0);
			if (defined $text{$key_lookup}->{$lang}) {
				print $text{$key_lookup}->{$lang};
				&print_textonly (' ' x ($length_max_description - length($text{$key_lookup}->{$lang})) );

				my $key_lookup_extra;

				if ($key_lookup =~ /^(IPV4|IPV6)(_REGISTRY)?$/o) {
					$key_lookup_extra = "_REGISTRY" if ($flag_whoisurl == 1);
				} elsif ($key_lookup =~ /^([^_]+)(_.+)$/o) {
					$key_lookup_extra = $2;
				};

				if (defined $key_lookup_extra) {
					if (defined $text_extra{$key_lookup_extra}->{$lang}) {
						my $text = $text_extra{$key_lookup_extra}->{$lang};

						$text =~ s/REGISTRY/$whois_registry/g if ($flag_whoisurl == 1);
						$text =~ s/reserved\(([^)]+)\)/IETF/g if ($flag_whoisurl == 1);

						&print_tagoutput("<br><font size=\"-2\">(" . $text . ")</font>");
					};
				};
			} elsif ($key_lookup =~ /^(IP2LOCATION|GEOIP2|DBIP2)(_.+)$/o) {
				# fallback
				my $key_lookup_without_db = $2;
				if (defined $text{$key_lookup_without_db}->{$lang}) {
					print $text_geodb{$1} . " " . $text{$key_lookup_without_db}->{$lang};
					&print_textonly (' ' x ($length_max_description - length($text{$key_lookup_without_db}->{$lang}) - length($text_geodb{$1}) - 1) );
				} else {
					&print_textonly (' ' x ($length_max_description) );
				};

				# print information about link
				if (defined $text_extra{$key_lookup_without_db}->{$lang}) {
					&print_tagoutput("<font size=\"-2\">(" . $text_extra{$key_lookup_without_db}->{$lang} . ")</font>")
				};
			} else {
				&print_textonly (' ' x ($length_max_description) );
			};
			&print_textonly (" | ");
			&print_tagoutput ( "</font>" ) if ($count_key_embedded > 0);
			&print_tagoutput ( "</td>\n" );
		};

		my $color = "";

		if (defined $colormode) {
			# set color

			if ($key eq "EUI64_SCOPE" && $$phash{$key} eq "local-random") {
				$color = "lime";
			} elsif ($key eq "EUI64_SCOPE" && $$phash{$key} eq "local") {
				$color = "cyan";
			} elsif ($key eq "EUI48_SCOPE") {
				if ($$phash{$key} eq "global") {
					$color = "yellow";
				};
			} elsif ($key eq "IID" && defined $$phash{"EUI64_SCOPE"} && $$phash{"EUI64_SCOPE"} eq "local") {
				$color = "cyan";
			} elsif ($key eq "IID" && defined $$phash{"EUI64_SCOPE"} && $$phash{"EUI64_SCOPE"} eq "local-random") {
				$color = "lime";
			} elsif ($key eq "EUI64" && defined $$phash{"OUI"}) {
				$color = "yellow";
			} elsif ($key eq "EUI48" && defined $$phash{"OUI"}) {
				$color = "yellow";
			} elsif ($key eq "OUI") {
				$color = "yellow";
			} else {
			};
		};

		# set background color
		if ($color ne "") {
			&print_tagoutput ( "        <td colspan=\"" . ($db_columns + 1) . "\" style=\"background-color: " . $color . ";\">" );
		} else {
			&print_tagoutput ( "        <td colspan=\"" . ($db_columns + 1) . "\">" );
		};

		if ($flag_tt) {
			&print_tagoutput ( "<tt>" );
		};

		my ($info, $rfc, $section, $url, $url2);
		if ( $flag_whoisurl == 1 ) {
			if ( $whois_registry =~ /^(.*)\(LISP(#.*)\)$/o ) {
				($whois_registry, $section) = ($1, $2);

				if ($key_lookup =~ /^IPV(4|6)_REGISTRY/o) {
					$info = "LISP" . $section;
					$url2 = $url_lisp . $section;
				};
			} elsif ( $whois_registry =~ /^reserved\((.*)\)$/o ) {
				($rfc, $section) = split /#/, $1;
				$url = $url_rfc . lc($rfc);
				if (defined $section) {
					$url .= "#section-" . $section;
				};
			};

			if ( defined $url_whoisservers{$whois_registry}->{$whois_type} ) {
				if ( $url_whoisservers{$whois_registry}->{$whois_type} ne "" ) {
					if ($key_lookup =~ /^IPV(4|6)_REGISTRY/o) {
						my $ip;
						if ($whois_type eq "ipv4") {
							$ip = $$phash{'IPV4' . $key_embedded};
						} elsif ($whois_type eq "ipv6") {
							$ip = $$phash{'IPV6' . $key_embedded};
						};

						if (defined $ip) {
							$url = $url_whoisservers{$whois_registry}->{$whois_type} . $ip;
						} else {
							# something strange happend
							$flag_whoisurl = 0;
						};
					} else {
						$url = $url_whoisservers{$whois_registry}->{$whois_type} . $$phash{$key};
					};
				} else {
					$flag_whoisurl = 0;
				};
			} else {
				if ($debug & 0x08) {
					logging("NOTICE", "whoisserver is not defined");
				};
				$flag_whoisurl = 0;
			};
		};

		if ($key =~ /GEONAME_ID/o && $key_lookup !~ /_(SOURCE|TYPE)$/o) {
			$url = $url_geoname_by_id;
			$url =~ s/#GEONAME_ID#/$$phash{$key}/g;
			$url =~ s/#USERNAME#/$username_geonamesorg/g;
			$api_used_geonamesorg = 1;
		} elsif ($key =~ /(LATITUDE|LONGITUDE)/o) {
			my $combined_key;
			my $combined_valid = 0;
			$url = $url_openstreetmap_by_lat_lon;

			if ($key =~ /LATITUDE/o) {
				$url =~ s/#LATITUDE#/$$phash{$key}/g;
				$combined_valid++;

				$combined_key = $key;
				$combined_key =~ s/LATITUDE/LONGITUDE/o;
				if(defined $$phash{$combined_key}) {
					$url =~ s/#LONGITUDE#/$$phash{$combined_key}/g;
					$combined_valid++;
				};
			} elsif ($key =~ /LONGITUDE/o) {
				$url =~ s/#LONGITUDE#/$$phash{$key}/g;
				$combined_valid++;

				$combined_key = $key;
				$combined_key =~ s/LONGITUDE/LATITUDE/o;
				if(defined $$phash{$combined_key}) {
					$url =~ s/#LATITUDE#/$$phash{$combined_key}/g;
					$combined_valid++;
				};
			};

			if ($combined_valid != 2) {
				$url = undef;
			};
		} elsif ($key_lookup =~ /WEATHERSTATIONCODE/o) {
			$url = $url_weather_com_by_station;
			$url =~ s/#WEATHERSTATIONCODE#/$$phash{$key}/g;
		};
		
		&print_tagoutput ( "<font size=\"-1\">" ) if ($count_key_embedded > 0);

		if (defined $url && defined $url2) {
			&print_tagoutput ( "<a target=\"_blank\" href=\"" . $url . "\">" );
			print $whois_registry;
			&print_tagoutput ( "</a>" );

			print "(";
			&print_tagoutput ( "<a target=\"_blank\" href=\"" . $url2 . "\">" );
			print $info;
			&print_tagoutput ( "</a>" );
			print ")";
		} elsif (defined $url) {
			&print_tagoutput ( "<a target=\"_blank\" href=\"" . $url . "\">" );
			print $chr_quote . $$phash{$key} . $chr_quote;
			&print_tagoutput ( "</a>" );
		} else {
			print $chr_quote . $$phash{$key} . $chr_quote;
		};

		&print_tagoutput ( "</font>" ) if ($count_key_embedded > 0);

		if ($flag_tt) {
			&print_tagoutput ( "</tt>" );
		};
		&print_tagoutput ( "</td>\n" );
		&print_tagoutput ( "      </tr>\n" );
		&print_textonly ("\n");
	};


	if ($mode_db_entries_in_subcolumns != 1) {
		# nothing more to do
		return;
	};

	## database subcolumns mode
	# print header
	my ($r, $g, $b);
	my $shift;

	# loop through all embedded
	for my $embedded (keys %key_values_db_tokens) {
		my $entry = " for $embedded";
		$entry =~ s/[\[\]]//g;

		if ($embedded eq "default") {
			$entry = "";
		};

		($r, $g, $b) = (240, 240, 255);
		&print_tagoutput ( "      <tr>\n" );
		&print_tagoutput ( "        <th style=\"background-color:rgb(255,255,224);\" align=\"left\" colspan=\"2\">Information from databases$entry</th>\n" );
		for my $key (sort keys %key_db_tokens) {
			&print_tagoutput ("        <th style=\"background-color:rgb(" . $r . "," . $g . "," . $b . "); font-size:$font_size_1;\" align=\"left\">");
			if (defined $urls{$key}) {
				# add URL
				&print_tagoutput("<a target=\"_blank\" href=\"" . $urls{$key} . "\">" . $key . "</a>");
			} else {
				&print_tagoutput($key);
			};
			if (defined $text_geodb{$key}) {
				&print_tagoutput("<br><font size=\"-2\">" . $text_geodb{$key} . "</font>");
			};
			&print_tagoutput("</th>\n");
			$shift = $r; $r = $g; $g = $b; $b = $shift;	
		};
		&print_tagoutput ( "      </tr>\n" );

		for my $key_lookup (sort keys %{$key_values_db_tokens{$embedded}}) {
			&print_tagoutput ( "      <tr>\n" );
			&print_tagoutput ( "        <th align=\"left\" style=\"background-color:rgb(255,255,224); font-size:$font_size_1;\"><b>$key_lookup</b></th>\n" );

			my $key;
			if ($embedded eq "default") {
				$key = $key_lookup;
			} else {
				$key = $key_lookup . $embedded;
			};

			# print description
			if ($outputtype ne "simple") {
				&print_tagoutput ( "        <td style=\"background-color:rgb(255,255,224); font-size:$font_size_1;\">" );
				if (defined $text{"_" . $key_lookup}->{$lang}) {
					print $text{"_" . $key_lookup}->{$lang};

					if (defined $text_extra{"_" . $key_lookup}->{$lang}) {
						&print_tagoutput("<br><font size=\"-2\">(" . $text_extra{"_" . $key_lookup}->{$lang} . ")</font>")
					};
				} elsif ($key_lookup =~ /^(IP2LOCATION|GEOIP2|DBIP2)$/o) {
					# fallback
					my $key_lookup_without_db = $2;
					if (defined $text{$key_lookup_without_db}->{$lang}) {
						print $1 . " " . $text{$key_lookup_without_db}->{$lang};
					};
				};
				&print_tagoutput ( "</td>\n" );
			};

			# print value
			($r, $g, $b) = (240, 240, 255);
			for my $db (sort keys %key_db_tokens) {
				&print_tagoutput ( "        <td style=\"background-color:rgb(" . $r . "," . $g . "," . $b . "); font-size:$font_size_1;\">" );
				my $key_result = $db . "_" . $key;
				if (defined $$phash{$key_result}) {
					my $flag = 0;
					my $url;
					if ($key_lookup =~ /GEONAME_ID/o && $key_lookup !~ /_(SOURCE|TYPE)$/o) {
						$url = $url_geoname_by_id;
						$url =~ s/#GEONAME_ID#/$$phash{$key_result}/g;
						$url =~ s/#USERNAME#/$username_geonamesorg/g;
						$api_used_geonamesorg = 1;
						&print_tagoutput ( "<a target=\"_blank\" href=\"" . $url . "\">" );
						$flag = 1;
					} elsif ($key =~ /(LATITUDE|LONGITUDE)/o) {
						printf STDERR "DEBUG : found combination key master: %s\n", $key if ($debug & 0x2000);
						my $combined_key;
						my $combined_valid = 0;
						$url = $url_openstreetmap_by_lat_lon;

						if ($key_lookup =~ /LATITUDE/o) {
							$url =~ s/#LATITUDE#/$$phash{$key_result}/g;
							$combined_valid++;

							$combined_key = $key_result;
							$combined_key =~ s/LATITUDE/LONGITUDE/o;
							printf STDERR "DEBUG : search for combination key slave: %s\n", $combined_key if ($debug & 0x2000);
							if(defined $$phash{$combined_key}) {
								$url =~ s/#LONGITUDE#/$$phash{$combined_key}/g;
								$combined_valid++;
							};
						} elsif ($key_lookup =~ /LONGITUDE/o) {
							$url =~ s/#LONGITUDE#/$$phash{$key_result}/g;
							$combined_valid++;

							$combined_key = $key_result;
							$combined_key =~ s/LONGITUDE/LATITUDE/o;
							if(defined $$phash{$combined_key}) {
								$url =~ s/#LATITUDE#/$$phash{$combined_key}/g;
								$combined_valid++;
							};
						};

						if ($combined_valid == 2) {
							&print_tagoutput ( "<a target=\"_blank\" href=\"" . $url . "\">" );
							$flag = 1; # workaround to close a tag below
						};
					} elsif ($key_lookup =~ /WEATHERSTATIONCODE/o) {
						$url = $url_weather_com_by_station;
						$url =~ s/#WEATHERSTATIONCODE#/$$phash{$key_result}/g;
						&print_tagoutput ( "<a target=\"_blank\" href=\"" . $url . "\">" );
						$flag = 1;
					};

					print $$phash{$key_result};

					if ( $flag == 1 ) {
						&print_tagoutput ( "</a>" );
					};
				} else {
					print "-";
				};
				&print_tagoutput ( "</td>\n" );
				$shift = $r; $r = $g; $g = $b; $b = $shift;	
			};
			&print_tagoutput ( "      </tr>\n" );
			&print_textonly ("\n");
		};
	};
};

############### Anti-DoS functions

sub salt_generate() {
	my $salt = "";
	my $result;
	my $f;

	logging("DEBUG", "salt_generate/start") if ($debug & 0x0100);

	# first try to use system UUID
	if (-f "/etc/sysconfig/hw-uuid") {
		logging("DEBUG", "hardware uuid file found") if ($debug & 0x0100);

		$result = open(INPUT, "/etc/sysconfig/hw-uuid");
		if (defined $result) {
			$salt = <INPUT>;
			close(INPUT);
			chomp($salt);
			if (length($salt) > 0) {
				logging("DEBUG", "hardware uuid found: $salt") if ($debug & 0x0100);
			} else {
				logging("DEBUG", "hardware uuid found but contents is empty") if ($debug & 0x0100)
			};
		} else {
			logging("NOTICE", "hardware uuid file exists, but can't open");
		};
	};

	# local data
	if (length($salt) == 0) {
		if (defined $ENV{'HOSTNAME'}) {
			$salt = $ENV{'HOSTNAME'};
		} elsif (defined $ENV{'SERVER_NAME'}) {
			$salt = $ENV{'SERVER_NAME'};
		} else {
			$salt = "unknown-hosts";
		};

		for $f ("/etc/passwd", "/etc/hosts", "/etc/resolv.conf") {
			if (-f "$f") {
				if (length($salt) > 0) { $salt .= "-"; };
				$salt .= sprintf("%x", (stat("$f"))[9]);
			};
		};
		logging("DEBUG", "local generated: $salt") if ($debug & 0x0100);
	};

	logging("DEBUG", "salt_generate/salt: $salt") if ($debug & 0x0100);
	return($salt);
}


sub token_generate($$$) {
	my $remote = $_[2] || "127.0.0.1";

	logging("DEBUG", "token_generate/salt  : $_[0]")   if ($debug & 0x0100);
	logging("DEBUG", "token_generate/time  : $_[1]")   if ($debug & 0x0100);
	logging("DEBUG", "token_generate/remote: $remote") if ($debug & 0x0100);


	if ($mask_remote_address == 1) {
		# mask remote information
		if ($_[2] =~ /^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)[0-9]{1,3}$/) {
			# IPv4 address, blank last 8 bits
			$remote = $1;
		} elsif ($_[2] =~ /^([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)[0-9]{1,3}$/) {
			# IPv4 address, blank last 8 bits
			$remote = $1;
		};
		if ($remote ne $_[2]) {
			logging("DEBUG", "token_generate/remote masked to: $remote") if ($debug & 0x0100);
		};
	};

	my $token = $_[0] . "#" . $_[1] . "#" . $remote;

	logging("DEBUG", "token: $token") if ($debug & 0x0100);

	my $token_sha1 = Digest::SHA1::sha1_hex($token);
	my $token_md5  = Digest::MD5::md5_hex($token);

	chomp $token_sha1;
	chomp $token_md5;

	logging("DEBUG", "token/md5 : $token_md5")  if ($debug & 0x0100);
	logging("DEBUG", "token/sha1: $token_sha1") if ($debug & 0x0100);

	return($token_md5 . $token_sha1);
};

####################
############### Main
####################

# Parse query string first 
if ( defined $ENV{'QUERY_STRING'} ) {
	# split query string
	foreach my $query_stringlet (split /[\?\&]/, $ENV{'QUERY_STRING'}) {
		if ($query_stringlet !~ /^([[:alnum:]]+)=([[:alnum:].\-:%+]+)$/ ) {
			logging("ERROR", "generic problem with data in query_stringlet");
			&print_error();
			next;
		};

		my ($name, $value) = ($1, $2);

		if ($name eq "lang") {
			for my $langtest (@supported_languages) {
				if ($value eq $langtest) {
					$lang = $langtest;
					last;
				};
			};
		} elsif ($name eq "format") {
			for my $formattest (@supported_formats) {
				if ($value eq $formattest) {
					$outputformat = $formattest;
					last;
				};
			};
		} elsif ($name eq "input") {
			if (length($value) >  $maxenvlength) {
				logging("ERROR", "problem with data in query_stringlet (input too long)");
				&print_error();
				next;
			};
			$input = uri_unescape($value);
			$input =~ s/^[+\s]+//g; # remove leading whitespaces (Firefox codes space as '+')
			$input =~ s/[+\s]+$//g; # remove trailing whitespaces (Firefox codes space as '+')
			if ($input !~ /^([[:alnum:]\/\.\:-]+)$/) {
				logging("ERROR", "problem with data in query_stringlet (input has unexpected chars)");
				&print_error();
			};
			logging("DEBUG", "QUERY_STRING contains: input=" . $input) if ($debug & 0x40);
		} elsif ($name eq "tokenhash") {
			if (length($value) >  $maxenvlength) {
				logging("ERROR", "problem with data in query_stringlet (tokenhash too long)");
				&print_error();
				next;
			};
			if (length($value) > 32 + 40) {
				logging("ERROR", "problem with tokenhash: " . length($value) . " chars, more than 72 chars");
				&print_error();
				next;
			};
			if ($value !~ /^[[:xdigit:]]+$/) {
				logging("ERROR", "problem with data in query_stringlet (tokenhash has unexpected chars)");
				&print_error();
				next;
			};
			$tokenhash = $value;
			$tokenhash_found = 1;
			logging("DEBUG", "QUERY_STRING contains: tokenhash=" . $tokenhash) if ($debug & 0x40);
		} elsif ($name eq "tokentime") {
			if (length($value) >  $maxenvlength) {
				logging("ERROR", "problem with data in query_stringlet (tokentime too long)");
				&print_error();
				next;
			};
			if (length($value) != 10) {
				logging("ERROR", "problem with tokentime: " . length($value) . " chars, not 10 chars");
				&print_error();
				next;
			};
			if ($value !~ /^[[:digit:]]+$/) {
				logging("ERROR", "problem with data in query_stringlet (tokentime has unexpected chars)");
				&print_error();
				next;
			};
			$tokentime = $value;
			$tokentime_found = 1;
			logging("DEBUG", "QUERY_STRING contains: tokentime=" . $tokentime) if ($debug & 0x40);
		};
	};
};


## check system health
if (($debug & 0x4000) == 0) {
	# retrieve amount of CPU cores
	my $cpu_cores = 1;
	if (-r "/proc/cpuinfo") {
		open my $h, "/proc/cpuinfo";
		if ($h) {
			my @info = <$h>;
			close $h;
			my $cpu_count = scalar(grep /^processor/, @info);
			if ($cpu_count > 1) {
				$cpu_cores = $cpu_count;
			}; 
			#logging("DEBUG", "System has CPU cores: $cpu_cores");
		};
	};

	# check system load
	if (-r "/proc/loadavg") {
		open my $h, "/proc/loadavg";
		if ($h) {
			my @info = <$h>;
			close $h;
			my ($load1, $load5, $load15, $other) = (split / /, $info[0]);
			my $overload = 0;

			# normalized
			my ($load1_norm, $load5_norm, $load15_norm) = ($load1, $load5, $load15);
			$load1_norm = $load1 / $cpu_cores;
			$load5_norm = $load5 / $cpu_cores;
			$load15_norm = $load15 / $cpu_cores;

			if ($load15_norm > $load15_max) {
				$overload = 15;
			} elsif ($load5_norm > $load5_max) {
				$overload = 5;
			} elsif ($load1_norm > $load1_max) {
				$overload = 1;
			};

			if ($overload > 0) {
				logging("WARN", "System overloaded by load$overload load=$load1/$load5/$load15 load_norm=$load1_norm/$load5_norm/$load15_norm limit=$load1_max/$load5_max/$load15_max cpus=$cpu_cores");
				$error = "System overloaded, please retry in $overload minutes - sorry";
				$error_flag = 1; # no automatic redirect
				&print_error();
			};
		};
	};

	my $proc_count = 0;
	my @proc_list;

	if ($proc_max != 0) {
		if ((defined $bin_ps) && (-x $bin_ps)) {
			my @proc_table = `$bin_ps -eo args`;
			@proc_list = grep /perl .*( |\/)$proc_pattern/, @proc_table;

			$proc_count = scalar(@proc_list);
			logging("DEBUG", "System has 'perl $proc_pattern' running in parallel: procs=$proc_count") if ($debug & 0x0100);
		};

		if ($proc_count > 0) {
			# check amount of processes
			my $proc_count_norm = $proc_count / $cpu_cores;
			if ($proc_count_norm > $proc_max) {
				logging("WARN", "System has too much 'perl $proc_pattern' running in parallel: procs=$proc_count procs_norm=$proc_count_norm limit=$proc_max");
				$error = "System overloaded, please retry in 1 minute - sorry";
				$error_flag = 1; # no automatic redirect
				&print_error();
			};
		};
	};
};


## Get variables
if ( defined $ENV{'REMOTE_ADDR'} ) {
	logging("DEBUG", "found environment: REMOTE_ADDR") if ($debug & 0x80);

	$ENV{'REMOTE_ADDR'} =~ /^([[:xdigit:]\.\:]+)$/o;
	if ( ! defined $1 || (length($1) > $maxenvlength)) {
		logging("ERROR", "problem with data (REMOTE_ADDR)");
		&print_error();
	};

	# validity checks
	if (length($ENV{'REMOTE_ADDR'}) < 2 || length($ENV{'REMOTE_ADDR'}) > 69) {
		logging("ERROR", "problem with REMOTE_ADDR: below 2 or more than 69 chars");
		&print_error();
	};

	$addr_remote = $1;
	logging("DEBUG", "environment: REMOTE_ADDR=$1") if ($debug & 0x80);
} else {
	$addr_remote = "127.0.0.1";
};


# verify token
if ($tokenhash_found == 1 && $tokentime_found == 1) {
	logging("DEBUG", "verify tokens now") if ($debug & 0x100);

	$salt = salt_generate();

	$tokenhash_verify = token_generate($salt, $tokentime, $addr_remote);

	if ($tokenhash_verify ne $tokenhash) {
		logging("ERROR", "tokenhash invalid");
		&print_error();
	};

	logging("DEBUG", "tokenhash valid") if ($debug & 0x100);

	if ($tokentime > time()) {
		logging("ERROR", "tokentime in the future (totally strange!)");
		&print_error();
	};

	my $delta = time() - $tokentime;
	if ($delta > $time_range_valid) {
		logging("ERROR", "tokentime expired");
		$error = "session expired";
		$error_insert_input = 1;
		&print_error();
	};

	if ($delta < $time_range_min) {
		logging("NOTICE", "tokentime usage too early, sleep seconds: " . ($time_range_min - $delta));
		sleep ($time_range_min - ($delta % 5)) if (! ($debug & 0x1000));
	} elsif (($delta % $time_range_min) < $time_range_min) {
		logging("NOTICE", "tokentime (re)usage not aligned to $time_range_min seconds, sleep seconds: " . ($time_range_min - ($delta % $time_range_min)));
		sleep ($time_range_min - ($delta % $time_range_min)) if (! ($debug & 0x1000));
	} else {
		logging("DEBUG", "tokentime in range") if ($debug & 0x100);
	};

} elsif ($tokenhash_found == 0 && $tokentime_found == 0) {
	if ((defined $input) && ($mode eq "form" || $mode eq "mixed")) {
		logging("NOTICE", "form/mixed mode, input given, but tokenhash/tokentime missing, use value as default and sleep seconds: 5");
		sleep (5) if (! ($debug & 0x1000));
		$input_default = 1; # take this input as default
	};
} else {
	logging("ERROR", "strange error occurs (tokentime/tokenhash only partially found)");
	&print_error();
}

if ($mode eq "form" || $mode eq "mixed") {
	# generate new token
	$salt = salt_generate();
	# generate token
	$tokentime = time();
	$tokenhash = token_generate($salt, $tokentime, $addr_remote);
	logging("DEBUG", "TOKENHASH=$tokenhash TOKENTIME=$tokentime REMOTE_ADDR=$addr_remote") if ($debug & 0x0100);
};

if ( defined $ENV{'REMOTE_HOST'} ) {
	$ENV{'REMOTE_HOST'} =~ /^([[:alnum:]\.\-_]*)$/;
	if ( ! defined $1 || (length($1) > $maxenvlength)) {
		logging("ERROR", "problem with data (REMOTE_HOST)");
		&print_error();
	};
	$name_remote = $1;
};

if ( defined $ENV{'HTTP_USER_AGENT'} ) {
	$ENV{'HTTP_USER_AGENT'} =~ /^([[:alnum:]\[\]\/\(\)\\\.\-+\;\: ]*)$/;
	if ( ! defined $1 || (length($1) > $maxenvlength)) {
		# not a problem, skip it
	} else {
		$user_agent = $1;
	};
};

if ( defined $ENV{'HTTP_X_FORWARDED_FOR'} ) {
	$ENV{'HTTP_X_FORWARDED_FOR'} =~ /^([[:alnum:]\[\]\/\(\)\\\.\-+\,\: ]*)$/;
	if ( ! defined $1 || (length($1) > $maxenvlength)) {
		# not a problem, skip it
	} else {
		$x_forwarded_for = $1;
	};
};

if ( defined $ENV{'HTTP_VIA'} ) {
	$ENV{'HTTP_VIA'} =~ /^([[:alnum:]\[\]\/\(\)\\\.\-+\;\,\: ]*)$/;
	if ( ! defined $1 || (length($1) > $maxenvlength)) {
		# not a problem, skip it
	} else {
		$via = $1;
	};
};
if ( defined $ENV{'SERVER_ADDR'} ) {
	$ENV{'SERVER_ADDR'} =~ /^([[:xdigit:]\.\:]*)$/;
	if ( ! defined $1 || (length($1) > $maxenvlength)) {
		logging("ERROR", "problem with data (SERVER_ADDR)");
		&print_error();
	};
	$addr_server = $1;
};

if ( defined $ENV{'SERVER_NAME'} ) {
	$ENV{'SERVER_NAME'} =~ /^([[:alnum:]\[\]\.\-\:_]*)$/;
	if ( ! defined $1 || (length($1) > $maxenvlength)) {
		logging("ERROR", "problem with data (SERVER_NAME)");
		&print_error();
	};
	$name_server = $1;
};

if ( defined $ENV{'SCRIPT_NAME'} ) {
	$ENV{'SCRIPT_NAME'} =~ /^([[:alnum:]\?\%\&=\.\-\:_\/]*)$/;
	if ( ! defined $1 || (length($1) > $maxenvlength)) {
		logging("ERROR", "problem with data (SCRIPT_NAME)");
		&print_error();
	};
	$script_name = $1;
} else {
	$script_name = "";
};


## Check caller and select output format and mode
if ( defined $ENV{'SERVER_PROTOCOL'} ) {
	if ( $ENV{'SERVER_PROTOCOL'} eq "INCLUDED" ) {
		$mode = "info"; # force info mode
		if ( $outputformat eq "htmlfull" ) {
			# Switch back to included html
			$outputformat = "html";
		};
	};
};

if ( defined $ENV{'HTTP_IPV6CALCWEB_OUTPUT_FORMAT'} ) {
	if ($ENV{'HTTP_IPV6CALCWEB_OUTPUT_FORMAT'} =~ /^(text|html|htmlfull|textkeyvalue)$/o) {
		$outputformat = $ENV{'HTTP_IPV6CALCWEB_OUTPUT_FORMAT'};
	};
};

if ( defined $ENV{'HTTP_IPV6CALCWEB_OUTPUT_FORMAT_HTML_DB'} ) {
	if ($ENV{'HTTP_IPV6CALCWEB_OUTPUT_FORMAT_HTML_DB'} =~ /^(sequential|subcolumns)$/o) {
		$outputformat_html_db = $ENV{'HTTP_IPV6CALCWEB_OUTPUT_FORMAT_HTML_DB'};
	};
};

if ($outputformat_html_db eq "subcolumns") {
	$mode_db_entries_in_subcolumns = 1;
};

if ($outputformat eq "textkeyvalue") {
	$outputtype = "simple";
	$chr_quote = "\"";
};

if (($outputformat ne "html") && ($outputformat ne "htmlfull")) {
	# no html format, disable subcolumn mode
	$mode_db_entries_in_subcolumns = 0;
};

## Check for binary ipv6calc exists and is executable
if (! defined $bin_ipv6calc) {
	logging("ERROR", "'bin_ipv6calc' not defined");
	&print_error();
};
if (length($bin_ipv6calc) == 0) {
	logging("ERROR", "'bin_ipv6calc' is empty");
	&print_error();
};
if (! -f $bin_ipv6calc ) {
	logging("ERROR", "$bin_ipv6calc (bin_ipv6calc) does not exist as file");
	&print_error();
};
if (! -x $bin_ipv6calc ) {
	logging("ERROR", "$bin_ipv6calc (bin_ipv6calc) not executable");
	&print_error();
};

## Check for library path exists
if (defined $lib_ipv6calc) {
	if (! -d $lib_ipv6calc) {
		logging("ERROR", "$lib_ipv6calc (lib_ipv6calc) not a directory");
	};
	if (! -r $lib_ipv6calc) {
		logging("ERROR", "$lib_ipv6calc (lib_ipv6calc) not readable");
	};
};

## Get and fill information
sub ipv6calc_exec($$$$) {
	# Anti-DoS sleep 100-200 ms
	my $sleep = 0.1 * (1 + rand(1));
	select(undef, undef, undef, $sleep) if (! ($debug & 0x1000));

	my $addr = $_[0];
	my $debug_tag = $_[1];
	my $p_hash = $_[2];
	my $p_array = $_[3];

	my $option_preload = "";
	
	if (! defined $addr) {
		logging("ERROR", "no input for ipv6calc (strange)");
		return 1;
	};

	if (defined $lib_ipv6calc) {
		$option_preload = "LD_LIBRARY_PATH=$lib_ipv6calc";
	};

	logging("DEBUG", "execute: $option_preload $bin_ipv6calc $options_ipv6calc $addr") if ($debug & 0x1);

	my @info = `$option_preload $bin_ipv6calc $options_ipv6calc $addr`;

	if ( $? != 0 ) {
		logging("ERROR", "ipv6calc did not proper return ($addr)");
		return 1;
	};

	logging("DEBUG", $debug_tag) if ($debug & 0x10);

	for my $line (@info) {
		chomp($line);
		logging("DEBUG", $line) if ($debug & 0x10);

		my ( $key, $content ) = split /=/, $line, 2;
		if ( (! defined $key) || (! defined $content) ) {
			logging("ERROR", "ipv6calc output parsing problem ($addr)");
			return 1;
		};
		chomp $content;

		if ( $key =~ /^IP2LOCATION_/ ) {
			#$flag_ip2location_used = 1;
			if ( $content =~ /^This (parameter|record) is unavailable/o || length($content) == 0 || $content eq "-" || $content eq "??") {
				# $content = "n/a";
				next;
			};
		};

		# remove quotes around contents
		$content =~ s/^\"//;
		$content =~ s/\"$//;

		# catch internal keys
		if ( $key =~ /^IPV6CALC_/ ) {
			if ( $key eq "IPV6CALC_COPYRIGHT" ) {
				$ipv6calc_copyright = $content;
			};
			if ( $key eq "IPV6CALC_VERSION" ) {
				$ipv6calc_version = $content;
			};
			if ( $key eq "IPV6CALC_NAME" ) {
				$ipv6calc_name = $content;
			};
			if ( $key eq "IPV6CALC_FEATURES" ) {
				$ipv6calc_features = $content;
				foreach my $feature (split / /, $ipv6calc_features) {
					$ipv6calc_feature_hash{$feature} = 1;
				};
			};
			if ( $key eq "IPV6CALC_CAPABILITIES" ) {
				$ipv6calc_capabilities = $content;
				foreach my $capability (split / /, $ipv6calc_capabilities) {
					# remove brackets
					$capability =~ s/\([^)]*\)//g;
					$ipv6calc_capability_hash{$capability} = 1;
				};
			};
			if ( $key =~ /^IPV6CALC_SETTINGS_(.*)$/ ) {
				if (! defined $ipv6calc_settings_hash{$1}) {
					if (length($ipv6calc_settings) > 0) {
						$ipv6calc_settings .= " ";
					};

					my $t = $1;

					$ipv6calc_settings_hash{$t} = $content;
					$ipv6calc_settings .= $t . ":" . $content;
				};
			};
		};

		$$p_hash{$key} = $content;
		push @$p_array, $key;
	};
	return 0;
};

my $result;

## check HTTP_X_FORWARDED_FOR
if (defined $x_forwarded_for) {
	my $addr_remote_temp = $addr_remote; # save exit proxy address

	my $x_forwarded_for_entries = 0;

	foreach my $address (split /, /, $x_forwarded_for) {
		if ($address !~ /^([[:xdigit:]\.\:]+)$/) {
			# silently skip
			next;
		};

		if ($x_forwarded_for_entries == 0) {
			# first found entry is real client
			$addr_remote = $address;
		} else {
			# next proxy
			push @proxy_addresses, $address;
		};

		$x_forwarded_for_entries++;
	};

	if ($x_forwarded_for_entries > 0) {
		# addr_remote is exit proxy
		push @proxy_addresses, $addr_remote_temp;
	};
};

## check HTTP_VIA
if (defined $via) {
	foreach my $entry (split /,/, $via) {
		$entry =~ s/^ +//o; # remove leading spaces
		$entry =~ s/ +$//o; # remove trailing spaces
		push @proxy_infos, $entry;
	};
};

## check proxy information
if (scalar(@proxy_addresses) != scalar(@proxy_infos)) {
	# amount not matching, don't show information
	undef @proxy_infos;
};

## check features
$result = ipv6calc_exec("127.0.0.1", "***feature***", \%infoh_localhost4, \@sort_localhost4);
if ($debug & 0x8000) {
	foreach my $key (sort keys %ipv6calc_feature_hash) {
		logging("DEBUG", "feature    " . $key . "=" . $ipv6calc_feature_hash{$key});
	};
	foreach my $key (sort keys %ipv6calc_capability_hash) {
		logging("DEBUG", "capability " . $key . "=" . $ipv6calc_capability_hash{$key});
	};
};
if ($result != 0) {
	&print_error();
};

# add options depending of available capabilities (fallback: features)
if (length($ipv6calc_capabilities) > 0) {
	if (defined $ipv6calc_capability_hash{"GeoIP2"} && ($ipv6calc_capability_hash{"GeoIP2"} == 1)) {
		if ($db_enabled{"GEOIP2"} == 0) {
			$options_ipv6calc .= " --db-geoip2-disable";
		} else {
			$options_ipv6calc .= $options_ipv6calc_geoip2;
			$db_columns++ if ($mode_db_entries_in_subcolumns == 1);
			$key_db_tokens{"GEOIP2"} = 0;
		};
	};
	if (defined $ipv6calc_capability_hash{"IP2Location"} && ($ipv6calc_capability_hash{"IP2Location"} == 1)) {
		if ($db_enabled{"IP2LOCATION"} == 0) {
			$options_ipv6calc .= " --db-ip2location-disable";
		} else {
			$options_ipv6calc .= $options_ipv6calc_ip2location;
			$db_columns++ if ($mode_db_entries_in_subcolumns == 1);
			$key_db_tokens{"IP2LOCATION"} = 0;
		};
	};
	if (defined $ipv6calc_capability_hash{"DBIP2"} && ($ipv6calc_capability_hash{"DBIP2"} == 1)) {
		if ($db_enabled{"DBIP2"} == 0) {
			$options_ipv6calc .= " --db-dbip2-disable";
		} else {
			$options_ipv6calc .= $options_ipv6calc_dbip2;
			$db_columns++ if ($mode_db_entries_in_subcolumns == 1);
			$key_db_tokens{"DBIP2"} = 0;
		};
	};
	if (defined $ipv6calc_capability_hash{"External"} && ($ipv6calc_capability_hash{"External"} == 1)) {
		if ($db_enabled{"EXTERNAL"} == 0) {
			$options_ipv6calc .= " --db-external-disable";
		} else {
			$options_ipv6calc .= $options_ipv6calc_external;
			$db_columns++ if ($mode_db_entries_in_subcolumns == 1);
			$key_db_tokens{"EXTERNAL"} = 0;
		};
	};
} else {
	# fallback for ipv6calc < 0.97.4
	if (defined $ipv6calc_feature_hash{"IP2Location"} && ($ipv6calc_feature_hash{"IP2Location"} == "1")) {
		if ($db_enabled{"IP2LOCATION"} == 0) {
			$options_ipv6calc .= " --db-ip2location-disable";
		} else {
			$options_ipv6calc .= $options_ipv6calc_ip2location;
			$db_columns++ if ($mode_db_entries_in_subcolumns == 1);
			$key_db_tokens{"IP2LOCATION"} = 0;
		};
	};
};

if ($db_columns == 0) {
	# no databases, disable subcolumn mode
	$mode_db_entries_in_subcolumns = 0;
} elsif ($db_columns > 0) {
	# one column is always available
	$db_columns--;
};


## remote
if ( defined $addr_remote ) {
	$result = ipv6calc_exec($addr_remote, "***remote***", \%infoh_remote, \@sort_remote);
	if ($result != 0) {
		&print_error();
	};

	if (defined $name_remote) {
		if ($name_remote ne $addr_remote) {
			$infoh_remote{'NAME'} = $name_remote;
			push @sort_remote, 'NAME';
		};
	};
	if (defined $user_agent) {
		$infoh_remote{'HTTP_USER_AGENT'} = $user_agent;
		push @sort_remote, 'HTTP_USER_AGENT';
	};

	# add x-forward-for information to client if not handled otherwise
	if ((defined $x_forwarded_for) && (scalar(@proxy_addresses) == 0)) {
		$infoh_remote{'HTTP_X_FORWARDED_FOR'} = $x_forwarded_for;
		push @sort_remote, 'HTTP_X_FORWARDED_FOR';
	};

	# add via information to client if not handled otherwise
	if ((defined $via) && (scalar(@proxy_infos) == 0)) {
		$infoh_remote{'HTTP_VIA'} = $via;
		push @sort_remote, 'HTTP_VIA';
	};
};

## server
if ((defined $addr_server) && ($skip_server == 0)) {
	$result = ipv6calc_exec($addr_server, "***server***", \%infoh_server, \@sort_server);
	if ($result != 0) {
		&print_error();
	};

	if (defined $name_server) {
		if ($name_server ne $addr_server) {
			$infoh_server{'NAME'} = $name_server;
			push @sort_server, 'NAME';
		};
	};
};

## input
if ((defined $input) && ($mode eq "form" || $mode eq "mixed") && ($tokenhash_found == 1 && $tokentime_found == 1)) {
	$result = ipv6calc_exec($input, "***input***", \%infoh_input, \@sort_input);
	if ($result != 0) {
		$infoh_input{'RESULT'} = "(unrecognized input)";
		push @sort_input, 'RESULT';
	};
};

## Print content
if ($debug & 0x01) {
	print STDERR "REMOTE\n";
	for my $key (keys %infoh_remote) {
		print STDERR " ". $key . "=" .  $infoh_remote{$key} . "\n";
	};
	print STDERR "SERVER\n";
	for my $key (keys %infoh_server) {
		print STDERR " " . $key . "=" .  $infoh_server{$key} . "\n";
	};
	print STDERR "INPUT\n";
	for my $key (keys %infoh_input) {
		print STDERR " " . $key . "=" .  $infoh_input{$key} . "\n";
	};
};

## Calculate max lengths
# geodb
my $length_max_key_geodb = 0;
my $length_max_desc_geodb = 0;
for my $key (keys %text_geodb) {
	if (length($key) > $length_max_key_geodb) {
		$length_max_key_geodb = length($key);
	};
	if (length($text_geodb{$key}) > $length_max_desc_geodb) {
		$length_max_desc_geodb = length($text_geodb{$key});
	};
};

# subkeys
my $length_max_key_sub = 0;
my $length_max_desc_sub = 0;
for my $key (keys %text) {
	next if ($key !~ /^_/o);

	if (length($key) > $length_max_key_sub) {
		$length_max_key_sub = length($key);
	};

	if (length($text{$key}->{$lang}) > $length_max_desc_sub) {
		$length_max_desc_sub = length($text{$key}->{$lang});

	};
};

# others
for my $key (keys %text) {
	next if ($key =~ /^_/o);

	if (length($key) > $length_max_key) {
		$length_max_key = length($key);
	};

	if (length($text{$key}->{$lang}) > $length_max_description) {
		$length_max_description = length($text{$key}->{$lang});

	};
};

# combine
if ($length_max_key_sub + $length_max_key_geodb > $length_max_key) {
	$length_max_key = $length_max_key_sub + $length_max_key_geodb;
};

if ($length_max_desc_sub + $length_max_desc_geodb + 1 > $length_max_description) {
	$length_max_description = $length_max_desc_sub + $length_max_desc_geodb + 1;
};

goto("OUTPUT_BEGIN");

OUTPUT_BEGIN_ERROR:
if (!defined $error) {
	$error = "$program_name: unexpected problem occurs";
	$error_flag = 1;
} else {
	$error = "$program_name: " . $error;
};

$returncode = 1;

# delay error output by sleeping 3-6 seconds
select(undef, undef, undef, 3 * (1 + rand(1))) if (! ($debug & 0x1000));

OUTPUT_BEGIN:

## Print output
#
## HTML header
&print_tagoutput ("Content-type: text/html\n\n");
&print_textonly  ("Content-type: text/plain\n\n");

if ($outputformat eq "htmlfull") {
	&print_tagoutput ( "<!DOCTYPE html>\n" );
	&print_tagoutput ( "<html>\n" );
	&print_tagoutput ( "  <head>\n" );
	&print_tagoutput ( "    <meta name=\"Author\" content=\"Peter Bieringer\">\n" );
	&print_tagoutput ( "    <title>" );
	print $text{'title'}->{$lang};
	&print_tagoutput ( "</title>\n" );

	# automatic redirect on errors
	if (defined $error && defined $ENV{'SCRIPT_NAME'} && $error_flag == 0) {
		if ($error_insert_input == 1) {
			$uri .= "input=" . uri_escape($input);
		};
		if (defined $lang) {
			if (length($uri) > 0) { $uri .= "&"; };
			$uri .= "lang=$lang";
		};

		if (length($uri) > 0) { $uri = "?" . $uri; };
			
		&print_tagoutput ( "    <meta HTTP-EQUIV=\"refresh\" CONTENT=\"5;URL=" . $ENV{'SCRIPT_NAME'} . $uri . "\">\n" );
	};

	&print_tagoutput ( "  </head>\n" );
	&print_tagoutput ( "  <body>\n" );
};

if (defined $error) {
	print $error . "\n";
	if ($outputformat eq "htmlfull") {
		if (defined $error && defined $ENV{'SCRIPT_NAME'} && $error_flag == 0) {
			print " (redirected soon back)\n";
		} else {
			print " (hit 'back' in browser)\n";
		};
	};
	goto("OUTPUT_END");
};


&print_tagoutput ( "    <table border CELLSPACING=0 width=\"100%\">\n" );
&print_tagoutput ( "     <colgroup width=\"40%\" span=\"2\">\n" );
&print_tagoutput ( "      <col width=\"20%\">\n" );
&print_tagoutput ( "      <col width=\"20%\">\n" );
&print_tagoutput ( "     </colgroup>\n" );
&print_tagoutput ( "     <colgroup width=\"60%\" span=\"" . ($db_columns + 1) . "\">\n" );
for (my $i = 0; $i < $db_columns + 1; $i++) {
	&print_tagoutput ( "      <col width=\"" . int(60 / ($db_columns + 1)) . "%\">\n" );
};
&print_tagoutput ( "     </colgroup>\n" );

if ($mode eq "form" || $mode eq "mixed") {
	# sleep 500-1000 ms
	select(undef, undef, undef, 0.5 * (1 + rand(1))) if (! ($debug & 0x1000));

	# Input
	&print_textonly ("\n");
	&print_tagoutput ( "      <tr>\n" );
	&print_tagoutput ( "        <th style=\"background-color:#44DD44;\" align=\"center\" colspan=\"" . ($db_columns + 3) . "\">" );
	&print_tagoutput ( "Enter your IPv4/IPv6/MAC/EUI-64 " . $text{'Address'}->{$lang});
	&print_tagoutput ( "</th>\n" ); # same line
	&print_tagoutput ( "      <tr>\n" );
	&print_tagoutput ( "        <td align=\"center\" colspan=\"" . ($db_columns + 3) . "\">\n" );
	&print_tagoutput ( "          <p>\n" );
	&print_tagoutput ( "          <form action=\"" . $script_name . "\" method=\"get\">\n" );
	if (!defined $input) {
		$input = "::1";
		$input_default = 1;
	};
	&print_tagoutput ( "            <input name=\"input\" type=\"text\" size=\"64\" maxlength=\" $maxenvlength\" value=\"$input\">\n" );
	&print_tagoutput ( "            <input type=\"text\" size=\"128\" maxlength=\"128\" name=\"tokenhash\" value=\"" . $tokenhash . "\" hidden>\n" );
	&print_tagoutput ( "            <input type=\"text\" size=\"128\" maxlength=\"128\" name=\"tokentime\" value=\"" . $tokentime . "\" hidden>\n" );
	&print_tagoutput ( "            <input name=\"lang\" type=\"text\" size=\"128\" maxlength=\"128\" value=\"$lang\" hidden>\n" );
	&print_tagoutput ( "            <input type=\"submit\" value=\"" . $text{'send'}->{$lang} . "\">  <input type=\"reset\" value=\"" . $text{'cancel'}->{$lang} . "\">\n" );
	&print_tagoutput ( "          </form>\n" );
	&print_tagoutput ( "          <form action=\"" . $script_name . "\" method=\"get\">\n" );
	&print_tagoutput ( "            <input name=\"lang\" type=\"text\" size=\"128\" maxlength=\"128\" value=\"$lang\" hidden>\n" );
	&print_tagoutput ( "            <input type=\"submit\" value=\"" . $text{'clear'}->{$lang} . "\">\n" );
	&print_tagoutput ( "          </form>\n" );
	&print_textonly ("\n");
	&print_tagoutput ( "        </td>\n" );
	&print_tagoutput ( "      </tr>\n" );

	if (defined $input && !defined $input_default) {
		&print_tagoutput ( "      <tr>\n" );
		&print_tagoutput ( "        <th style=\"background-color:#44DD44;\" align=\"center\" colspan=\"" . ($db_columns + 3) . "\">" );
		print $text{'INPUT'}->{$lang} . " " . $input;
		&print_textonly ("\n");
		&print_tagoutput ( "</th>\n" ); # same line
		&print_tagoutput ( "      </tr>\n" );
		&print_infohash (\%infoh_input, \@sort_input, 'form');
	};
};

if ($mode eq "info" || $mode eq "mixed") {
	&print_textonly("INFO__OUTPUTVERSION=\"" . $outputversion_textkeyvalue . "\"\n") if ($outputformat eq "textkeyvalue");

	# Client
	&print_textonly ("\n") if ($outputformat ne "textkeyvalue");
	&print_tagoutput ( "      <tr>\n" );
	&print_tagoutput ( "        <th style=\"background-color:#FFDDDD;\" colspan=\"" . ($db_columns + 3) . "\">" );
	print $text{'REMOTE'}->{$lang} if ($outputformat ne "textkeyvalue");
	&print_textonly ("\n");
	&print_tagoutput ( "</th>\n" ); # same line
	&print_tagoutput ( "      </tr>\n" );
	&print_infohash (\%infoh_remote, \@sort_remote, 'client');

	# Proxy
	if (scalar(@proxy_addresses) > 0) {
		&print_textonly ("\n");
		&print_textonly("PROXY__COUNT=\"" . scalar(@proxy_addresses) . "\"\n") if ($outputformat eq "textkeyvalue");
	};

	my $proxy_count = 0;
	for my $address (@proxy_addresses) {
		$proxy_count++;

		undef %infoh_proxy;
		undef @sort_proxy;

		$result = ipv6calc_exec($address, "***proxy***", \%infoh_proxy, \@sort_proxy);
		if ($result != 0) {
			# silently skip
			next;
		} else {
			# add information from "via" header
			if (defined $proxy_infos[$proxy_count - 1]) {
				$infoh_proxy{'PROXY_INFO'} = $proxy_infos[$proxy_count - 1];
				splice @sort_proxy, 0, 0, 'PROXY_INFO';
			};

			&print_textonly ("\n") if ($outputformat ne "textkeyvalue");
			&print_tagoutput ( "      <tr>\n" );
			&print_tagoutput ( "        <th style=\"background-color:#EEDDDD;\" colspan=\"" . ($db_columns + 3) . "\">" );
			print $text{'PROXY'}->{$lang} . " #" . $proxy_count if ($outputformat ne "textkeyvalue");
			&print_textonly ("\n");
			&print_tagoutput ( "</th>\n" ); # same line
			&print_tagoutput ( "      </tr>\n" );
			&print_infohash (\%infoh_proxy, \@sort_proxy, 'proxy', $proxy_count);
		};
	};

	if ((defined $addr_server) && ($skip_server == 0)) {
		# Server
		&print_textonly ("\n") if ($outputformat ne "textkeyvalue");
		&print_tagoutput ( "      <tr>\n" );
		&print_tagoutput ( "        <th style=\"background-color:#DDFFDD;\" colspan=\"" . ($db_columns + 3) . "\">" );
		print $text{'SERVER'}->{$lang} if ($outputformat ne "textkeyvalue");
		&print_textonly ("\n");
		&print_tagoutput ( "</th>\n" ); # same line
		&print_tagoutput ( "      </tr>\n" );
		&print_infohash (\%infoh_server, \@sort_server, 'server');
	};
};

## create uri
sub create_uri($) {
	my $string = $_[0];
	$string =~ s#(https?://[^ ]+)#<a target="_blank" href="$1">$1</a>#g;
	return($string);
};

# Footer
&print_textonly ("\n");
&print_tagoutput ( "      <tr>\n" );
&print_tagoutput ( "        <td style=\"font-size:60%; background-color:#DDDDDD;\" colspan=\"" . ($db_columns + 3) . "\">\n" );

my $datetime = POSIX::strftime("%Y-%m-%d %H:%M:%S", gmtime) . " UTC";

if ($outputformat =~ /^html/o) {
	print "          " . $text{'generated'}->{$lang} . " " . encode_entities($program_name) . " " . encode_entities($program_version) . ", " . encode_entities($program_copyright) . " " . $text{'at'}->{$lang} . " " . $datetime . "\n";
	print "          <br>\n";
	print "          " . $text{'powered'}->{$lang} . " <a target=\"_blank\" href=\"" . $urls{'IPV6CALC'} . "\">" . encode_entities($ipv6calc_name) . "</a> " . encode_entities($ipv6calc_version) . ", " . encode_entities($ipv6calc_copyright);
	if (length($ipv6calc_features) > 0) {
		print " (features: " . $ipv6calc_features . ")";
	};
	if (length($ipv6calc_capabilities) > 0) {
		print " (capabilities: " . $ipv6calc_capabilities . ")";
	};
	if (length($ipv6calc_settings) > 0) {
		print " (settings: " . $ipv6calc_settings . ")";
	};

	print "\n";

	if (scalar(keys %db_info_ip2location) > 0) {
		print "          <br>\n";
		print "          " . $text{'powered'}->{$lang} . " <a target=\"_blank\" href=\"" . $urls{'IP2LOCATION'} . "\">IP2Location</a> " . $text{'database'}->{$lang} . " " . create_uri(encode_entities(join(" / ", sort keys %db_info_ip2location))). "\n";
	};

	if (scalar(keys %db_info_geoip2) > 0) {
		print "          <br>\n";
		print "          " . $text{'powered'}->{$lang} . " <a target=\"_blank\" href=\"" . $urls{'GEOIP2'} . "\">MaxMind</a> " . $text{'database'}->{$lang} . " " . encode_entities(join(" / ", sort keys %db_info_geoip2)) . "\n";
	};

	if (scalar(keys %db_info_dbip2) > 0) {
		print "          <br>\n";
		print "          " . $text{'powered'}->{$lang} . " <a target=\"_blank\" href=\"" . $urls{'DBIP2'} . "\">db-ip.com</a> " . $text{'database'}->{$lang} . " " . encode_entities(join(" / ", sort keys %db_info_dbip2)) . "\n";
	};

	if (scalar(keys %db_info_external) > 0) {
		print "          <br>\n";
		print "          " . $text{'powered'}->{$lang} . " External " . $text{'database'}->{$lang} . " " . encode_entities(join(" / ", sort keys %db_info_external)) . "\n";
	};

	if (scalar(keys %db_info_builtin) > 0) {
		print "          <br>\n";
		print "          " . $text{'powered'}->{$lang} . " BuiltIn " . $text{'database'}->{$lang} . " " . encode_entities(join(" / ", sort keys %db_info_builtin)) . "\n";
	};

	if ($api_used_geonamesorg > 0) {
		print "          <br>\n";
		print "          " . $text{'credits'}->{$lang} . " <a target=\"_blank\" href=\"" .  $urls{'GEONAMES'} . "\">GeoNames</a>\n";
	};
} else {
	# text output
	print "INFO__GENERATED=" if ($outputformat eq "textkeyvalue");
	print $chr_quote . $text{'generated'}->{$lang} . " " . $program_name . " " . $program_version . ", " . $program_copyright . " " . $text{'at'}->{$lang} . " " . $datetime . $chr_quote . "\n";
	print "INFO__POWEREDBY=" if ($outputformat eq "textkeyvalue");
	print $chr_quote . $text{'powered'}->{$lang} . " " . $ipv6calc_name . " " . $ipv6calc_version . ", " . $ipv6calc_copyright . " (" . $urls{'IPV6CALC'} . ")";
	if (length($ipv6calc_features) > 0) {
		print " (features: " . $ipv6calc_features . ")";
	};
	if (length($ipv6calc_capabilities) > 0) {
		print " (capabilities: " . $ipv6calc_capabilities . ")";
	};
	if (length($ipv6calc_settings) > 0) {
		print " (settings: " . $ipv6calc_settings . ")";
	};
	print $chr_quote . "\n";

	if (scalar(keys %db_info_ip2location) > 0) {
		print "INFO__POWEREDBY_IP2LOCATION=" if ($outputformat eq "textkeyvalue");
		print $chr_quote . $text{'powered'}->{$lang} . " " . $text{'database'}->{$lang} . " " . join(" / ", sort keys %db_info_ip2location) . " (" . $urls{'IP2LOCATION'} . ")" . $chr_quote . "\n";
	};

	if (scalar(keys %db_info_geoip2) > 0) {
		print "INFO__POWEREDBY_GEOIP2=" if ($outputformat eq "textkeyvalue");
		print $chr_quote . $text{'powered'}->{$lang} . " " . $text{'database'}->{$lang} . " " . join(" / ", sort keys %db_info_geoip2) . " (" . $urls{'GEOIP2'} . ")" . $chr_quote . "\n";
	};

	if (scalar(keys %db_info_dbip2) > 0) {
		print "INFO__POWEREDBY_DBIP2=" if ($outputformat eq "textkeyvalue");
		print $chr_quote . $text{'powered'}->{$lang} . " " . $text{'database'}->{$lang} . " " . join(" / ", sort keys %db_info_dbip2) . " (" . $urls{'DBIP2'} . ")" . $chr_quote . "\n";
	};

	if (scalar(keys %db_info_external) > 0) {
		print "INFO__POWEREDBY_EXTERNAL=" if ($outputformat eq "textkeyvalue");
		print $chr_quote . $text{'powered'}->{$lang} . " " . $text{'database'}->{$lang} . " " . join(" / ", sort keys %db_info_external) . $chr_quote . "\n";
	};

	if (scalar(keys %db_info_builtin) > 0) {
		print "INFO__POWEREDBY_BUILTIN=" if ($outputformat eq "textkeyvalue");
		print $chr_quote . $text{'powered'}->{$lang} . " " . $text{'database'}->{$lang} . " " . join(" / ", sort keys %db_info_builtin) . $chr_quote . "\n";
	};

	if ($api_used_geonamesorg > 0) {
		print "INFO__CREDITS_GEONAMESORG=" if ($outputformat eq "textkeyvalue");
		print $chr_quote . $text{'credits'}->{$lang} . " GeoNames (" . $urls{'GEONAMES'} . ")" . $chr_quote . "\n";
	};
};

&print_tagoutput ( "        </td>\n" );
&print_tagoutput ( "      </tr>\n" );
&print_tagoutput ( "    </table>\n" );


OUTPUT_END:

if ($outputformat eq "htmlfull") {
	&print_tagoutput ( "  </body>\n" );
	&print_tagoutput ( "</html>\n" );
};

exit ($returncode);
