|
|
@@ -0,0 +1,167 @@
|
|
|
+#!/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});
|
|
|
+ };
|
|
|
+};
|