AnyEvent::ReverseHTTP - reversehttp for AnyEvent


AnyEvent-ReverseHTTP documentation Contained in the AnyEvent-ReverseHTTP distribution.

Index


Code Index:

NAME

Top

AnyEvent::ReverseHTTP - reversehttp for AnyEvent

SYNOPSIS

Top

  use AnyEvent::ReverseHTTP;

  # simple Hello World server
  my $guard = reverse_http "myserver123", "token", sub {
      my $req = shift;
      return "Hello World"; # You can return HTTP::Response object for more control
  };

  # more controls over options and callbacks
  my $server = AnyEvent::ReverseHTTP->new(
      endpoint => "http://www.reversehttp.net/reversehttp",
      label    => "aedemo1234",
      token    => "mytoken",
  );

  $server->on_register(sub {
      my $pub_url = shift;
  });

  $server->on_request(sub {
      my $req = shift;
      # $req is HTTP::Request, return HTTP::Response or AnyEvent::CondVar that receives it
  });

  my $guard = $server->connect;

  AnyEvent->condvar->recv;

DESCRIPTION

Top

AnyEvent::ReverseHTTP is an AnyEvent module that acts as a Reverse HTTP server (which is actually a polling client for Reverse HTTP gateway).

This module implements simple Reverse HTTP client that's tested against reversehttp.net demo server. More complicated specification like relaying or pipelining is not (yet) implemented.

AUTHOR

Top

Tatsuhiko Miyagawa <miyagawa@bulknews.net>

LICENSE

Top

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

SEE ALSO

Top

http://www.reversehttp.net/reverse-http-spec.html


AnyEvent-ReverseHTTP documentation Contained in the AnyEvent-ReverseHTTP distribution.

package AnyEvent::ReverseHTTP;

use strict;
use 5.008_001;
our $VERSION = '0.05';

use Carp;
use AnyEvent::Util;
use AnyEvent::HTTP;
use HTTP::Request;
use HTTP::Response;
use URI::Escape;
use Scalar::Util;

use base qw(Exporter);
our @EXPORT = qw(reverse_http);

use Any::Moose;

has endpoint => (
    is => 'rw', isa => 'Str',
    required => 1, default => "http://www.reversehttp.net/reversehttp",
);

has label => (
    is => 'rw', isa => 'Str',
    required => 1,
    lazy => 1, default => sub {
        require Digest::SHA;
        require Time::HiRes;
        return Digest::SHA::sha1_hex($$ . Time::HiRes::gettimeofday() . {});
    },
);

has token => (
    is => 'rw', isa => 'Str',
    default => '-',
);

has on_register => (
    is => 'rw', isa => 'CodeRef',
    default => sub { sub { warn "Public Application URL: $_[0]\n" } },
);

has on_error => (
    is => 'rw', isa => 'CodeRef',
    default => sub { sub { Carp::croak(@_) } },
);

has on_request => (
    is => 'rw', isa => 'CodeRef',
    default => sub { sub { Carp::croak("on_request handler is not defined!") } },
);

sub reverse_http {
    my $cb = pop;

    my @args =
        @_ == 1 ? qw(label) :
        @_ == 2 ? qw(label token) :
        @_ >= 3 ? qw(endpoint label token) : ();

    my %args; @args{@args} = @_;
    return __PACKAGE__->new(%args, on_request => $cb)->connect;
}

sub connect {
    my $self = shift;

    my %query = (name => $self->label);
    $query{token} = $self->token if $self->token;

    my $body = join "&", map "$_=" . URI::Escape::uri_escape($query{$_}), keys %query;

    http_post $self->endpoint, $body, sub {
        my($body, $hdr) = @_;

        if ($hdr->{Status} eq '201' || $hdr->{Status} eq '204') {
            my $app_url = _extract_link($hdr, 'related');
            $self->on_register->($app_url);
        } else {
            return $self->on_error->("$hdr->{Status}: $hdr->{Reason}");
        }

        my $poller; $poller = sub {
            my($body, $hdr) = @_;

            if ($hdr->{Status} eq '200') {
                my $req  = HTTP::Request->parse($body);
                $req->header('Requesting-Client', $hdr->{'requesting-client'});
                my $res  = $self->on_request->($req);

                my $postback = sub {
                    my $res = shift;

                    # Duck typing for as_string, but accepts plaintext too for 200
                    unless (Scalar::Util::blessed($res) && $res->can('as_string')) {
                        my $content = $res;
                        $res = HTTP::Response->new(200);
                        $res->content_type('text/plain');
                        $res->content($content);
                    }

                    $res->protocol("HTTP/1.1"); # Upgrade since reversehttp.net requires so

                    # HTTP::Response->as_string by default adds a new line which could be harmful
                    my $res_body = $res->as_string;
                    chomp $res_body if $res->content_type eq 'text/plain';

                    http_post $hdr->{URL}, $res_body,
                        headers => { 'content-type' => 'message/http' },
                        sub {
                            my($body, $hdr) = @_;
                            if ($hdr->{Status} ne '202') {
                                $self->on_error->("$hdr->{Status}: $hdr->{Reason}");
                            }
                        };
                };

                # Return condvar to pass back to event loop
                if (Scalar::Util::blessed($res) && $res->isa('AnyEvent::CondVar')) {
                    $res->cb(sub { $postback->($res->recv) });
                } else {
                    $postback->($res);
                }
            }

            my $next = _extract_link($hdr, 'next');
            http_get $next, $poller;
        };

        my $url = _extract_link($hdr, 'first');
        http_get $url, $poller;
    };

    return AnyEvent::Util::guard { undef $self };
}

sub _extract_link {
    my($hdr, $rel) = @_;
    my @links = $hdr->{link} =~ /<([^>]*)>;\s*rel="\Q$rel\E"/g;
    return $links[0];
}

no Any::Moose;
__PACKAGE__->meta->make_immutable;

1;
__END__