#!/usr/bin/env starman package Clover::Proxy; use strict; use warnings; use HTTP::Tiny; use HTTP::Body; use URL::Encode; use Data::Dumper; =head1 Clover::Proxy Application which proxies requests to the clover API, handling the oauth parts for you =cut # Clover generally expects that you have an HTTP endpoint on your end that behaves a particular way. # Example: $my_site/clover?code=AUTH_CODE&merchant_id=MERCH_ID # It is then expected to POST to the oauth endpoint. # Ye olde PSGI boilerstrap our $CHUNK_SIZE = 1024000; our %content_types = ( text => "text/plain", html => "text/html", json => "application/json", blob => "application/octet-stream", xml => "text/xml", xsl => "text/xsl", css => "text/css", rss => "application/rss+xml", email => "multipart/related", ); our %byct = reverse %content_types; my $ct = 'Content-type'; my $cur_query = {}; my %routes = ( '/oauth' => \&oauth, ); my $clover_uri = "https://apisandbox.dev.clover.com"; # On startup, let's oauth. sub new { my $self = {}; $self->{agent} = HTTP::Tiny->new(); # TODO Grab our 2 secrets from config bless($self, __PACKAGE__); return $self; } sub agent { $_[0]->{agent} } sub oauth { my ($self, $data) = @_; my $oauth_endpoint = 'oauth/v2/authorize'; my $token_endpoint = 'oauth/v2/token'; my $client_id = 'W5QCN9ZCQX4WT'; my $client_secret = 'df5ac68e-2cc5-8e4d-0788-928ea78615c1'; # This is going to be provided to us by the HTTP endpoint listed above my $auth_code = $data->{code}; return badrequest() unless $auth_code; my $oauth_data = { client_id => $client_id, client_secret => $client_secret, authorization_code => $auth_code, }; return [200, ['Content-type', 'text/plain'], [Dumper($oauth_data)]]; # Should give us an access_token and refresh_token die Dumper($self->agent->request('POST', qq{$clover_uri/$oauth_endpoint}, $oauth_data )); # TODO store this data in ye olde DB, and do cache invalidation etc } sub badrequest { return _generic(400, "Bad Request"); } sub error { return _generic(500, "Internal Server Error"); } sub unavailable { return _generic(503, "Service Unavailable"); } sub toolong { return _generic(419, "Excessive URI Length"); } sub _generic { my ($code, $msg) = @_; return [$code, [$ct => $content_types{html}], [$msg]]; } sub proxy { my ($self, $method, $route, $data) = @_; die Dumper($self->agent->request($method, qq{$clover_uri/$route}, $data)); } sub _parse_request { my ($env) = @_; my $query = URL::Encode::url_params_mixed( $env->{QUERY_STRING} ) if $env->{QUERY_STRING}; #Actually parse the POSTDATA and dump it into the QUERY object if this is a POST if ( $env->{REQUEST_METHOD} eq 'POST' ) { my $body = HTTP::Body->new( $env->{CONTENT_TYPE}, $env->{CONTENT_LENGTH} ); while ( $env->{'psgi.input'}->read( my $buf, $CHUNK_SIZE ) ) { $body->add($buf); } @$query{ keys( %{ $body->param } ) } = values( %{ $body->param } ); @$query{ keys( %{ $body->upload } ) } = values( %{ $body->upload } ); } return $query; } # Just dispatch it sub app { my ($self, $env) = @_; return toolong() if length( $env->{REQUEST_URI} ) > 2048; my $route = $env->{PATH_INFO}; my $method = $env->{REQUEST_METHOD}; my $data = _parse_request($env); return $routes{$route}->($self, $data) if $routes{$route}; # Otherwise just proxy stuffs if we have a token return unavailable() unless $self->_has_valid_token(); return $self->proxy($route, $method, $data); } my %options; # Don't ever put anything past $app our $app = sub { my $self = __PACKAGE__->new(%options); return eval { $self->app(@_) } || do { my $env = shift; $env->{'psgi.errors'}->print($@); # Redact the stack trace past line 1, it usually has things which should not be shown $self->{cur_query}->{message} = $@; $self->{cur_query}->{message} =~ s/\n.*//g if $self->{cur_query}->{message}; return error($self->{cur_query}); }; };