App-proxyforurl

 view release on metacpan or  search on metacpan

script/proxyforurl  view on Meta::CPAN

#!/usr/bin/env perl
use Mojolicious::Lite -signatures;

use Mojo::Util  qw(network_contains);
use NetAddr::IP ();
use Socket      qw(AF_INET AF_INET6 inet_ntop getaddrinfo unpack_sockaddr_in unpack_sockaddr_in6);

get
  '/' => {layout => 'pac'},
  'index';

get '/pac', [format => [qw(js)]], => 'pac';

post '/v1/gethostbyname' => sub ($c) {
  return $c->render(text => 'Host missing.', status => 400) unless my $host = $c->param('host');
  return $c->render(text => "Invalid host: $host", status => 400) unless $host =~ /[A-Za-z:\.]/;

  $c->res->headers->cache_control('max-age=600');
  return $c->render_later->host_to_ip_p($host)->then(sub ($addr) {
    return $c->render(text => "No IP found for $host", status => 400) unless $addr;
    return $c->render(text => $addr);
  })->catch(
    sub ($err, @) {
      return $c->render(text => exception_to_text($err), status => 500);
    }
  );
};

post '/v1/is-in-net' => sub ($c) {
  my ($ip, $net, $mask) = map { $c->param($_) } qw(ip net mask);
  return $c->render(text => 'IP or Net is missing.', status => 400)
    unless 2 == grep { $_ && $_ =~ /[:\.]/ } $ip, $net;
  return $c->render(text => 'Mask is invalid or missing.', status => 400)
    unless $mask and $mask =~ /^\d{1,3}$/;

  $c->res->headers->cache_control('max-age=600');
  return $c->render_later->host_to_ip_p($ip)->then(sub ($resolved) {
    $c->render(text => $resolved && network_contains("$net/$mask", $resolved) ? 1 : 0);
  })->catch(
    sub ($err, @) {
      $c->render(text => exception_to_text($err), status => 500);
    }
  );
};

get '/v1/template' => {template => 'template'};

helper host_to_ip_p => sub ($c, $host) {
  return Mojo::IOLoop->subprocess->run_p(sub (@) {
    local $SIG{ALRM} = sub { die 'Timeout!' };
    alarm 2;
    my ($err, @info) = getaddrinfo $host;
    return undef if $err;

    @info = grep { $_->{family} & (AF_INET6 | AF_INET) } @info;
    for my $item (@info) {
      $item->{pri} = $item->{family} == AF_INET6 ? 0 : 1;
      @$item{qw(port ip)}
        = $item->{family} == AF_INET6
        ? unpack_sockaddr_in6($item->{addr})
        : unpack_sockaddr_in($item->{addr});
      $item->{ip} = inet_ntop @$item{qw(family ip)};
    }

    # IPv4 before IPv6
    @info = sort { $b->{pri} <=> $a->{pri} } @info;

    return @info && $info[0]{ip} || undef;
  })->catch(
    sub ($err, @) {
      return $err =~ m!Timeout! ? undef : Mojo::Promise->reject($err);
    }
  );
};

if (my $env_base = $ENV{PROXYFORURL_X_REQUEST_BASE}) {
  $env_base = '' unless $env_base =~ m!^http!;
  hook before_dispatch => sub ($c) {
    return unless my $base = $env_base || $c->req->headers->header('X-Request-Base');
    $c->req->url->base(Mojo::URL->new($base));
  };
}

app->renderer->paths([$ENV{PROXYFORURL_TEMPLATES} || 'templates']);
app->static->paths([$ENV{PROXYFORURL_PUBLIC}      || 'public']);
app->config(brand_name => $ENV{PROXYFORURL_BRAND_NAME} || 'ProxyForURL');
app->config(brand_url  => $ENV{PROXYFORURL_BRAND_URL}  || '/');
app->start;

sub exception_to_text ($err) {
  $err =~ s!\sat\s\S+.*?line.*!!s;
  return $err;
}

