<?php

namespace App\Helpers\Utility;

use Carbon\Carbon;
use Symfony\Component\Process\Process;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Exception;

class WhoIs {
  
  /**
  * Exists to ensure code is nice and DRY - the system should select from a pool of randoming forwarding agents to ensure we never hit a rate-limit, and so if one fails the others work.
  */
  private static function random_source_query($_path, $_params) {
    
    // We use random sources to prevent burning anything via a rate-limit
    $sources = config('creatia.endpoint.whois_forward');
    
    // Keep track of sources to ignore
    $ignore_sources = [];
    
    for($i = 0; $i < 1; ++$i) {
      $source = $sources[random_int(0, sizeof($sources)-1)];
      
      for($j = 0; $j < sizeof($sources); ++$j) {
        if(in_array($source, $ignore_sources)) {
          $source = $sources[random_int(0, sizeof($sources)-1)];
        }
        else {
          break;
        } 
      }
      
      try {        
        return Http::timeout(10)->get("http://$source/$_path", $_params)->json();
        break;
        
      } catch(Exception $ex) {
        Log::warning("WHOIS WARNING - We failed to use: $source");
        $ignore_sources[] = $source;
      }
    }
    
    Log::error("WHOIS ERROR - We failed to use any source");
    
    return false;
  }
  
  /**
  * Returns a list of emails for a given ASN that may be associated with it.
  */
  static function asn_emails($_asn) {
    if(preg_match('/(?:ASN|AS)*(\d+)?/i', $_asn)) {
      
      $result = self::random_source_query('asn', [ 'asn' => $_asn ]);
      
      if($result !== false && isset($result['result'])) {
        $emails = [];
        
        preg_match_all('/((?!\'|\")[\S.]+@[\S.]+(?:\.\w+)+)/m', $result['result'], $emails);
        
        // Registries and IRR operators whose own contact addresses appear in
        // whois output. Matched against the email domain, subdomains included.
        $irr_domains = [
          // RIRs
          'arin.net', 'ripe.net', 'apnic.net', 'lacnic.net', 'afrinic.net',
          // NIRs
          'apjii.or.id', 'idnic.id', 'jpnic.net', 'nic.ad.jp', 'krnic.net',
          'krnic.or.kr', 'kisa.or.kr', 'twnic.net.tw', 'twnic.tw', 'cnnic.cn',
          'cnnic.net.cn', 'vnnic.vn', 'irinn.in', 'nixi.in', 'registro.br',
          'nic.br', 'cgi.br', 'nic.mx',
          // Stand-alone IRRs
          'radb.net', 'merit.edu', 'altdb.net', 'rgnet.com',
          'nestegg.net', 'bboi.net', 'canarie.ca',
          // IANA/ICANN boilerplate
          'iana.org', 'icann.org',
        ];
        
        $filtered = [];
        
        foreach($emails[1] as $email) {
          // The capture group swallows adjacent punctuation, so <foo@ripe.net>
          // arrives with the brackets attached.
          $email = trim($email, "<>(),;:.\"'");
          
          if($email === '') {
            continue;
          }
          
          $at = strrpos($email, '@');
          
          if($at === false) {
            continue;
          }
          
          $domain = strtolower(substr($email, $at + 1));
          $is_irr = false;
          
          foreach($irr_domains as $irr_domain) {
            if($domain === $irr_domain || substr($domain, -(strlen($irr_domain) + 1)) === '.' . $irr_domain) {
              $is_irr = true;
              break;
            }
          }
          
          if(!$is_irr) {
            $filtered[] = $email;
          }
        }
        
        return array_unique($filtered);
      }
    }
    
    return false;
  }
  
  /**
  * Returns the registration date for a given ASN.
  * @return Carbon|false Registration date or false if not found/invalid
  */
  static function asn_registration_year($_asn) {
    if(preg_match('/(?:ASN|AS)*(\d+)?/i', $_asn)) {
      
      $result = self::random_source_query('asn', [ 'asn' => $_asn ]);
      
      if($result !== false && isset($result['result'])) {
        // Patterns for different RIR date formats
        $patterns = [
          // ARIN format: RegDate: 1988-10-21
          '/RegDate:\s*(\d{4}-\d{2}-\d{2})/i',
          
          // RIPE/AFRINIC format: created: 2002-08-21T16:00:00Z
          '/created:\s*(\d{4}-\d{2}-\d{2})(?:T[\d:]+Z?)?/i',
          
          // LACNIC format: created: 20120524
          '/created:\s*(\d{8})(?:\s|$)/i',
          
          // Alternative format sometimes seen
          '/(?:reg-date|registered):\s*(\d{4}-\d{2}-\d{2})/i'
        ];
        
        foreach ($patterns as $pattern) {
          if (preg_match($pattern, $result['result'], $matches)) {
            $dateStr = $matches[1];
            
            // Convert LACNIC format YYYYMMDD to YYYY-MM-DD
            if (preg_match('/^\d{8}$/', $dateStr)) {
              $dateStr = substr($dateStr, 0, 4) . '-' . substr($dateStr, 4, 2) . '-' . substr($dateStr, 6, 2);
            }
            
            try {
              return Carbon::parse($dateStr)->format('Y');
            } catch (Exception $e) {
              // Invalid date format, continue to next pattern
              continue;
            }
          }
        }

        if (preg_match('/source:\s*(APNIC|AFRINIC)/i', $result['result']) || preg_match('/\# ENGLISH\n\nKRNIC/i', $result['result'])) {

          Log::info("The ASN AS$_asn is associated with APNIC or another weird registry and thus lacks any registration date.");

          return Carbon::now()->format('Y');

        }
      }
    }
    
    return false;
  }
  
  /**
  * Returns whether a hierarchical AS-SET exists 
  */
  static function as_set_exists($_as_set) {
    if(preg_match('/(?:AS\d+)\:(?:AS(?:\w|-)+)/i', $_as_set)) {
      
      $result = self::random_source_query('as_set', [ 'as_set' => $_as_set ]);
      
      if($result !== false && isset($result['result'])) {
        
        return preg_match('/^as-set:\s*AS\d+:AS-[A-Z0-9][A-Z0-9-]*[A-Z0-9]$/i', $result['result']) !== false;
      }
    }
    
    return false;
  }
  
  static function location_lookup($_ip) {
    $command = new Process(['whois', '-a', $_ip]);
    
    $command->run();
    
    if($command->isSuccessful()) {
      $country = null;
      
      $output = $command->getOutput();
      
      if(str_contains($output, 'No no entries found')) {
        return 'NO_ENTRY';
      }
      
      preg_match_all('/(?:country:\s+)([A-z]+)/', $output, $country);
      
      
      return isset($country[1]) ? $country[1] : 'NO_COUNTRY';
    }
    
    return 'NO_COUNTRY';
  }
  
}

?>