package Net::Shadowsocks::Client;

    # ABSTRACT: Shadowsocks protocol client module.
    use 5.006;
    use strict;
    use warnings;
    use AnyEvent;
    use AnyEvent::Handle;
    use AnyEvent::Socket;
    use Carp;
    use Config;
    use Digest::MD5;
    use Digest::SHA;
    use IO::Socket::Socks qw(:constants $SOCKS_ERROR ESOCKSPROTO);
    use Net::Shadowsocks;
    use Net::Shadowsocks qw(_EVP_BytesToKey _initialize_cipher);
    use Socket qw(IPPROTO_TCP MSG_FASTOPEN);


our $VERSION = '0.7.0';


    sub new($$$$$$) 
    {
        my $_osname = $Config{osname};
        my $_osvers = $Config{osvers};
        AE::log info => "Shadowsocks server starting up on $_osname $_osvers";
        my ( $class, %args ) = @_;
        my $self = bless {
            map { ( $_ => $args{$_} ) }
              qw(local_address local_port password server server_port method),
        }, $class;
        if ( $self->{local_address} eq 'localhost' ) 
        {
            undef $self->{local_address};
        }

        my $tcp_server;
        $tcp_server = AnyEvent::Socket::tcp_server(
            $self->{local_address},
            $self->{local_port},
            sub 
            {
                my ( $client_socket, $client_host, $client_port ) = @_;

                AE::log info => "Got new client connection: $client_host:$client_port";
                my $addr_to_send = '';
                my $server_iv;
                my $server_nonce;
                my $stage = 0;
                my $mode  = 0;
                my ( $encryptor, $decryptor, $key, $iv, $nonce ) = Net::Shadowsocks::_initialize_cipher( $self->{method},$self->{password} );

                my $clienthandler;
                my $remotehandler;
                $clienthandler = AnyEvent::Handle->new
                (
                    autocork  => 1,
                    keepalive => 1,
                    no_delay  => 1,
                    fh        => $client_socket,
                    on_eof    => sub 
                    {
                        my $chandler = shift;
                        AE::log info => "Client: Done.";
                        $chandler->destroy();
                    },
                    on_error => sub 
                    {
                        my ( $chandler, $fatal, $msg ) = @_;
                        AE::log error => $msg;
                        $chandler->destroy();
                    },
                    on_read => sub 
                    {
                        my $client_buffer = $clienthandler->rbuf();

                        if ( $client_buffer eq '' ) 
                        {
                            return;
                        }
                        else 
                        {
                            if ( $stage == 0 ) 
                            {
                                my $ver = ord( substr( $client_buffer, 0, 1 ) );

                                if ( $ver != SOCKS5_VER )
                                 {
                                    $! = ESOCKSPROTO;
                                    $SOCKS_ERROR->set( ISS_BAD_VERSION, $@ ="Socks version should be 5, $ver recieved");
                                    return;
                                }
                                my $nmethods = ord( substr( $client_buffer, 1, 1 ) );

                                if ( $nmethods == 0 ) 
                                {
                                    $! = ESOCKSPROTO;
                                    $SOCKS_ERROR->set( AUTHMECH_INVALID, $@ = "No auth methods sent" );
                                    return;
                                }
                                my @methods = unpack( 'C' x $nmethods, substr( $client_buffer, 2 ) );

                                $clienthandler->push_write(pack( 'CC', SOCKS5_VER, 0 ) );
                                $stage = 1;

                            }
                            elsif ( $stage == 1 ) 
                            {
                                my $cmd = ord( substr( $client_buffer, 1, 1 ) );

                                if ( $cmd == CMD_CONNECT ) 
                                {
                                    my $addrtype = ord( substr( $client_buffer, 3, 1 ) );
                                    if (    $addrtype != 1 and $addrtype != 3 and $addrtype != 4 )
                                    {
                                        carp 'addrtype not supported';
                                        return;
                                    }
                                    $addr_to_send = substr( $client_buffer, 3 );

                                    if ( !defined( $self->{local_address} ) ) 
                                    {
                                        $self->{local_address} = 'localhost';
                                    }
                                    my $hlen = length( $self->{local_address} );
                                    $clienthandler->push_write(pack( 'CCCC',SOCKS5_VER, 0,0,          ADDR_DOMAINNAME )
                                          . pack( 'C', $hlen )
                                          . $self->{local_address}
                                          . pack( 'n', $self->{local_port} )
                                    );
                                    $stage = 4;

                                    $remotehandler = AnyEvent::Handle->new
                                    (
                                        autocork  => 1,
                                        keepalive => 1,
                                        no_delay  => 1,
                                        connect   => [$self->{server},$self->{server_port}],
                                        on_connect => sub 
                                        {
                                            my ($rhandle,  $peerhost,$peerport, $retry) = @_;

                                            unless ($rhandle) 
                                            {
                                                AE::log info =>"couldn't connect: $!";
                                                return;
                                            }
                                        },
                                        on_eof => sub 
                                        {
                                            my $rhandler = shift;
                                            $rhandler->destroy();
                                            $mode = 0;
                                            AE::log info => "Remote: Done.";
                                        },
                                        on_error => sub 
                                        {
                                            my ( $rhandler, $fatal, $msg ) = @_;
                                            AE::log error => $msg;
                                            $mode = 0;
                                            $rhandler->destroy();
                                        },
                                        on_read => sub 
                                        {
                                            my $incomingdata = $remotehandler->rbuf();
                                            my $decrypteddata;
                                                unless ( defined($server_iv) ) 
                                                {
                                                    $server_iv = substr( $incomingdata, 0,length($iv) );
                                                    $incomingdata = substr($incomingdata,length($iv));
                                                    if ( $self->{method} eq'rc4-md5' )
                                                    {
                                                        my $md = Digest::MD5->new();
                                                        $md->add($key . $server_iv);
                                                        #$md->add($server_iv);
                                                        my $decrypt_rc4_key = $md->digest();
                                                        Mcrypt::mcrypt_init($decryptor,$decrypt_rc4_key,'' );
                                                    }
                                                    elsif($self->{method} eq'rc4-sha')
                                                    {
                                                        my $sha = Digest::SHA->new();
                                                        $sha->add($key . $server_iv);
                                                        #$md->add($server_iv);
                                                        my $decrypt_rc4_key = substr($sha->digest(),0,16);
                                                        Mcrypt::mcrypt_init($decryptor,$decrypt_rc4_key,'' );
                                                        }
                                                        else
                                                        {
                                                            
                                                            Mcrypt::mcrypt_init($decryptor,$key,$server_iv );
                                                            }
                                                }
                                                if ($self->{method} eq 'rc5')
                                                {
                                                    $decrypteddata = $decryptor->decrypt($incomingdata,$key,$server_iv);
                                                    }
                                                    else
                                                    {
                                                $decrypteddata = Mcrypt::mcrypt_decrypt($decryptor, $incomingdata );
                                            }
                                            $clienthandler->push_write($decrypteddata);
                                            $remotehandler->{rbuf} = '';
                                        },
                                    );
                                    $stage = 5;
                                }
                                elsif ( $cmd == CMD_BIND ) 
                                {
                                    carp 'BIND Request not supported';
                                    $stage = 0;
                                    $clienthandler->destroy();
                                    return;
                                }
                                elsif ( $cmd == CMD_UDPASSOC ) 
                                {
                                    carp 'UDP ASSOCIATE request not implemented';
                                    $stage = 0;
                                    $clienthandler->destroy();
                                    return;
                                }

                                else 
                                {
                                    carp 'Unknown command';
                                    $stage = 0;
                                    $clienthandler->destroy();
                                    return;
                                }
                            }

                            elsif ( $stage == 4 or $stage == 5 ) 
                            {
                                my $plaindata = $client_buffer;
                                my $encrypteddata;
                                if ( $self->{method} eq 'rc6' ) 
                                    {
                                          if ( $addr_to_send ne '' ) 
                                          {
                                             $plaindata = $addr_to_send . $client_buffer; 
                                              $addr_to_send = '';
                                          }                                   
                                          $encrypteddata = $encryptor->encrypt($plaindata,$key,$iv);
                                    }
                                    else
                                    {
                                         if ( $addr_to_send ne '' ) 
                                         {
                                             $plaindata = $addr_to_send . $client_buffer;
                                             if ( $self->{method} eq 'rc4-md5' )
                                             {
                                                 my $md = Digest::MD5->new();
                                                 $md->add($key . $iv);
                                                 #$md->add($iv);
                                                 my $encrypt_rc4_key = $md->digest();
                                                 Mcrypt::mcrypt_init($encryptor, $encrypt_rc4_key, '' );
                                            }
                                            elsif($self->{method} eq 'rc4-sha' )
                                            {
                                                my $sha = Digest::SHA->new();
                                                 $sha->add($key . $iv);
                                                 #$md->add($iv);
                                                 my $encrypt_rc4_key = substr($sha->digest(),0,16);
                                                 Mcrypt::mcrypt_init($encryptor, $encrypt_rc4_key, '' );
                                                }
                                            else 
                                            {
                                                 Mcrypt::mcrypt_init($encryptor, $key, $iv );
                                            }
                                            $addr_to_send = '';
                                     }
                                     $encrypteddata = Mcrypt::mcrypt_encrypt($encryptor,$plaindata);
                                }
                                my $datatosend;
                                if ( $mode == 0  ) 
                                {
                                    if (   $self->{method} =~ /gcm$/ or $self->{method} =~ /poly1305$/ )
                                    {
                                        $datatosend = $nonce . $encrypteddata;
                                    }
                                    else {
                                        $datatosend = $iv . $encrypteddata;
                                    }
                                    $mode = 1;
                                }
                                else {
                                    $datatosend = $encrypteddata;
                                }

                                $remotehandler->push_write($datatosend);
                            }
                        }
                        $clienthandler->{rbuf} = '';
                    },
                );
            }
        );
        my $cv = AE::cv;
        $cv->recv();
        return $self;
    }
    1;    # End of Net::Shadowsocks::Client

