diff --git a/src/app/Console/Command.php b/src/app/Console/Command.php new file mode 100644 index 00000000..2036fb70 --- /dev/null +++ b/src/app/Console/Command.php @@ -0,0 +1,104 @@ +getObject(\App\Domain::class, $domain, 'namespace'); + } + + /** + * Find an object. + * + * @param string $objectClass The name of the class + * @param string $objectIdOrTitle The name of a database field to match. + * @param string|null $objectTitle An additional database field to match. + * + * @return mixed + */ + public function getObject($objectClass, $objectIdOrTitle, $objectTitle) + { + $object = $objectClass::find($objectIdOrTitle); + + if (!$object && !empty($objectTitle)) { + $object = $objectClass::where($objectTitle, $objectIdOrTitle)->first(); + } + + return $object; + } + + /** + * Find the user. + * + * @param string $user User ID or email + * + * @return \App\User|null + */ + public function getUser($user) + { + return $this->getObject(\App\User::class, $user, 'email'); + } + + /** + * Find the wallet. + * + * @param string $wallet Wallet ID + * + * @return \App\Wallet|null + */ + public function getWallet($wallet) + { + return $this->getObject(\App\Wallet::class, $wallet, null); + } + + /** + * Return a string for output, with any additional attributes specified as well. + * + * @param mixed $entry An object + * + * @return string + */ + protected function toString($entry) + { + /** + * Haven't figured out yet, how to test if this command implements an option for additional + * attributes. + if (!in_array('attr', $this->options())) { + return $entry->{$entry->getKeyName()}; + } + */ + + $str = [ + $entry->{$entry->getKeyName()} + ]; + + foreach ($this->option('attr') as $attr) { + if ($attr == $entry->getKeyName()) { + $this->warn("Specifying {$attr} is not useful."); + continue; + } + + if (!array_key_exists($attr, $entry->toArray())) { + $this->error("Attribute {$attr} isn't available"); + continue; + } + + if (is_numeric($entry->{$attr})) { + $str[] = $entry->{$attr}; + } else { + $str[] = !empty($entry->{$attr}) ? $entry->{$attr} : "null"; + } + } + + return implode(" ", $str); + } +} diff --git a/src/app/Console/Commands/DataCountries.php b/src/app/Console/Commands/Data/Import/CountriesCommand.php similarity index 56% rename from src/app/Console/Commands/DataCountries.php rename to src/app/Console/Commands/Data/Import/CountriesCommand.php index ac94cdf6..080a54b2 100644 --- a/src/app/Console/Commands/DataCountries.php +++ b/src/app/Console/Commands/Data/Import/CountriesCommand.php @@ -1,97 +1,114 @@ currency 'LT' => 'EUR', ]; /** * The name and signature of the console command. * * @var string */ - protected $signature = 'data:countries'; + protected $signature = 'data:import:countries'; /** * The console command description. * * @var string */ - protected $description = 'Fetches countries map from wikipedia'; + protected $description = 'Fetches countries map from country.io'; /** * Execute the console command. * * @return mixed */ public function handle() { + $today = Carbon::now()->toDateString(); + $countries = []; $currencies = []; - $currencies_url = 'http://country.io/currency.json'; - $countries_url = 'http://country.io/names.json'; - - $this->info("Fetching currencies from $currencies_url..."); + $currencySource = 'http://country.io/currency.json'; + $countrySource = 'http://country.io/names.json'; - // fetch currency table and create an index by country page url - $currencies_json = file_get_contents($currencies_url); - - if (!$currencies_json) { - $this->error("Failed to fetch currencies"); - return; - } + // + // countries + // + $file = storage_path("countries-{$today}.json"); - $this->info("Fetching countries from $countries_url..."); + \App\Utils::downloadFile($countrySource, $file); - $countries_json = file_get_contents($countries_url); + $countryJson = file_get_contents($file); - if (!$countries_json) { + if (!$countryJson) { $this->error("Failed to fetch countries"); - return; + return 1; } - $currencies = json_decode($currencies_json, true); - $countries = json_decode($countries_json, true); + $countries = json_decode($countryJson, true); if (!is_array($countries) || empty($countries)) { $this->error("Invalid countries data"); + return 1; + } + + // + // currencies + // + $file = storage_path("currencies-{$today}.json"); + + \App\Utils::downloadFile($currencySource, $file); + + // fetch currency table and create an index by country page url + $currencyJson = file_get_contents($file); + + if (!$currencyJson) { + $this->error("Failed to fetch currencies"); return; } + $currencies = json_decode($currencyJson, true); + if (!is_array($currencies) || empty($currencies)) { $this->error("Invalid currencies data"); - return; + return 1; } + // + // export + // $file = resource_path('countries.php'); - $this->info("Generating resource file $file..."); - asort($countries); $out = " $name) { $currency = $currencies[$code] ?? null; if (!empty($this->currency_fixes[$code])) { $currency = $this->currency_fixes[$code]; } if (!$currency) { - $this->error("Unknown currency for {$name} ({$code}). Skipped."); + $this->warn("Unknown currency for {$name} ({$code}). Skipped."); continue; } $out .= sprintf(" '%s' => ['%s','%s'],\n", $code, $currency, addslashes($name)); } + $out .= "];\n"; file_put_contents($file, $out); } } diff --git a/src/app/Console/Commands/Data/Import/IP4NetsCommand.php b/src/app/Console/Commands/Data/Import/IP4NetsCommand.php new file mode 100644 index 00000000..5200fd78 --- /dev/null +++ b/src/app/Console/Commands/Data/Import/IP4NetsCommand.php @@ -0,0 +1,225 @@ + 'http://ftp.afrinic.net/stats/afrinic/delegated-afrinic-latest', + 'apnic' => 'http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest', + 'arin' => 'http://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest', + 'lacnic' => 'http://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-latest', + 'ripencc' => 'https://ftp.ripe.net/ripe/stats/delegated-ripencc-latest' + ]; + + $today = Carbon::now()->toDateString(); + + foreach ($rirs as $rir => $url) { + $file = storage_path("{$rir}-{$today}"); + + \App\Utils::downloadFile($url, $file); + + $serial = $this->serialFromStatsFile($file); + + if (!$serial) { + \Log::error("Can not derive serial from {$file}"); + continue; + } + + $numLines = $this->countLines($file); + + if (!$numLines) { + \Log::error("No relevant lines could be found in {$file}"); + continue; + } + + $bar = \App\Utils::createProgressBar( + $this->output, + $numLines, + "Importing IPv4 Networks from {$file}" + ); + + $fp = fopen($file, 'r'); + + $nets = []; + + while (!feof($fp)) { + $line = trim(fgets($fp)); + + if ($line == "") { + continue; + } + + if ((int)$line) { + continue; + } + + if ($line[0] == "#") { + continue; + } + + $items = explode('|', $line); + + if (sizeof($items) < 7) { + continue; + } + + if ($items[1] == "*") { + continue; + } + + if ($items[2] != "ipv4") { + continue; + } + + if ($items[5] == "00000000") { + $items[5] = "19700102"; + } + + if ($items[1] == "" || $items[1] == "ZZ") { + continue; + } + + $bar->advance(); + + $mask = 32 - log($items[4], 2); + + $net = \App\IP4Net::where( + [ + 'net_number' => $items[3], + 'net_mask' => $mask, + 'net_broadcast' => long2ip((ip2long($items[3]) + 2 ** (32 - $mask)) - 1) + ] + )->first(); + + if ($net) { + if ($net->updated_at > Carbon::now()->subDays(1)) { + continue; + } + + // don't use ->update() method because it doesn't update updated_at which we need for expiry + $net->rir_name = $rir; + $net->country = $items[1]; + $net->serial = $serial; + $net->updated_at = Carbon::now(); + + $net->save(); + + continue; + } + + $nets[] = [ + 'rir_name' => $rir, + 'net_number' => $items[3], + 'net_mask' => $mask, + 'net_broadcast' => long2ip((ip2long($items[3]) + 2 ** (32 - $mask)) - 1), + 'country' => $items[1], + 'serial' => $serial, + 'created_at' => Carbon::parse($items[5], 'UTC'), + 'updated_at' => Carbon::now() + ]; + + if (sizeof($nets) >= 100) { + \App\IP4Net::insert($nets); + $nets = []; + } + } + + if (sizeof($nets) > 0) { + \App\IP4Net::insert($nets); + $nets = []; + } + + $bar->finish(); + + $this->info("DONE"); + } + + return 0; + } + + private function countLines($file) + { + $numLines = 0; + + $fh = fopen($file, 'r'); + + while (!feof($fh)) { + $line = trim(fgets($fh)); + + $items = explode('|', $line); + + if (sizeof($items) < 3) { + continue; + } + + if ($items[2] == "ipv4") { + $numLines++; + } + } + + fclose($fh); + + return $numLines; + } + + private function serialFromStatsFile($file) + { + $serial = null; + + $fh = fopen($file, 'r'); + + while (!feof($fh)) { + $line = trim(fgets($fh)); + + $items = explode('|', $line); + + if (sizeof($items) < 2) { + continue; + } + + if ((int)$items[2]) { + $serial = (int)$items[2]; + break; + } + } + + fclose($fh); + + return $serial; + } +} diff --git a/src/app/Console/Commands/Data/Import/IP6NetsCommand.php b/src/app/Console/Commands/Data/Import/IP6NetsCommand.php new file mode 100644 index 00000000..afb0c67a --- /dev/null +++ b/src/app/Console/Commands/Data/Import/IP6NetsCommand.php @@ -0,0 +1,223 @@ + 'http://ftp.afrinic.net/stats/afrinic/delegated-afrinic-latest', + 'apnic' => 'http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest', + 'arin' => 'http://ftp.arin.net/pub/stats/arin/delegated-arin-extended-latest', + 'lacnic' => 'http://ftp.lacnic.net/pub/stats/lacnic/delegated-lacnic-latest', + 'ripencc' => 'https://ftp.ripe.net/ripe/stats/delegated-ripencc-latest' + ]; + + $today = Carbon::now()->toDateString(); + + foreach ($rirs as $rir => $url) { + $file = storage_path("{$rir}-{$today}"); + + \App\Utils::downloadFile($url, $file); + + $serial = $this->serialFromStatsFile($file); + + if (!$serial) { + \Log::error("Can not derive serial from {$file}"); + continue; + } + + $numLines = $this->countLines($file); + + if (!$numLines) { + \Log::error("No relevant lines could be found in {$file}"); + continue; + } + + $bar = \App\Utils::createProgressBar( + $this->output, + $numLines, + "Importing IPv6 Networks from {$file}" + ); + + $fp = fopen($file, 'r'); + + $nets = []; + + while (!feof($fp)) { + $line = trim(fgets($fp)); + + if ($line == "") { + continue; + } + + if ((int)$line) { + continue; + } + + if ($line[0] == "#") { + continue; + } + + $items = explode('|', $line); + + if (sizeof($items) < 7) { + continue; + } + + if ($items[1] == "*") { + continue; + } + + if ($items[2] != "ipv6") { + continue; + } + + if ($items[5] == "00000000") { + $items[5] = "19700102"; + } + + if ($items[1] == "" || $items[1] == "ZZ") { + continue; + } + + $bar->advance(); + + $broadcast = \App\Utils::ip6Broadcast($items[3], (int)$items[4]); + + $net = \App\IP6Net::where( + [ + 'net_number' => $items[3], + 'net_mask' => (int)$items[4], + 'net_broadcast' => $broadcast + ] + )->first(); + + if ($net) { + if ($net->updated_at > Carbon::now()->subDays(1)) { + continue; + } + + // don't use ->update() method because it doesn't update updated_at which we need for expiry + $net->rir_name = $rir; + $net->country = $items[1]; + $net->serial = $serial; + $net->updated_at = Carbon::now(); + + $net->save(); + + continue; + } + + $nets[] = [ + 'rir_name' => $rir, + 'net_number' => $items[3], + 'net_mask' => (int)$items[4], + 'net_broadcast' => $broadcast, + 'country' => $items[1], + 'serial' => $serial, + 'created_at' => Carbon::parse($items[5], 'UTC'), + 'updated_at' => Carbon::now() + ]; + + if (sizeof($nets) >= 100) { + \App\IP6Net::insert($nets); + $nets = []; + } + } + + if (sizeof($nets) > 0) { + \App\IP6Net::insert($nets); + $nets = []; + } + + $bar->finish(); + + $this->info("DONE"); + } + } + + private function countLines($file) + { + $numLines = 0; + + $fh = fopen($file, 'r'); + + while (!feof($fh)) { + $line = trim(fgets($fh)); + + $items = explode('|', $line); + + if (sizeof($items) < 3) { + continue; + } + + if ($items[2] == "ipv6") { + $numLines++; + } + } + + fclose($fh); + + return $numLines; + } + + private function serialFromStatsFile($file) + { + $serial = null; + + $fh = fopen($file, 'r'); + + while (!feof($fh)) { + $line = trim(fgets($fh)); + + $items = explode('|', $line); + + if (sizeof($items) < 2) { + continue; + } + + if ((int)$items[2]) { + $serial = (int)$items[2]; + break; + } + } + + fclose($fh); + + return $serial; + } +} diff --git a/src/app/Console/Commands/Data/ImportCommand.php b/src/app/Console/Commands/Data/ImportCommand.php new file mode 100644 index 00000000..2e9849e9 --- /dev/null +++ b/src/app/Console/Commands/Data/ImportCommand.php @@ -0,0 +1,54 @@ +output = $this->output; + $execution->handle(); + } + + return 0; + } +} diff --git a/src/app/Console/Kernel.php b/src/app/Console/Kernel.php index e9123e07..44ae80b2 100644 --- a/src/app/Console/Kernel.php +++ b/src/app/Console/Kernel.php @@ -1,47 +1,57 @@ command('inspire') - // ->hourly(); + // This command imports countries and the current set of IPv4 and IPv6 networks allocated to countries. + $schedule->command('data:import')->dailyAt('05:00'); + + $schedule->command('wallet:charge')->dailyAt('00:00'); + $schedule->command('wallet:charge')->dailyAt('04:00'); + $schedule->command('wallet:charge')->dailyAt('08:00'); + $schedule->command('wallet:charge')->dailyAt('12:00'); + $schedule->command('wallet:charge')->dailyAt('16:00'); + $schedule->command('wallet:charge')->dailyAt('20:00'); + + // this is a laravel 8-ism + //$schedule->command('wallet:charge')->everyFourHours(); } /** * Register the commands for the application. * * @return void */ protected function commands() { $this->load(__DIR__ . '/Commands'); if (\app('env') != 'production') { $this->load(__DIR__ . '/Development'); } include base_path('routes/console.php'); } } diff --git a/src/app/IP4Net.php b/src/app/IP4Net.php new file mode 100644 index 00000000..1e57ba77 --- /dev/null +++ b/src/app/IP4Net.php @@ -0,0 +1,19 @@ += INET6_ATON(?) + ORDER BY INET6_ATON(net_number), net_mask DESC LIMIT 1 + "; + + $results = DB::select($query, [$ip, $ip]); + + if (sizeof($results) == 0) { + return null; + } + + return \App\IP6Net::find($results[0]->id); + } +} diff --git a/src/app/Observers/UserObserver.php b/src/app/Observers/UserObserver.php index e9c16ae5..62891f82 100644 --- a/src/app/Observers/UserObserver.php +++ b/src/app/Observers/UserObserver.php @@ -1,261 +1,261 @@ id) { while (true) { $allegedly_unique = \App\Utils::uuidInt(); if (!User::find($allegedly_unique)) { $user->{$user->getKeyName()} = $allegedly_unique; break; } } } $user->email = \strtolower($user->email); // only users that are not imported get the benefit of the doubt. $user->status |= User::STATUS_NEW | User::STATUS_ACTIVE; // can't dispatch job here because it'll fail serialization } /** * Handle the "created" event. * * Ensures the user has at least one wallet. * * Should ensure some basic settings are available as well. * * @param \App\User $user The user created. * * @return void */ public function created(User $user) { $settings = [ - 'country' => 'CH', + 'country' => \App\Utils::countryForRequest(), 'currency' => 'CHF', /* 'first_name' => '', 'last_name' => '', 'billing_address' => '', 'organization' => '', 'phone' => '', 'external_email' => '', */ ]; foreach ($settings as $key => $value) { $settings[$key] = [ 'key' => $key, 'value' => $value, 'user_id' => $user->id, ]; } // Note: Don't use setSettings() here to bypass UserSetting observers // Note: This is a single multi-insert query $user->settings()->insert(array_values($settings)); $user->wallets()->create(); // Create user record in LDAP, then check if the account is created in IMAP $chain = [ new \App\Jobs\User\VerifyJob($user->id), ]; \App\Jobs\User\CreateJob::withChain($chain)->dispatch($user->id); } /** * Handle the "deleted" event. * * @param \App\User $user The user deleted. * * @return void */ public function deleted(User $user) { // } /** * Handle the "deleting" event. * * @param User $user The user that is being deleted. * * @return void */ public function deleting(User $user) { if ($user->isForceDeleting()) { $this->forceDeleting($user); return; } // TODO: Especially in tests we're doing delete() on a already deleted user. // Should we escape here - for performance reasons? // TODO: I think all of this should use database transactions // Entitlements do not have referential integrity on the entitled object, so this is our // way of doing an onDelete('cascade') without the foreign key. $entitlements = Entitlement::where('entitleable_id', $user->id) ->where('entitleable_type', User::class)->get(); foreach ($entitlements as $entitlement) { $entitlement->delete(); } // Remove owned users/domains $wallets = $user->wallets()->pluck('id')->all(); $assignments = Entitlement::whereIn('wallet_id', $wallets)->get(); $users = []; $domains = []; $entitlements = []; foreach ($assignments as $entitlement) { if ($entitlement->entitleable_type == Domain::class) { $domains[] = $entitlement->entitleable_id; } elseif ($entitlement->entitleable_type == User::class && $entitlement->entitleable_id != $user->id) { $users[] = $entitlement->entitleable_id; } else { $entitlements[] = $entitlement->id; } } $users = array_unique($users); $domains = array_unique($domains); // Domains/users/entitlements need to be deleted one by one to make sure // events are fired and observers can do the proper cleanup. if (!empty($users)) { foreach (User::whereIn('id', $users)->get() as $_user) { $_user->delete(); } } if (!empty($domains)) { foreach (Domain::whereIn('id', $domains)->get() as $_domain) { $_domain->delete(); } } if (!empty($entitlements)) { Entitlement::whereIn('id', $entitlements)->delete(); } // FIXME: What do we do with user wallets? \App\Jobs\User\DeleteJob::dispatch($user->id); } /** * Handle the "deleting" event on forceDelete() call. * * @param User $user The user that is being deleted. * * @return void */ public function forceDeleting(User $user) { // TODO: We assume that at this moment all belongings are already soft-deleted. // Remove owned users/domains $wallets = $user->wallets()->pluck('id')->all(); $assignments = Entitlement::withTrashed()->whereIn('wallet_id', $wallets)->get(); $entitlements = []; $domains = []; $users = []; foreach ($assignments as $entitlement) { $entitlements[] = $entitlement->id; if ($entitlement->entitleable_type == Domain::class) { $domains[] = $entitlement->entitleable_id; } elseif ( $entitlement->entitleable_type == User::class && $entitlement->entitleable_id != $user->id ) { $users[] = $entitlement->entitleable_id; } } $users = array_unique($users); $domains = array_unique($domains); // Remove the user "direct" entitlements explicitely, if they belong to another // user's wallet they will not be removed by the wallets foreign key cascade Entitlement::withTrashed() ->where('entitleable_id', $user->id) ->where('entitleable_type', User::class) ->forceDelete(); // Users need to be deleted one by one to make sure observers can do the proper cleanup. if (!empty($users)) { foreach (User::withTrashed()->whereIn('id', $users)->get() as $_user) { $_user->forceDelete(); } } // Domains can be just removed if (!empty($domains)) { Domain::withTrashed()->whereIn('id', $domains)->forceDelete(); } // Remove transactions, they also have no foreign key constraint Transaction::where('object_type', Entitlement::class) ->whereIn('object_id', $entitlements) ->delete(); Transaction::where('object_type', Wallet::class) ->whereIn('object_id', $wallets) ->delete(); } /** * Handle the "retrieving" event. * * @param User $user The user that is being retrieved. * * @todo This is useful for audit. * * @return void */ public function retrieving(User $user) { // TODO \App\Jobs\User\ReadJob::dispatch($user->id); } /** * Handle the "updating" event. * * @param User $user The user that is being updated. * * @return void */ public function updating(User $user) { \App\Jobs\User\UpdateJob::dispatch($user->id); } } diff --git a/src/app/Utils.php b/src/app/Utils.php index 1f48f22b..281fe7c8 100644 --- a/src/app/Utils.php +++ b/src/app/Utils.php @@ -1,152 +1,338 @@ = INET_ATON(?) + ORDER BY INET_ATON(net_number), net_mask DESC LIMIT 1 + "; + } else { + $query = " + SELECT id FROM ip6nets + WHERE INET6_ATON(net_number) <= INET6_ATON(?) + AND INET6_ATON(net_broadcast) >= INET6_ATON(?) + ORDER BY INET6_ATON(net_number), net_mask DESC LIMIT 1 + "; + } + + $nets = \Illuminate\Support\Facades\DB::select($query, [$ip, $ip]); + + if (sizeof($nets) > 0) { + return $nets[0]->country; + } + + return 'CH'; + } + + /** + * Return the country ISO code for the current request. + */ + public static function countryForRequest() + { + $request = \request(); + $ip = $request->ip(); + + return self::countryForIP($ip); + } + + /** + * Shortcut to creating a progress bar of a particular format with a particular message. + * + * @param \Illuminate\Console\OutputStyle $output Console output object + * @param int $count Number of progress steps + * @param string $message The description + * + * @return \Symfony\Component\Console\Helper\ProgressBar + */ + public static function createProgressBar($output, $count, $message = null) + { + $bar = $output->createProgressBar($count); + + $bar->setFormat( + '%current:7s%/%max:7s% [%bar%] %percent:3s%% %elapsed:7s%/%estimated:-7s% %message% ' + ); + + if ($message) { + $bar->setMessage($message . " ..."); + } + + $bar->start(); + + return $bar; + } + /** * Return the number of days in the month prior to this one. * * @return int */ public static function daysInLastMonth() { $start = new Carbon('first day of last month'); $end = new Carbon('last day of last month'); return $start->diffInDays($end) + 1; } + /** + * Download a file from the interwebz and store it locally. + * + * @param string $source The source location + * @param string $target The target location + * @param bool $force Force the download (and overwrite target) + * + * @return void + */ + public static function downloadFile($source, $target, $force = false) + { + if (is_file($target) && !$force) { + return; + } + + \Log::info("Retrieving {$source}"); + + $fp = fopen($target, 'w'); + + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $source); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_FILE, $fp); + curl_exec($curl); + + if (curl_errno($curl)) { + \Log::error("Request error on {$source}: " . curl_error($curl)); + + curl_close($curl); + fclose($fp); + + unlink($target); + return; + } + + curl_close($curl); + fclose($fp); + } + + /** + * Calculate the broadcast address provided a net number and a prefix. + * + * @param string $net A valid IPv6 network number. + * @param int $prefix The network prefix. + * + * @return string + */ + public static function ip6Broadcast($net, $prefix) + { + $netHex = bin2hex(inet_pton($net)); + + // Overwriting first address string to make sure notation is optimal + $net = inet_ntop(hex2bin($netHex)); + + // Calculate the number of 'flexible' bits + $flexbits = 128 - $prefix; + + // Build the hexadecimal string of the last address + $lastAddrHex = $netHex; + + // We start at the end of the string (which is always 32 characters long) + $pos = 31; + while ($flexbits > 0) { + // Get the character at this position + $orig = substr($lastAddrHex, $pos, 1); + + // Convert it to an integer + $origval = hexdec($orig); + + // OR it with (2^flexbits)-1, with flexbits limited to 4 at a time + $newval = $origval | (pow(2, min(4, $flexbits)) - 1); + + // Convert it back to a hexadecimal character + $new = dechex($newval); + + // And put that character back in the string + $lastAddrHex = substr_replace($lastAddrHex, $new, $pos, 1); + + // We processed one nibble, move to previous position + $flexbits -= 4; + $pos -= 1; + } + + // Convert the hexadecimal string to a binary string + # Using pack() here + # Newer PHP version can use hex2bin() + $lastaddrbin = pack('H*', $lastAddrHex); + + // And create an IPv6 address from the binary string + $lastaddrstr = inet_ntop($lastaddrbin); + + return $lastaddrstr; + } + /** * Provide all unique combinations of elements in $input, with order and duplicates irrelevant. * * @param array $input The input array of elements. * * @return array[] */ public static function powerSet(array $input): array { $output = []; for ($x = 0; $x < count($input); $x++) { self::combine($input, $x + 1, 0, [], 0, $output); } return $output; } /** * Returns the current user's email address or null. * * @return string */ public static function userEmailOrNull(): ?string { $user = Auth::user(); if (!$user) { return null; } return $user->email; } /** * Returns a UUID in the form of an integer. * * @return integer */ public static function uuidInt(): int { $hex = Uuid::uuid4(); $bin = pack('h*', str_replace('-', '', $hex)); $ids = unpack('L', $bin); $id = array_shift($ids); return $id; } /** * Returns a UUID in the form of a string. * * @return string */ public static function uuidStr(): string { return Uuid::uuid4()->toString(); } private static function combine($input, $r, $index, $data, $i, &$output): void { $n = count($input); // Current cobination is ready if ($index == $r) { $output[] = array_slice($data, 0, $r); return; } // When no more elements are there to put in data[] if ($i >= $n) { return; } // current is included, put next at next location $data[$index] = $input[$i]; self::combine($input, $r, $index + 1, $data, $i + 1, $output); // current is excluded, replace it with next (Note that i+1 // is passed, but index is not changed) self::combine($input, $r, $index, $data, $i + 1, $output); } /** * Create self URL * * @param string $route Route/Path * * @return string Full URL */ public static function serviceUrl(string $route): string { $url = \config('app.public_url'); if (!$url) { $url = \config('app.url'); } return rtrim(trim($url, '/') . '/' . ltrim($route, '/'), '/'); } /** * Create a configuration/environment data to be passed to * the UI * * @todo For a lack of better place this is put here for now * * @return array Configuration data */ public static function uiEnv(): array { $opts = ['app.name', 'app.url', 'app.domain']; $env = \app('config')->getMany($opts); $countries = include resource_path('countries.php'); $env['countries'] = $countries ?: []; $isAdmin = strpos(request()->getHttpHost(), 'admin.') === 0; $env['jsapp'] = $isAdmin ? 'admin.js' : 'user.js'; $env['paymentProvider'] = \config('services.payment_provider'); $env['stripePK'] = \config('services.stripe.public_key'); return $env; } } diff --git a/src/composer.json b/src/composer.json index 7e9c78b7..f719d82f 100644 --- a/src/composer.json +++ b/src/composer.json @@ -1,89 +1,85 @@ { "name": "laravel/laravel", "type": "project", "description": "The Laravel Framework.", "keywords": [ "framework", "laravel" ], "license": "MIT", "repositories": [ { "type": "vcs", "url": "https://git.kolab.org/diffusion/PNL/php-net_ldap3.git" } ], "require": { "php": "^7.1.3", "barryvdh/laravel-dompdf": "^0.8.6", - "doctrine/dbal": "^2.9", + "dyrynda/laravel-nullable-fields": "*", "fideloper/proxy": "^4.0", - "geoip2/geoip2": "^2.9", - "iatstuti/laravel-nullable-fields": "*", "kolab/net_ldap3": "dev-master", "laravel/framework": "6.*", "laravel/tinker": "^2.4", "mollie/laravel-mollie": "^2.9", "morrislaptop/laravel-queue-clear": "^1.2", - "silviolleite/laravelpwa": "^1.0", + "silviolleite/laravelpwa": "^2.0", "spatie/laravel-translatable": "^4.2", "spomky-labs/otphp": "~4.0.0", "stripe/stripe-php": "^7.29", "swooletw/laravel-swoole": "^2.6", - "torann/currency": "^1.0", - "torann/geoip": "^1.0", "tymon/jwt-auth": "^1.0" }, "require-dev": { "beyondcode/laravel-dump-server": "^1.0", "beyondcode/laravel-er-diagram-generator": "^1.3", "code-lts/doctum": "^5.1", "filp/whoops": "^2.0", "fzaninotto/faker": "^1.4", "kirschbaum-development/mail-intercept": "^0.2.4", "laravel/dusk": "~5.11.0", "mockery/mockery": "^1.0", "nunomaduro/larastan": "^0.6", "phpstan/phpstan": "^0.12", "phpunit/phpunit": "^8" }, "config": { "optimize-autoloader": true, "preferred-install": "dist", "sort-packages": true }, "extra": { "laravel": { "dont-discover": [] } }, "autoload": { "psr-4": { "App\\": "app/" }, "classmap": [ "database/seeds", "database/factories", "include" ] }, "autoload-dev": { "psr-4": { "Tests\\": "tests/" } }, "minimum-stability": "dev", "prefer-stable": true, "scripts": { "post-autoload-dump": [ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "@php artisan package:discover --ansi" ], "post-root-package-install": [ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" ], "post-create-project-cmd": [ "@php artisan key:generate --ansi" ] } } diff --git a/src/config/geoip.php b/src/config/geoip.php deleted file mode 100644 index c47880ad..00000000 --- a/src/config/geoip.php +++ /dev/null @@ -1,165 +0,0 @@ - true, - - /* - |-------------------------------------------------------------------------- - | Include Currency in Results - |-------------------------------------------------------------------------- - | - | When enabled the system will do it's best in deciding the user's currency - | by matching their ISO code to a preset list of currencies. - | - */ - - 'include_currency' => true, - - /* - |-------------------------------------------------------------------------- - | Default Service - |-------------------------------------------------------------------------- - | - | Here you may specify the default storage driver that should be used - | by the framework. - | - | Supported: "maxmind_database", "maxmind_api", "ipapi" - | - */ - - 'service' => 'maxmind_database', - - /* - |-------------------------------------------------------------------------- - | Storage Specific Configuration - |-------------------------------------------------------------------------- - | - | Here you may configure as many storage drivers as you wish. - | - */ - - 'services' => [ - - 'maxmind_database' => [ - 'class' => \Torann\GeoIP\Services\MaxMindDatabase::class, - 'database_path' => storage_path('app/geoip.mmdb'), - 'update_url' => 'https://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz', - 'locales' => ['en'], - ], - - 'maxmind_api' => [ - 'class' => \Torann\GeoIP\Services\MaxMindWebService::class, - 'user_id' => env('MAXMIND_USER_ID'), - 'license_key' => env('MAXMIND_LICENSE_KEY'), - 'locales' => ['en'], - ], - - 'ipapi' => [ - 'class' => \Torann\GeoIP\Services\IPApi::class, - 'secure' => true, - 'key' => env('IPAPI_KEY'), - 'continent_path' => storage_path('app/continents.json'), - 'lang' => 'en', - ], - - 'ipgeolocation' => [ - 'class' => \Torann\GeoIP\Services\IPGeoLocation::class, - 'secure' => true, - 'key' => env('IPGEOLOCATION_KEY'), - 'continent_path' => storage_path('app/continents.json'), - 'lang' => 'en', - ], - - 'ipdata' => [ - 'class' => \Torann\GeoIP\Services\IPData::class, - 'key' => env('IPDATA_API_KEY'), - 'secure' => true, - ], - - 'ipfinder' => [ - 'class' => \Torann\GeoIP\Services\IPFinder::class, - 'key' => env('IPFINDER_API_KEY'), - 'secure' => true, - 'locales' => ['en'], - ], - - ], - - /* - |-------------------------------------------------------------------------- - | Default Cache Driver - |-------------------------------------------------------------------------- - | - | Here you may specify the type of caching that should be used - | by the package. - | - | Options: - | - | all - All location are cached - | some - Cache only the requesting user - | none - Disable cached - | - */ - - 'cache' => 'all', - - /* - |-------------------------------------------------------------------------- - | Cache Tags - |-------------------------------------------------------------------------- - | - | Cache tags are not supported when using the file or database cache - | drivers in Laravel. This is done so that only locations can be cleared. - | - */ - - 'cache_tags' => false, - - /* - |-------------------------------------------------------------------------- - | Cache Expiration - |-------------------------------------------------------------------------- - | - | Define how long cached location are valid. - | - */ - - 'cache_expires' => 30, - - /* - |-------------------------------------------------------------------------- - | Default Location - |-------------------------------------------------------------------------- - | - | Return when a location is not found. - | - */ - - 'default_location' => [ - 'ip' => '127.0.0.0', - 'iso_code' => 'CH', - 'country' => 'Switzerland', - 'city' => 'Zurich', - 'state' => 'ZH', - 'state_name' => 'Zurich', - 'postal_code' => '8703', - 'lat' => 47.30, - 'lon' => 8.59, - 'timezone' => 'Europe/Zurich', - 'continent' => 'EU', - 'default' => true, - 'currency' => 'CHF', - ], - -]; diff --git a/src/database/migrations/2020_06_04_140800_create_ip4nets_table.php b/src/database/migrations/2020_06_04_140800_create_ip4nets_table.php new file mode 100644 index 00000000..bfd3fb20 --- /dev/null +++ b/src/database/migrations/2020_06_04_140800_create_ip4nets_table.php @@ -0,0 +1,43 @@ +bigIncrements('id'); + $table->string('rir_name', 8); + $table->string('net_number', 15)->index(); + $table->tinyInteger('net_mask')->unsigned(); + $table->string('net_broadcast', 15)->index(); + $table->string('country', 2)->nullable(); + $table->bigInteger('serial')->unsigned(); + $table->timestamps(); + + $table->index(['net_number', 'net_mask', 'net_broadcast']); + } + ); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('ip4nets'); + } +} diff --git a/src/database/migrations/2020_06_04_140800_create_ip6nets_table.php b/src/database/migrations/2020_06_04_140800_create_ip6nets_table.php new file mode 100644 index 00000000..91d9531f --- /dev/null +++ b/src/database/migrations/2020_06_04_140800_create_ip6nets_table.php @@ -0,0 +1,43 @@ +bigIncrements('id'); + $table->string('rir_name', 8); + $table->string('net_number', 39)->index(); + $table->tinyInteger('net_mask')->unsigned(); + $table->string('net_broadcast', 39)->index(); + $table->string('country', 2)->nullable(); + $table->bigInteger('serial')->unsigned(); + $table->timestamps(); + + $table->index(['net_number', 'net_mask', 'net_broadcast']); + } + ); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('ip6nets'); + } +}