package Mojolicious::Plugin::ForwardedFor;

use Mojo::Base 'Mojolicious::Plugin';
use Mojo::Util 'trim';

our $VERSION = '0.002';

sub register {
  my ($self, $app, $options) = @_;

  my ($levels, $err);
  {
    local $@;
    use warnings FATAL => 'numeric';
    unless (eval { $levels = int($options->{levels} // $ENV{MOJO_REVERSE_PROXY} // 1); 1 }) {
      $err = $@;
    }
  }

  die "Invalid reverse proxy 'levels' for ForwardedFor: $err" if defined $err;

  $app->helper(forwarded_for => sub {
    my $c = shift;
    return $c->tx->original_remote_address unless $levels > 0;
    my @addresses = split /\s*,\s*/, trim($c->tx->req->headers->header('X-Forwarded-For') // '');
    return $addresses[-$levels] // $addresses[0] // $c->tx->original_remote_address;
  });
}

1;

=head1 NAME

Mojolicious::Plugin::ForwardedFor - Retrieve the remote address from X-Forwarded-For

=head1 SYNOPSIS

  use Mojolicious::Lite;
  plugin ForwardedFor => {levels => 2}; # number of reverse proxies you control
  any '/' => sub {
    my $c = shift;
    $c->render(json => {remote_addr => $c->forwarded_for});
  };
  app->start;

=head1 DESCRIPTION

L<Mojolicious> supports deployment via a
L<reverse proxy|Mojolicious::Guides::Cookbook/"Reverse proxy"> setup by
specifying the L<proxy|Mojo::Server::Hypnotoad/"proxy"> configuration option
for Hypnotoad, or the C<MOJO_REVERSE_PROXY> environment variable. However,
L<Mojo::Transaction/"remote_address"> will in this case only return the most
recent address from the C<X-Forwarded-For> header, as it cannot automatically
determine how many remote addresses correspond to proxies.

L<Mojolicious::Plugin::ForwardedFor> can be configured with the number of
reverse proxy L</"levels"> that you control, and provides a L</"forwarded_for">
helper method that will return the remote address at that level. It is
important to set L</"levels"> no higher than the number of proxies that will
have appended addresses to the C<X-Forwarded-For> header, as the original
requests can pass anything as the initial value of the header, and thus spoof
additional proxy levels.

Since Mojolicious 8.72, you can configure
L<Mojo::Server::Hypnotoad/"trusted_proxies"> as a more reliable alternative to
the baseline reverse proxy configuration, affecting
L<Mojo::Transaction/"remote_address"> directly without need of this plugin.

=head1 HELPERS

L<Mojolicious::Plugin::ForwardedFor> implements the following helpers.

=head2 forwarded_for

  my $remote_addr = $c->forwarded_for;

Returns the least recently appended remote address from the C<X-Forwarded-For>
header, while skipping no more than the configured number of reverse proxy
L</"levels">. Returns the originating address of the current request if
configured for 0 reverse proxy levels, or if no addresses have been appended to
the header.

=head1 METHODS

L<Mojolicious::Plugin::ForwardedFor> inherits all methods from
L<Mojolicious::Plugin> and implements the following new ones.

=head2 register

  $plugin->register(Mojolicious->new);
  $plugin->register(Mojolicious->new, {levels => 1});

Register helper in L<Mojolicious> application. Takes the following options:

=over 4

=item levels

Number of remote proxy levels to allow for when parsing C<X-Forwarded-For>.
Defaults to the value of the C<MOJO_REVERSE_PROXY> environment variable, or 1.

=back

=head1 BUGS

Report any issues on the public bugtracker.

=head1 AUTHOR

Dan Book <dbook@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2018 by Dan Book.

This is free software, licensed under:

  The Artistic License 2.0 (GPL Compatible)

=head1 SEE ALSO

L<Mojo::Transaction>, L<Mojolicious::Guides::Cookbook>
