#!/usr/bin/perl -w
#===============================================================================
#
#         FILE:  sentrypeer
#
#        USAGE:  ./sentrypeer
#
#  DESCRIPTION:
#
#      OPTIONS:  ---
# REQUIREMENTS:  ---
#         BUGS:  ---
#        NOTES:  ---
#       AUTHOR:  Gavin Henry (GH), <ghenry@sentrypeer.org>
#      COMPANY:  .
#      VERSION:  1.0
#      CREATED:  19/08/21 19:32:20 BST
#     REVISION:  ---
#
#
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only
# Copyright (c) 2021 - 2025 Gavin Henry <ghenry@sentrypeer.org>
#   _____            _              _____
#  / ____|          | |            |  __ \
# | (___   ___ _ __ | |_ _ __ _   _| |__) |__  ___ _ __
#  \___ \ / _ \ '_ \| __| '__| | | |  ___/ _ \/ _ \ '__|
#  ____) |  __/ | | | |_| |  | |_| | |  |  __/  __/ |
# |_____/ \___|_| |_|\__|_|   \__, |_|   \___|\___|_|
#                              __/ |
#                             |___/
#
#===============================================================================

use strict;
use warnings;
use utf8;
use feature ':5.16';

use DBI;
use Net::SIP;
use Net::SIP::Util qw/ sip_uri2sockinfo /;

#use Net::SIP::Debug '2';
use Getopt::Long;
use Config::Tiny;
use Time::HiRes qw/ gettimeofday /;
use Mojolicious::Lite -signatures;

GetOptions(
    q{debug}   => \my $debug,
    q{verbose} => \my $verbose
);

my $dbfile      = qq{$0.db};
my $config_file = qq{$0.conf};

my $config = Config::Tiny->read( $config_file, 'utf8' )
  or die "Can't open $config_file\n";

# TODO: Cleanup
$SIG{'INT'}  = \&sig_handler;
$SIG{'QUIT'} = \&sig_handler;

# TODO: Clean slate - maybe call devmode so we don't clean up valuable honey data
if ($debug) {
    unlink $dbfile          if -e $dbfile;
    say qq{Deleted $dbfile} if $debug;
}

# This creates $dbfile
my $dbh = DBI->connect(
    qq{dbi:SQLite:dbname=$dbfile},
    q{}, q{},
    {
        AutoCommit     => 1,
        RaiseError     => 1,
        sqlite_unicode => 1,
    }
);

# TODO: Use this for WebSocket pushes and BGP announcements
# https://www.sqlite.org/c3ref/update_hook.html
# https://metacpan.org/pod/DBD::SQLite#$dbh-%3Esqlite_update_hook(-$code_ref-)
if ($debug) {
    my $print_sqlite_event_ref = sub {
        my ( $action_code, $database, $table, $rowid ) = @_;
        say "=== Event from SQLite has run a callback:";
        say "Action code: $action_code";
        say "Database: $database";
        say "Table: $table";
        say "Row ID: $rowid";
    };
    $dbh->sqlite_update_hook($print_sqlite_event_ref);
}

# TODO: Add IP address (maxmind geoip) country origination and AS Number
# Schema - open for change at any stage during prototype phase
my $schema = <<'SCHEMA';
CREATE TABLE IF NOT EXISTS honey (
   honey_id INTEGER PRIMARY KEY,
   event_timestamp TEXT,
   source_ip TEXT,
   called_number TEXT,
   sip_method TEXT,
   sip_uri TEXT,
   sip_via TEXT,
   sip_contact TEXT,
   sip_user_agent TEXT
);
SCHEMA

$dbh->do($schema);
say qq{$dbfile created and SCHEMA populated} if $debug;

# TODO: Find our IP out automatically
my $sip_listen_ip_address = $config->{main}->{sip_listen_ip_address};
my $sip_listen_port       = $config->{main}->{sip_listen_port};
my $ua =
  Net::SIP::Simple->new( leg => qq{$sip_listen_ip_address:$sip_listen_port} );

# TODO: Refactor
my $save_details = sub {
    my $sip_packet = shift;

    # Methods we don't want to save
    # TODO: Are ACK and BYE used to sniff out responses like in the TCP world?
    return 1
      if grep { $_ eq $sip_packet->method() }
      qw/ SUBSCRIBE PUBLISH PRESENCE ACK BYE /;

    # TODO: Maybe convert to DT with a TZ
    my $event_timestamp = gettimeofday();
    say qq{Event datetime is: $event_timestamp} if $debug;

    my ( $proto, $host, $port, $family ) =
      sip_uri2sockinfo( $sip_packet->get_header('contact') );

# A bit from https://stackoverflow.com/a/55337094/1072411
# TODO: Do we want to save anything other than digits here for analysis reasons?
    my ($called_number) = $sip_packet->uri() =~ m/^sips?:\+?(\d+)\@/g;
    $called_number //= q{not_found};

    if ($debug) {

        say '======= Source IP/From:';
        say $host;

        say '======= Called Number:';
        say $called_number;

        say '======= Method:';
        say $sip_packet->method();

        say '======= URI:';
        say $sip_packet->uri();

        say '======= Via:';
        say $sip_packet->get_header('via');

        say '======= Contact:';
        say $sip_packet->get_header('contact');

        say '======= User-Agent:';
        say $sip_packet->get_header('user-agent');
    }

    my $sth = $dbh->prepare(
        q{
        INSERT INTO honey 
        (event_timestamp, source_ip, called_number, sip_method, sip_uri, sip_via, sip_contact, sip_user_agent) 
        VALUES(?, ?, ?, ?, ?, ?, ?, ?);
      }
    );
    say q{Prepared INSERT} if $debug;

    $sth->execute(
        $event_timestamp,
        $host,
        $called_number,
        $sip_packet->method(),
        $sip_packet->uri(),
        $sip_packet->get_header('via'),
        $sip_packet->get_header('contact'),
        $sip_packet->get_header('user-agent')
    );
    say q{INSERTed data} if $debug;

    return 1;
};

# https://github.com/noxxi/p5-net-sip/issues/49#issuecomment-902025333
# Creates a https://metacpan.org/dist/Net-SIP/view/lib/Net/SIP/ReceiveChain.pod
$ua->create_chain(
    [ $ua->create_registrar, $ua->listen ],
    (
        filter => $save_details
    )
);

# TODO: Check DBD::SQLite does re fork()
# local $SIG{CHLD} = "IGNORE";
my $pid = fork();
die "Failed to fork: $!" unless defined $pid;

# TODO: This is running in a child process - best way or move to another program. Will be easier in non-prototype
# as limitation here is the event loops of Mojo and Net::SIP. We should do our own and put these in them?
# Mojolicious
if ( $pid == 0 ) {
    say "Hi from the Mojo child process." if $debug;

    # https://metacpan.org/pod/DBD::SQLite#DBD::SQLite-and-fork()
    my $dbh = DBI->connect(
        qq{dbi:SQLite:dbname=$dbfile},
        q{}, q{},
        {
            AutoCommit     => 1,
            RaiseError     => 1,
            sqlite_unicode => 1,
        }
    );

    get '/' => sub ($c) {

        # TODO: Add bind_columns for json payload on API
        my $honey_data = $dbh->selectall_arrayref(q{SELECT * FROM honey});

 # https://docs.mojolicious.org/Mojolicious/Guides/Rendering#Content-negotiation
        $c->respond_to(
            json => { json     => { honey_data => $honey_data } },
            html => { template => 'sentrypeer', honey_data => $honey_data },
            any  => { text     => '',           status     => 204 }
        );
    };

    app->start( 'daemon', '-l', 'http://*:8090' );
}

if ( $pid != 0 ) {
    if ( $verbose or $debug ) {
        say 'Listening...';
    }
    $ua->loop;
}

sub sig_handler {    # 1st argument is signal name
    my ($sig) = @_;
    say "Caught a SIG$sig. Closing $dbfile" if $debug;
    $dbh->disconnect;
    say "Waiting for child process to exit: $pid" if $debug;
    waitpid $pid, 0;
    exit(0);
}

# TODO: Move to standalone files in ./templates when needed
#
__DATA__

@@ sentrypeer.html.ep
<!DOCTYPE html>
<!-- 
   _____            _              _____               
  / ____|          | |            |  __ \              
 | (___   ___ _ __ | |_ _ __ _   _| |__) |__  ___ _ __ 
  \___ \ / _ \ '_ \| __| '__| | | |  ___/ _ \/ _ \ '__|
  ____) |  __/ | | | |_| |  | |_| | |  |  __/  __/ |   
 |_____/ \___|_| |_|\__|_|   \__, |_|   \___|\___|_|   
                              __/ |                    
                             |___/                                                                  
-->
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="Log in to SentryPeer.">
    <meta name="author" content="Gavin Henry (c) 2021 - 2025">
    <title>SentryPeer</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
  </head>
  <body>
    <table class="table table-bordered">
      <thead>
        <tr>
          <th scope="col">Event ID</th>
          <th scope="col">Timestamp</th>
          <th scope="col">Source IP</th>
          <th scope="col">Called Number</th>
          <th scope="col">SIP Method</th>
          <th scope="col">SIP URI</th>
          <th scope="col">SIP Via</th>
          <th scope="col">SIP Contact</th>
          <th scope="col">SIP User-Agent</th>
        </tr>
      </thead>
      <tbody>
      % my $i = 8;
      % for my $sip (@{$honey_data}) {
        <tr>
          % for my $detail (0 .. $i) {
          <td>
            %= $sip->[$detail]
          </td>
          % }
        </tr>
      % }    
      </tbody>
    </table>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>  
  </body>
</html>