__DATA__
@@ layouts/pac.html.ep
<!DOCTYPE html>
<html>
  <head>
    %= include 'partial/pac/head'
  </head>
  <body>
    <nav>
      %= include 'partial/pac/nav'
    </nav>
    <div class="container">
      <article>
        <header>
          <h1>Online proxy PAC file tester</h1>
          <p>Enter the content of your PAC file and test it against a URL.</p>
        </header>
        %= content
        <footer>
          <a href="https://github.com/jhthorsen/app-proxyforurl">This program</a> is free software,
          you can redistribute it and/or modify it under the terms of the
          <a href="https://spdx.org/licenses/Artistic-2.0.html">Artistic License version 2.0</a>.
        </footer>
      </article>
    </div>
    %= javascript begin
      const switcher = document.getElementById('theme_switcher');
      const setTheme = (e) => {
        const prefers = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
        const other = prefers === 'dark' ? 'light' : 'dark';
        document.documentElement.setAttribute('data-theme', e.target.checked ? other : prefers);
      };
      switcher.addEventListener('click', setTheme);
      setTheme({target: switcher});
    % end
  </body>
</html>

script/proxyforurl  view on Meta::CPAN

  }

  async dnsDomainLevels(host) {
    const m = host.match(/\./g);
    return m ? m.length : 0;
  }

  async dnsResolve(host) {
    const body = new FormData();
    body.append('host', host);
    const res = await fetch(this.basePath + '/v1/gethostbyname', {method: 'POST', body});
    const text = await res.text();
    if (res.status >= 500) throw 'dnsResolve() FAIL ' + (text || res.status);
    return text;
  }

  async isInNet(ip, net, mask) {
    // Ex: Turn 255.255.255.0 into 24
    if (mask.match(/\./)) mask = Math.round(mask.split('.').reduce((c, o) => c - Math.log2(256 - o), 32));

    const body = new FormData();
    body.append('ip', ip);
    body.append('net', net);
    body.append('mask', mask);
    const res = await fetch(this.basePath + '/v1/is-in-net', {method: 'POST', body});
    const text = await res.text();
    if (res.status >= 500) throw 'isInNet() FAIL ' + (text || res.status);
    return parseInt(text, 10) ? true : false;
  }

  async isResolvable(host) {
    return await this.dnsResolve(host) ? true : false;
  }

  async isPlainHostName(str) {
    return str.match(/\./) ? false : true;
  }

  async localHostOrDomainIs(host, str) {
    return str.match(/^\./) ? dnsDomainIs(host, str) : host === str ? true : host === host.split('.')[0];
  }

  async myIpAddress() {
    return this.myIpAddressInput.value || this.remoteAddress || '127.0.0.1';
  }

  async shExpMatch(host, re) {
    return host.match(new RegExp(re.replace(/\*/, '.*?'), 'i')) ? true : false;
  }

  async timeRange() {
    return true;
  }

  async weekdayRange() {
    return true;
  }

  _animate(start) {
    const method = start ? 'setAttribute' : 'removeAttribute';
    setTimeout(() => {
      this.form.querySelector('button')[method]('aria-busy', true);
    }, (start ? 1 : 350));
  }

  async _init() {
    const res = await fetch(this.basePath + '/v1/template');
    const text = await res.text();

    if (!this.rulesInput.value) this.rulesInput.value = text;
    const yourIp = text.match(/Your IP: (\S+)/) || [];
    this.remoteAddress = yourIp[1] || '127.0.0.1';
    this.myIpAddressInput.placeholder = this.remoteAddress;
    this.myIpAddressInput.value = this.remoteAddress;
  }

  async _wrap(func) {
    const args = [].slice.call(arguments, 1);
    const res = await this[func].apply(this, args);
    this.log(func, args, res);
    return res;
  }
}
@@ template.html.ep
// Your IP: <%= $c->tx->remote_address %>
function FindProxyForURL(url, host) {
  // our local URLs from the domains below example.com don't need a proxy:
  if (shExpMatch(host, "*.example.com")) return "DIRECT";
  if (isPlainHostName(host)) return "DIRECT";
  if (localHostOrDomainIs(host, "www.example.com")) return "PROXY local.proxy:8080";
  if (!isResolvable(host)) return "SOCKS4 example.com:1080";
  if (dnsDomainLevels(host) === 4) return "SOCKS4 example.com:1081";

  alert("Checking other rules...");
  alert(myIpAddress());

  // URLs within this network are accessed through
  // port 8080 on fastproxy.example.com:
  if (isInNet(host, "10.0.0.0", "255.255.248.0")) return "PROXY fastproxy.example.com:8080";

  if (dnsDomainIs(host, "le.com")) return "PROXY proxy2.example.com:8888";

  // All other requests go through port 8080 of proxy.example.com.
  // should that fail to respond, go directly to the WWW:
  return "PROXY proxy.example.com:8080; DIRECT";
}



( run in 0.526 second using v1.01-cache-2.11-cpan-39bf76dae61 )