From 4bc9da9f6a01340fffe9b8e1169185e4acae49c4 Mon Sep 17 00:00:00 2001 From: Nicolas Boisselier Date: Mon, 21 Mar 2016 11:49:50 +0100 Subject: [PATCH] api-ovh --- bin/ovh-api | 181 +++++++++++++++++++ lib/perl/OvhApi.pm | 359 ++++++++++++++++++++++++++++++++++++++ lib/perl/OvhApi/Answer.pm | 200 +++++++++++++++++++++ 3 files changed, 740 insertions(+) create mode 100755 bin/ovh-api create mode 100644 lib/perl/OvhApi.pm create mode 100644 lib/perl/OvhApi/Answer.pm diff --git a/bin/ovh-api b/bin/ovh-api new file mode 100755 index 00000000..be4efbea --- /dev/null +++ b/bin/ovh-api @@ -0,0 +1,181 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use OvhApi; +use JSON; +################################################################################# +# +# VERSION +# +################################################################################# +my $VERSION = '0.0.1'; +# NB 21.03.16 +# - create script: + +################################################################################# +# +# GLOBALS +# +################################################################################# +my ($NAME) = $0 =~ m,([^/]+)$,; + +################################################################################# +# +# ARGS +# +################################################################################# +my $VERBOSE = $main::VERBOSE = 1; +my $DEBUG = $main::DEBUG = 0; + +my %Opt; get_options(\%Opt); +help() unless @ARGV; +$main::_DATA_ = undef; + +################################################################################# +# +# BEGIN +# +################################################################################# +#use Data::Dumper; +@_ = split(":",$ARGV[0]); +my $o = OvhApi->new(type => OvhApi::OVH_API_EU, applicationKey => $_[0], applicationSecret => $_[1], consumerKey => $_[2]); +print JSON::encode_json($o->get(path=>$ARGV[1])->content); + +################################################################################# +# +# END +# +################################################################################# +exit 0; + +################################################################################# +# +# Functions +# +################################################################################# +sub help { +#------------------------------------------------------------------------------ +# Print help and exit +#------------------------------------------------------------------------------ + + require 'Pod/Usage.pm' unless $INC{'Pod/Usage.pm'}; + require 'Pod/Perldoc.pm' unless $INC{'Pod/Perldoc.pm'}; + + # Substitutions + sub pod_env { + my $v = ''; + eval '$v = ref(\\'.$_[0].') eq "ARRAY" ? join(" ",'.$_[0].') : '.$_[0].'; return defined $v ? $v : qq|UNDEF|;'; + return $v; + } + + $main::_DATA_ =~ s/([@\$][A-Z_]+)/pod_env($1)/eg; + + # Create tmp + my $in_file = (-e '/dev/shm' ? '/dev/shm' : '/tmp')."/$NAME.$$"; + my $in; + open($in,">$in_file") or die "$NAME: Can't write into $in_file: $!"; + print $in $main::_DATA_; + close $in; + + # Output + open(STDOUT,"|perl -pe 's/\.$$//g'".(($ENV{PAGER}||'') eq 'less' ? "|less -FRi" : "")); + my $opts = { + -input => $in_file, + -ouput => \*STDOUT, + -exitval => 'noexit', + -sections => [qw(SYNOPSIS DESCRIPTION OPTIONS)], + -verbose => ($Opt{'help'} ? 99 : 3), + }; + + Pod::Usage::pod2usage($opts); + close STDOUT; + unlink $in_file if $in_file and -e $in_file; + + exit 0; +} + +#------------------------------------------------------------------------------ +# Print version and exit +#------------------------------------------------------------------------------ +sub version { print "$NAME: version [$VERSION]\n"; exit 0; } + +#------------------------------------------------------------------------------ +# Get options from pod +#------------------------------------------------------------------------------ +sub get_options { + + use Getopt::Long qw(:config no_ignore_case no_auto_abbrev); + + my @Opt; + + sub pod_opt { + local $_; + my $o = shift; + $o =~ s/(=.|[\+\-\!]$)//; + $o = join(", ",map{"-$_"} split(/[\|,:;]/,$o)); + return "$o"; + } + + while () { + s/option\[([^\]]+)\]/push(@Opt,$1) and pod_opt($1)/eg; + $main::_DATA_ .= $_; + } + + GetOptions($_[0],@Opt) || exit -1; + + help() if $_[0]{'help'} or $_[0]{'man'}; + version() if $_[0]{'version'}; + + $main::VERBOSE = $VERBOSE = $_[0]{'verbose'} if defined $_[0]{'verbose'}; + $main::DEBUG = $DEBUG = $_[0]{'debug'} if defined $_[0]{'debug'}; + +} + +__DATA__ + +=head1 NAME + +$NAME - Script to query ovh + +=head1 SYNOPSIS + +Quick usage: $NAME k1:k2:k3 URL + +=head1 DESCRIPTION + +https://api.ovh.com/g934.first_step_with_api +https://api.ovh.com/console/#/domain + +=head1 OPTIONS + + option[verbose|v+] Verbose mode: increase the verbosity level. + option[debug+] Debug mode: increase the verbosity level. + option[version|V] Print version (default: $VERSION) + option[help|h|?] Print a brief help message and exits. + option[man] Print the manual page and exits. + +=cut + +=head1 EXAMPLES + +$NAME k1:k2:k3 /domain + +=head1 REQUIRES + +Getopt::Std, Pod::Usage + +=head1 COPYRIGHT AND LICENSE + +Copyright (C) 2016 Nicolas Boisselier + +This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +See . + +=head1 AUTHOR + +Nicolas Boisselier nicolas.boisselier@gmail.com + +=cut diff --git a/lib/perl/OvhApi.pm b/lib/perl/OvhApi.pm new file mode 100644 index 00000000..4077e624 --- /dev/null +++ b/lib/perl/OvhApi.pm @@ -0,0 +1,359 @@ +package OvhApi; + +use strict; +use warnings; + +our $VERSION = 1.0; + + +use OvhApi::Answer; + +use Carp qw{ carp croak }; +use List::Util 'first'; +use LWP::UserAgent (); +use JSON (); +# NB 21.03.16 use Digest::SHA1 'sha1_hex'; +use Digest::SHA 'sha1_hex'; + + + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Class constants + +use constant { + OVH_API_EU => 'https://eu.api.ovh.com/1.0', + OVH_API_CA => 'https://ca.api.ovh.com/1.0', +}; + +# End - Class constants +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Class variables + +my $UserAgent = LWP::UserAgent->new(timeout => 10); +my $Json = JSON->new->allow_nonref; + +my @accessRuleMethods = qw{ GET POST PUT DELETE }; + +# End - Class variables +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Class methods + +sub new +{ + my @keys = qw{ applicationKey applicationSecret consumerKey }; + + my ($class, %params) = @_; + + if (my @missingParameters = grep { not $params{$_} } qw{ applicationKey applicationSecret }) + { + local $" = ', '; + croak "Missing parameter: @missingParameters"; + } + + unless ($params{'type'} and grep { $params{'type'} eq $_ } (OVH_API_EU, OVH_API_CA)) + { + carp 'Missing or invalid type parameter: defaulting to OVH_API_EU'; + } + + my $self = { + _type => ($params{'type'} or OVH_API_EU), + }; + + @$self{@keys} = @params{@keys}; + + bless $self, $class; +} + +sub setRequestTimeout +{ + my ($class, %params) = @_; + + if ($params{'timeout'} =~ /^\d+$/) + { + $UserAgent->timeout($params{'timeout'}); + } + elsif (exists $params{'timeout'}) + { + carp "Invalid timeout: $params{'timeout'}"; + } + else + { + carp 'Missing parameter: timeout'; + } +} + +# End - Class methods +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Instance methods + +sub rawCall +{ + my ($self, %params) = @_; + + my $method = lc $params{'method'}; + my $url = $self->{'_type'} . (substr($params{'path'}, 0, 1) eq '/' ? '' : '/') . $params{'path'}; + + my %httpHeaders; + + my $body = ''; + my %content; + + if ($method ne 'get' and $method ne 'delete') + { + $body = $Json->encode($params{'body'}); + + $httpHeaders{'Content-type'} = 'application/json'; + $content{'Content'} = $body; + } + + unless ($params{'noSignature'}) + { + my $now = $self->_timeDelta + time; + + $httpHeaders{'X-Ovh-Consumer'} = $self->{'consumerKey'}, + $httpHeaders{'X-Ovh-Timestamp'} = $now, + $httpHeaders{'X-Ovh-Signature'} = '$1$' . sha1_hex(join('+', ( + # Full signature is '$1$' followed by the hex digest of the SHA1 of all these data joined by a + sign + $self->{'applicationSecret'}, # Application secret + $self->{'consumerKey'}, # Consumer key + uc $method, # HTTP method (uppercased) + $url, # Full URL + $body, # Full body + $now, # Curent OVH server time + ))); + } + + $httpHeaders{'X-Ovh-Application'} = $self->{'applicationKey'}, + + return OvhApi::Answer->new(response => $UserAgent->$method($url, %httpHeaders, %content)); +} + +sub requestCredentials +{ + my ($self, %params) = @_; + + croak 'Missing parameter: accessRules' unless $params{'accessRules'}; + croak 'Invalid parameter: accessRules' if ref $params{'accessRules'} ne 'ARRAY'; + + my @rules = map { + croak 'Invalid access rule: must be HASH ref' if ref ne 'HASH'; + + my %rule = %$_; + + $rule{'method'} = uc $rule{'method'}; + + croak 'Access rule must have method and path keys' unless $rule{'method'} and $rule{'path'}; + croak 'Invalid access rule method' unless first { $_ eq $rule{'method'} } (@accessRuleMethods, 'ALL'); + + if ($rule{'method'} eq 'ALL') + { + map { path => $rule{'path'}, method => $_ }, @accessRuleMethods; + } + else + { + \%rule + } + } @{ $params{'accessRules'} }; + + return $self->post(path => '/auth/credential/', noSignature => 1, body => { accessRules => \@rules }); +} + +# Generation of helper subs: simple wrappers to rawCall +# Generate: get(), post(), put(), delete() +{ + no strict 'refs'; + + for my $method (qw{ get post put delete }) + { + *$method = sub { rawCall(@_, 'method', $method ) }; + } +} + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Private part + +sub _timeDelta +{ + my ($self, %params) = @_; + + unless (defined $self->{'_timeDelta'}) + { + if (my $ServerTimeResponse = $self->get(path => 'auth/time', noSignature => 1)) + { + $self->{'_timeDelta'} = ($ServerTimeResponse->content - time); + } + else + { + return 0; + } + } + + return $self->{'_timeDelta'}; +} + +# End - Instance methods +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + +return 42; + + +__END__ + +=head1 NAME + +OvhApi - Official OVH Perl wrapper upon the OVH RESTful API. + +=head1 SYNOPSIS + + use OvhApi; + + my $Api = OvhApi->new(type => OvhApi::OVH_API_EU, applicationKey => $AK, applicationSecret => $AS, consumerKey => $CK); + my $Answer = $Api->get(path => '/me'); + +=head1 DESCRIPTION + +This module is an official Perl wrapper that OVH provides in order to offer a simple way to use its RESTful API. +C handles the authentication layer, and uses C in order to run requests. + +Answer are retured as instances of L. + +=head1 CLASS METHODS + +=head2 Constructor + +There is only one constructor: C. + +Its parameters are: + + Parameter Mandatory Default Usage + ------------ ------------ ---------- -------- + type Carp if missing OVH_API_EU() Determine if you'll use european or canadian OVH API (possible values are OVH_API_EU and OVH_API_CA) + timeout No 10 Set the timeout LWP::UserAgent will use + applicationKey Yes - Your application key + applicationSecret Yes - Your application secret + consumerKey Yes, unless for a credential request - Your consumer key + +=head2 OVH_API_EU + +L that points to the root URL of OVH european API. + +=head2 OVH_API_CA + +L that points to the root URL of OVH canadian API. + +=head2 setRequestTimeout + +This method changes the timeout C uses. You can set that in L instead. + +Its parameters are: + + Parameter Mandatory + ------------ ------------ + timeout Yes + +=head1 INSTANCE METHODS + +=head2 rawCall + +This is the main method of that wrapper. This method will take care of the signature, of the JSON conversion of your data, and of the effective run of the query. + +Its parameters are: + + Parameter Mandatory Default Usage + ------------ ------------ ---------- -------- + path Yes - The API URL you want to request + method Yes - The HTTP method of the request (GET, POST, PUT, DELETE) + body No '' The body to send in the query. Will be ignore on a GET + noSignature No false If set to a true value, no signature will be send + +=head2 get + +Helper method that wraps a call to: + + rawCall(method => 'get"); + +All parameters are forwarded to L. + +=head2 post + +Helper method that wraps a call to: + + rawCall(method => 'post'); + +All parameters are forwarded to L. + +=head2 put + +Helper method that wraps a call to: + + rawCall(method => 'put'); + +All parameters are forwarded to L. + +=head2 delete + +Helper method that wraps a call to: + + rawCall(method => 'delete'); + +All parameters are forwarded to L. + +=head2 requestCredentials + +This method will request a Consumer Key to the API. That credential will need to be validated with the link returned in the answer. + +Its parameters are: + + Parameter Mandatory + ------------ ------------ + accessRules Yes + +The C parameter is an ARRAY of HASHes. Each hash contains these keys: + +=over + +=item * method: an HTTP method among GET, POST, PUT and DELETE. ALL is a special values that includes all the methods; + +=item * path: a string that represents the URLs the credential will have access to. C<*> can be used as a wildcard. C will allow all URLs, for example. + +=back + +=head3 Example + + my $Api = OvhApi->new(type => OvhApi::OVH_API_EU, applicationKey => $AK, applicationSecret => $AS, consumerKey => $CK); + my $Answer = $Api->requestCredentials(accessRules => [ { method => 'ALL', path => '/*' }]); + + if ($Answer) + { + my ($consumerKey, $validationUrl) = @{ $Answer->content}{qw{ consumerKey validationUrl }}; + + # $consumerKey contains the newly created Consumer Key + # $validationUrl contains a link to OVH website in order to login an OVH account and link it to the credential + } + +=head1 SEE ALSO + +The guts of module are using: C, C, C. + +=head1 COPYRIGHT + +Copyright (c) 2013, OVH SAS. +All rights reserved. + +This library is distributed under the terms of C. + +=cut + diff --git a/lib/perl/OvhApi/Answer.pm b/lib/perl/OvhApi/Answer.pm new file mode 100644 index 00000000..f5b7df15 --- /dev/null +++ b/lib/perl/OvhApi/Answer.pm @@ -0,0 +1,200 @@ +package OvhApi::Answer; + +use strict; +use warnings; + +our $VERSION = 1.0; + + +use overload ( + bool => \&isSuccess, + '!' => \&isFailure, + fallback => 0, +); + +use Scalar::Util 'blessed'; +use Carp qw{ carp croak }; +use JSON (); + + + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Class variables + +my $Json = JSON->new->allow_nonref; + +# End - Class variables +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Class methods + +sub new +{ + my ($class, %params) = @_; + + unless ($params{'response'}) + { + croak 'Missing parameter: response'; + } + + unless (blessed $params{'response'} and $params{'response'}->isa('HTTP::Response')) + { + croak 'Invalid parameter: reponse'; + } + + bless { response => $params{'response'} }, $class; +} + +# End - Class methods +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Instance methods + +sub isSuccess +{ + my ($self) = @_; + + return $self->{'response'}->is_success; +} + +sub isFailure +{ + my ($self) = @_; + + return not $self->isSuccess; +} + + +sub content +{ + my ($self) = @_; + + if ($self->isFailure) + { + carp 'Fetching content from a failed OvhApi::Response Object'; + return; + } + + return $self->_generateContent; +} + +sub error +{ + my ($self) = @_; + + return $self ? '' : $self->_generateContent->{'message'}; +} + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# private part + +sub _generateContent +{ + my ($self) = @_; + + my $content; + + if ($self->{'response'}->header('Client-Warning') and $self->{'response'}->header('Client-Warning') eq 'Internal response') + { + return { message => 'Internal LWP::UserAgent error : ' . $self->{'response'}->content }; + } + + eval { $content = $Json->decode($self->{'response'}->content); 1; } or do { + carp 'Failed to parse JSON content from the answer: ', $self->{'response'}->content; + return; + }; + + return $content; +} + +# End - Instance methods +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + +return 42; + +__END__ + +=head1 NAME + +OvhApi::Answer - Response to a request run with C. + +=head1 SYNOPSIS + + my $Answer = $Api->get(path => '/me'); + + if ($Answer) + { + # Success: can fetch content and process + my $content = $Answer->content; + } + else + { + # Request failed: stop here and retrieve the error + my $error = $Answer->error; + } + +=head1 DESCRIPTION + +This module represents a response to a query run with C. It is build upon a C object. + +=head1 CLASS METHODS + +=head2 Constructor + +There is only one constructor: C. + +Its parameters are: + + Parameter Mandatory Default Usage + ------------ ------------ ---------- -------- + response Yes - An HTTP::Response object return by LWP::UserAgent + +=head1 INSTANCE METHODS + +=head2 content + +Returns the content of the answer. This method will C if the answer is an error. + +It takes no parameter. + +=head2 error + +Returns the error message of the answer, or an empty string if the answer is a success. + +It takes no parameter. + +=head2 isSuccess + +Forwards a call to C in the inner C of the answer. Returns true is the request was a success, false otherwise. + +It takes no parameter. + +This method is used for the C L. + +=head2 isFailure + +Helper method which returns the boolean negation of L. + +It takes no parameter. + +=head1 SEE ALSO + +The guts of module are using: C. + +=head1 COPYRIGHT + +Copyright (c) 2013, OVH SAS. +All rights reserved. + +This library is distributed under the terms of C. + +=cut + + -- 2.47.3