2016-12-16 10:52:37 +03:00
package PVE::APIClient::LWP ;
use strict ;
use warnings ;
2020-11-30 17:01:49 +03:00
use Carp ;
use HTTP::Request::Common ;
2016-12-16 10:52:37 +03:00
use IO::Socket::SSL ; # important for SSL_verify_callback
2020-11-30 17:01:49 +03:00
use JSON ;
2016-12-16 10:52:37 +03:00
use LWP::UserAgent ;
use Net::SSLeay ;
2020-11-30 17:01:49 +03:00
use URI::Escape ;
use URI ;
2017-12-14 13:12:05 +03:00
use PVE::APIClient::Exception qw( raise ) ;
2016-12-16 10:52:37 +03:00
my $ extract_data = sub {
my ( $ res ) = @ _ ;
croak "undefined result" if ! defined ( $ res ) ;
croak "undefined result data" if ! exists ( $ res - > { data } ) ;
return $ res - > { data } ;
} ;
sub get_raw {
my ( $ self , $ path , $ param ) = @ _ ;
return $ self - > call ( 'GET' , $ path , $ param ) ;
}
sub get {
my ( $ self , $ path , $ param ) = @ _ ;
return $ extract_data - > ( $ self - > call ( 'GET' , $ path , $ param ) ) ;
}
sub post_raw {
my ( $ self , $ path , $ param ) = @ _ ;
return $ self - > call ( 'POST' , $ path , $ param ) ;
}
sub post {
my ( $ self , $ path , $ param ) = @ _ ;
return $ extract_data - > ( $ self - > call ( 'POST' , $ path , $ param ) ) ;
}
sub put_raw {
my ( $ self , $ path , $ param ) = @ _ ;
return $ self - > call ( 'PUT' , $ path , $ param ) ;
}
sub put {
my ( $ self , $ path , $ param ) = @ _ ;
return $ extract_data - > ( $ self - > call ( 'PUT' , $ path , $ param ) ) ;
}
sub delete_raw {
my ( $ self , $ path , $ param ) = @ _ ;
return $ self - > call ( 'DELETE' , $ path , $ param ) ;
}
sub delete {
my ( $ self , $ path , $ param ) = @ _ ;
return $ extract_data - > ( $ self - > call ( 'DELETE' , $ path , $ param ) ) ;
}
sub update_csrftoken {
my ( $ self , $ csrftoken ) = @ _ ;
$ self - > { csrftoken } = $ csrftoken ;
my $ agent = $ self - > { useragent } ;
$ agent - > default_header ( 'CSRFPreventionToken' , $ self - > { csrftoken } ) ;
}
sub update_ticket {
my ( $ self , $ ticket ) = @ _ ;
my $ agent = $ self - > { useragent } ;
$ self - > { ticket } = $ ticket ;
my $ encticket = uri_escape ( $ ticket ) ;
2017-04-06 12:03:41 +03:00
my $ cookie = "$self->{cookie_name}=$encticket; path=/; secure;" ;
2016-12-16 10:52:37 +03:00
$ agent - > default_header ( 'Cookie' , $ cookie ) ;
}
2019-08-16 14:51:34 +03:00
sub two_factor_auth_login {
my ( $ self , $ type , $ challenge ) = @ _ ;
if ( $ type eq 'PVE:tfa' ) {
raise ( "TFA-enabled login currently works only with a TTY." ) if ! - t STDIN ;
print "\nEnter OTP code for user $self->{username}: " ;
my $ tfa_response = <STDIN> ;
chomp $ tfa_response ;
return $ self - > post ( '/api2/json/access/tfa' , { response = > $ tfa_response } ) ;
} elsif ( $ type eq 'PVE:u2f' ) {
# TODO: implement u2f-enabled join
raise ( "U2F-enabled login is currently not implemented." ) ;
} else {
raise ( "Authentication type '$type' not recognized, aborting!" ) ;
}
}
2016-12-16 10:52:37 +03:00
sub login {
my ( $ self ) = @ _ ;
my $ uri = URI - > new ( ) ;
$ uri - > scheme ( $ self - > { protocol } ) ;
$ uri - > host ( $ self - > { host } ) ;
$ uri - > port ( $ self - > { port } ) ;
$ uri - > path ( '/api2/json/access/ticket' ) ;
my $ ua = $ self - > { useragent } ;
2019-06-27 23:04:33 +03:00
my $ username = $ self - > { username } // 'unknown' ,
2016-12-16 10:52:37 +03:00
2020-11-30 17:00:38 +03:00
delete $ self - > { fingerprint } - > { last_unknown } ;
2016-12-16 10:52:37 +03:00
my $ exec_login = sub {
return $ ua - > post ( $ uri , {
2019-06-27 23:04:33 +03:00
username = > $ username ,
2020-12-03 17:59:06 +03:00
password = > $ self - > { password } || ''
} ) ;
2016-12-16 10:52:37 +03:00
} ;
my $ response = $ exec_login - > ( ) ;
if ( ! $ response - > is_success ) {
2020-11-30 17:00:38 +03:00
if ( my $ fp = delete ( $ self - > { fingerprint } - > { last_unknown } ) ) {
2016-12-16 10:52:37 +03:00
if ( $ self - > manual_verify_fingerprint ( $ fp ) ) {
$ response = $ exec_login - > ( ) ; # try again
}
}
}
if ( ! $ response - > is_success ) {
2017-12-14 13:12:05 +03:00
raise ( $ response - > status_line . "\n" , code = > $ response - > code )
2016-12-16 10:52:37 +03:00
}
my $ res = from_json ( $ response - > decoded_content , { utf8 = > 1 , allow_nonref = > 1 } ) ;
my $ data = $ extract_data - > ( $ res ) ;
$ self - > update_ticket ( $ data - > { ticket } ) ;
$ self - > update_csrftoken ( $ data - > { CSRFPreventionToken } ) ;
2019-08-16 14:51:34 +03:00
# handle two-factor login
my $ tfa_ticket_re = qr/^([^\s!]+)![^!]*(!([0-9a-zA-Z\/ . = _ \ - + ] + ) ) ? $/ ;
if ( $ data - > { ticket } =~ m/$tfa_ticket_re/ ) {
my ( $ type , $ challenge ) = ( $ 1 , $ 2 ) ;
$ data = $ self - > two_factor_auth_login ( $ type , $ challenge ) ;
$ self - > update_ticket ( $ data - > { ticket } ) ;
}
2016-12-16 10:52:37 +03:00
return $ data ;
}
sub manual_verify_fingerprint {
my ( $ self , $ fingerprint ) = @ _ ;
if ( ! $ self - > { manual_verification } ) {
2017-12-14 13:12:06 +03:00
raise ( "fingerprint '$fingerprint' not verified, abort!\n" ) ;
2016-12-16 10:52:37 +03:00
}
print "The authenticity of host '$self->{host}' can't be established.\n" .
"X509 SHA256 key fingerprint is $fingerprint.\n" .
"Are you sure you want to continue connecting (yes/no)? " ;
2018-01-22 12:52:13 +03:00
my $ answer = <STDIN> ;
2016-12-16 10:52:37 +03:00
my $ valid = ( $ answer =~ m/^\s*yes\s*$/i ) ? 1 : 0 ;
2020-11-30 17:00:38 +03:00
$ self - > { fingerprint } - > { cache } - > { $ fingerprint } = $ valid ;
2016-12-16 10:52:37 +03:00
2017-12-14 13:12:06 +03:00
raise ( "Fingerprint not verified, abort!\n" ) if ! $ valid ;
2016-12-16 10:52:37 +03:00
if ( my $ cb = $ self - > { register_fingerprint_cb } ) {
$ cb - > ( $ fingerprint ) if $ valid ;
}
return $ valid ;
}
sub call {
my ( $ self , $ method , $ path , $ param ) = @ _ ;
2020-11-30 17:00:38 +03:00
delete $ self - > { fingerprint } - > { last_unknown } ;
2016-12-16 10:52:37 +03:00
my $ ticket = $ self - > { ticket } ;
2020-01-30 14:07:52 +03:00
my $ apitoken = $ self - > { apitoken } ;
2016-12-16 10:52:37 +03:00
my $ ua = $ self - > { useragent } ;
# fixme: check ticket lifetime?
2020-01-30 14:07:52 +03:00
if ( ! $ ticket && ! $ apitoken && $ self - > { username } && $ self - > { password } ) {
2016-12-16 10:52:37 +03:00
$ self - > login ( ) ;
}
my $ uri = URI - > new ( ) ;
$ uri - > scheme ( $ self - > { protocol } ) ;
$ uri - > host ( $ self - > { host } ) ;
$ uri - > port ( $ self - > { port } ) ;
$ path =~ s!^/+!! ;
if ( $ path !~ m !^api2/! ) {
$ uri - > path ( "api2/json/$path" ) ;
} else {
$ uri - > path ( $ path ) ;
}
#print "CALL $method : " . $uri->as_string() . "\n";
my $ exec_method = sub {
my $ response ;
if ( $ method eq 'GET' ) {
$ uri - > query_form ( $ param ) ;
$ response = $ ua - > request ( HTTP::Request::Common:: GET ( $ uri ) ) ;
} elsif ( $ method eq 'POST' ) {
$ response = $ ua - > request ( HTTP::Request::Common:: POST ( $ uri , Content = > $ param ) ) ;
} elsif ( $ method eq 'PUT' ) {
# We use another temporary URI object to format
# the application/x-www-form-urlencoded content.
my $ tmpurl = URI - > new ( 'http:' ) ;
$ tmpurl - > query_form ( %$ param ) ;
my $ content = $ tmpurl - > query ;
$ response = $ ua - > request ( HTTP::Request::Common:: PUT ( $ uri , 'Content-Type' = > 'application/x-www-form-urlencoded' , Content = > $ content ) ) ;
} elsif ( $ method eq 'DELETE' ) {
$ response = $ ua - > request ( HTTP::Request::Common:: DELETE ( $ uri ) ) ;
} else {
2017-12-14 13:12:05 +03:00
raise ( "method $method not implemented\n" ) ;
2016-12-16 10:52:37 +03:00
}
return $ response ;
} ;
my $ response = $ exec_method - > ( ) ;
2020-11-30 17:00:38 +03:00
if ( my $ fp = delete ( $ self - > { fingerprint } - > { last_unknown } ) ) {
2016-12-16 10:52:37 +03:00
if ( $ self - > manual_verify_fingerprint ( $ fp ) ) {
$ response = $ exec_method - > ( ) ; # try again
}
}
my $ ct = $ response - > header ( 'Content-Type' ) || '' ;
if ( $ response - > is_success ) {
2017-12-14 13:12:05 +03:00
raise ( "got unexpected content type" , code = > $ response - > code )
if $ ct !~ m | application / json | ;
2016-12-16 10:52:37 +03:00
return from_json ( $ response - > decoded_content , { utf8 = > 1 , allow_nonref = > 1 } ) ;
} else {
2017-12-14 13:12:05 +03:00
my $ msg = $ response - > message ;
my $ errors = eval {
2016-12-16 10:52:37 +03:00
return if $ ct !~ m | application / json | ;
my $ res = from_json ( $ response - > decoded_content , { utf8 = > 1 , allow_nonref = > 1 } ) ;
2017-12-14 13:12:05 +03:00
return $ res - > { errors } ;
2016-12-16 10:52:37 +03:00
} ;
2017-12-14 13:12:05 +03:00
raise ( "$msg\n" , code = > $ response - > code , errors = > $ errors ) ;
2016-12-16 10:52:37 +03:00
}
}
2020-11-30 17:00:38 +03:00
my sub verify_cert_callback {
my ( $ fingerprint , $ cert , $ verify_cb ) = @ _ ;
2016-12-16 10:52:37 +03:00
# check server certificate against cache of pinned FPs
# get fingerprint of server certificate
2017-11-27 12:18:48 +03:00
my $ fp = Net::SSLeay:: X509_get_fingerprint ( $ cert , 'sha256' ) ;
2017-12-12 13:02:48 +03:00
return 0 if ! defined ( $ fp ) || $ fp eq '' ; # error
2016-12-16 10:52:37 +03:00
2020-11-30 17:00:38 +03:00
my $ valid = $ fingerprint - > { cache } - > { $ fp } ;
2016-12-16 10:52:37 +03:00
return $ valid if defined ( $ valid ) ; # return cached result
2020-11-30 17:00:38 +03:00
if ( $ verify_cb ) {
$ valid = $ verify_cb - > ( $ cert ) ;
$ fingerprint - > { cache } - > { $ fp } = $ valid ;
2016-12-16 10:52:37 +03:00
return $ valid ;
}
2020-11-30 17:00:38 +03:00
$ fingerprint - > { last_unknown } = $ fp ;
2016-12-16 10:52:37 +03:00
return 0 ;
} ;
sub new {
my ( $ class , % param ) = @ _ ;
my $ ssl_default_opts = { verify_hostname = > 0 } ;
my $ ssl_opts = $ param { ssl_opts } || $ ssl_default_opts ;
my $ self = {
username = > $ param { username } ,
password = > $ param { password } ,
host = > $ param { host } || 'localhost' ,
port = > $ param { port } ,
protocol = > $ param { protocol } ,
2017-04-06 12:03:41 +03:00
cookie_name = > $ param { cookie_name } // 'PVEAuthCookie' ,
2016-12-16 10:52:37 +03:00
manual_verification = > $ param { manual_verification } ,
2020-11-30 17:00:38 +03:00
fingerprint = > {
cache = > $ param { cached_fingerprints } || { } ,
last_unknown = > undef ,
} ,
2016-12-16 10:52:37 +03:00
register_fingerprint_cb = > $ param { register_fingerprint_cb } ,
timeout = > $ param { timeout } || 60 ,
} ;
2020-11-30 17:03:30 +03:00
bless $ self , $ class ;
2016-12-16 10:52:37 +03:00
if ( ! $ ssl_opts - > { SSL_verify_callback } ) {
$ ssl_opts - > { 'SSL_verify_mode' } = SSL_VERIFY_PEER ;
2020-11-30 17:00:38 +03:00
2020-12-03 17:59:06 +03:00
my $ fingerprints = $ self - > { fingerprint } ; # avoid passing $self, that's a RC cycle!
2020-11-30 17:00:38 +03:00
my $ verify_fingerprint_cb = $ param { verify_fingerprint_cb } ;
2016-12-16 10:52:37 +03:00
$ ssl_opts - > { 'SSL_verify_callback' } = sub {
my ( undef , undef , undef , undef , $ cert , $ depth ) = @ _ ;
# we don't care about intermediate or root certificates
return 1 if $ depth != 0 ;
2020-12-03 17:59:06 +03:00
return verify_cert_callback ( $ fingerprints , $ cert , $ verify_fingerprint_cb ) ;
2016-12-16 10:52:37 +03:00
}
}
if ( ! $ self - > { port } ) {
$ self - > { port } = $ self - > { host } eq 'localhost' ? 85 : 8006 ;
}
if ( ! $ self - > { protocol } ) {
$ self - > { protocol } = $ self - > { host } eq 'localhost' ? 'http' : 'https' ;
}
$ self - > { useragent } = LWP::UserAgent - > new (
protocols_allowed = > [ 'http' , 'https' ] ,
ssl_opts = > $ ssl_opts ,
timeout = > $ self - > { timeout } ,
keep_alive = > $ param { keep_alive } // 50 ,
) ;
$ self - > { useragent } - > default_header ( 'Accept-Encoding' = > 'gzip' ) ; # allow gzip
2020-01-30 14:07:52 +03:00
if ( $ param { apitoken } && $ param { password } ) {
warn "password will be ignored in favor of API token\n" ;
delete $ self - > { password } ;
}
if ( $ param { ticket } ) {
if ( $ param { apitoken } ) {
warn "ticket will be ignored in favor of API token\n" ;
} else {
$ self - > update_ticket ( $ param { ticket } ) ;
}
}
2016-12-16 10:52:37 +03:00
$ self - > update_csrftoken ( $ param { csrftoken } ) if $ param { csrftoken } ;
2020-01-30 14:07:52 +03:00
if ( $ param { apitoken } ) {
my $ agent = $ self - > { useragent } ;
$ self - > { apitoken } = $ param { apitoken } ;
$ agent - > default_header ( 'Authorization' , $ param { apitoken } ) ;
}
2016-12-16 10:52:37 +03:00
return $ self ;
}
1 ;