__END__

=pod

=encoding UTF-8

=head1 NAME

Net::Shadowsocks::Client - Shadowsocks protocol client module.

=head1 VERSION

version 0.7.0

=head1 SYNOPSIS

    Shadowsocks protocol client module.
    
    The following encryption methods are supported:

rc4-md5 rc4-sha aes-128-cbc aes-128-cfb aes-128-ofb aes-256-cbc aes-256-cfb aes-256-ofb

Experimental support of RC6 is also provided.

Please also note that both the stock libmcrypt and stock Mcrypt module do not export CTR 
mode, So the aes-128-ctr and aes-256-ctr method will not work out of box. You have to use this 
  patched version of libmcrypt and Libmcrypt module from https://osdn.net/projects/mcrypt/
to get it to work.  

Net::Shadowsocks::Client

=head1 VERSION

Version 0.7.0

=head1 METHODS

=head2 new

    The C<new> constructor lets you create a new B<Net::Shadowsocks::Client> object.

    So no big surprises there...

    Returns a new B<Net::Shadowsocks::Client> or dies on error.

    example use:

    use Net::Shadowsocks::Client;

    my $foo = Net::Shadowsocks::Client->new(
    local_address => 'localhost',
    local_port => 1491,
    password => '49923641',
    server => 'jp.ssip.club',
    server_port => 23333,
    method => 'rc6',
    );

    This is all you need to do. Take a look at client.pl under eg directory for a compelete example on how to
    use the client module.

=head1 AUTHOR

Li ZHOU <lzh@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2017 by Li ZHOU.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut
