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 )