Skip to content

Commit

Permalink
Rating improved, conflicts between ip addresses and netmasks resolved
Browse files Browse the repository at this point in the history
  • Loading branch information
73h committed Apr 13, 2024
1 parent 1e234e6 commit 66cecef
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 71 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ subtleties. If you have unusual requirements, it is better to write the proxy sc

## I would still like to implement this

- [x] ~~Automatic sorting for overlapping host names~~
- [x] ~~Implementation of inclusion and exclusion filters for output~~
- [ ] Automatic sorting with overlapping net masks
- [x] ~~automatic sorting for overlapping host names~~
- [x] ~~implementation of inclusion and exclusion filters for output~~
- [x] ~~sort automatically if ip address in network mask~~
- [ ] publish on pypi
- [ ] Add annotation to automatically add local networks to the proxy

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "pypacer"
version = "0.1.5"
version = "0.1.6"
dynamic = ["dependencies"]
authors = [
{ name = "Heiko Schmidt", email = "73h@gmx.net" },
Expand Down
69 changes: 46 additions & 23 deletions src/examples/unittests.pac
Original file line number Diff line number Diff line change
Expand Up @@ -5,58 +5,81 @@ function FindProxyForURL(url, host) {
*/
host = host.toLowerCase();
if (
localHostOrDomainIs(host, "example.com")
|| localHostOrDomainIs(host, "foo.example.com")
|| localHostOrDomainIs(host, "foo.example.net")
isPlainHostName(host)// rating: 0
) {
/* a proxy for plain hostnames */
return "PROXY plain-hostname.example.com";
}
if (
localHostOrDomainIs(host, "example.com")// rating: 2
|| localHostOrDomainIs(host, "foo.example.com")// rating: 1
|| localHostOrDomainIs(host, "foo.example.net")// rating: 2
) {
/* here domains overlap with the default route */
return "PROXY domain-overlaps.example.com";
}
if (
localHostOrDomainIs(host, "example.net")
|| localHostOrDomainIs(host, "bar.example.com")
|| localHostOrDomainIs(host, "bar.example.net")
localHostOrDomainIs(host, "example.net")// rating: 2
|| localHostOrDomainIs(host, "bar.example.com")// rating: 1
|| localHostOrDomainIs(host, "bar.example.net")// rating: 2
) {
/* a proxy for mixed matches, this should be split up */
return "PROXY mixed.example.com";
}
if (
dnsDomainIs(host, ".example.com")
dnsDomainIs(host, ".example.com")// rating: 4
) {
/* take the direct route */
return "DIRECT";
}
if (
isPlainHostName(host)
host === "10"// rating: 6
|| host.substring(0, 3) === "10."// rating: 8
|| host.substring(host.length - 8) === ".102.123"// rating: 10
) {
/* a proxy for plain hostnames */
return "PROXY plain-hostname.example.com";
/* a proxy for string matches */
return "PROXY string.example.com";
}
if (
host.substring(0, 3) === "10."
|| host.substring(host.length - 8) === ".102.123"
|| host === "10"
dnsResolve(host) === "240.100.50.3"// rating: 11
) {
/* a proxy for string matches */
return "PROXY string.example.com";
/* the ip is within the netmask */
return "PROXY ip-within-netmask-2.example.com";
}
if (
dnsResolve(host) === "192.0.0.170"// rating: 12
|| dnsResolve(host) === "192.0.0.171"// rating: 12
|| dnsResolve(host) === "127.0.0.1"// rating: 12
) {
/* a proxy for IPs */
return "PROXY ip.example.com";
}
if (
isInNet(host, "93.184.0.0", "255.255.0.0")
dnsResolve(host) === "130.131.132.133"// rating: 12
) {
/* a proxy for mixed matches, this should be split up */
return "PROXY mixed.example.com";
}
if (
isInNet(host, "240.100.50.0", "255.255.255.0")// rating: 14
) {
/* the ip is within the netmask */
return "PROXY ip-within-netmask-1.example.com";
}
if (
isInNet(host, "93.184.0.0", "255.255.0.0")// rating: 14
) {
/* a proxy for netmask */
return "PROXY netmask.example.com";
}
if (
dnsResolve(host) === "192.0.0.170"
|| dnsResolve(host) === "192.0.0.171"
|| dnsResolve(host) === "127.0.0.1"
isInNet(host, "240.100.51.0", "255.255.255.0")// rating: 14
) {
/* a proxy for IPs */
return "PROXY ip.example.com";
/* the ip is within the netmask */
return "PROXY ip-within-netmask-2.example.com";
}
if (
isInNet(host, "20.10.10.0", "255.255.255.0")
|| dnsResolve(host) === "130.131.132.133"
isInNet(host, "20.10.10.0", "255.255.255.0")// rating: 14
) {
/* a proxy for mixed matches, this should be split up */
return "PROXY mixed.example.com";
Expand Down
9 changes: 9 additions & 0 deletions src/examples/unittests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ proxies:
- "example.com"
- "foo.example.com"
- "foo.example.net"
- route: "PROXY ip-within-netmask-1.example.com"
description: the ip is within the netmask
targets:
- "240.100.50.0/24"
- route: "PROXY ip-within-netmask-2.example.com"
description: the ip is within the netmask
targets:
- "240.100.50.3"
- "240.100.51.0/24"
- route: "PROXY netmask.example.com"
description: a proxy for netmask
tags:
Expand Down
2 changes: 2 additions & 0 deletions src/pypacer/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from pypacer.pypacer import load_from_yaml, load_from_dict
from .helpers import TargetType
from .pypacer import PyPacer

__all__ = [
"PyPacer",
"load_from_yaml",
"load_from_dict",
"TargetType",
]
33 changes: 21 additions & 12 deletions src/pypacer/helpers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ipaddress
import re
from enum import Enum


def is_ipaddress(ip) -> bool:
Expand Down Expand Up @@ -30,25 +31,33 @@ def is_hostname(hostname):
return False


def get_target_type(target: str) -> str:
class TargetType(Enum):
PLAIN_HOSTNAME = 0
HOST = 2
HOSTS = 4
STRING = 6
STRING_L = 8
STRING_R = 10
IP = 12
NETWORK = 14


def get_target_type(target: str) -> TargetType:
if is_ipaddress(target):
return "IP"
return TargetType.IP
elif is_network(target):
return "NETWORK"
return TargetType.NETWORK
elif is_hostname(target):
if target.startswith("."):
return "HOSTS"
return "HOST"
return TargetType.HOSTS
return TargetType.HOST
else:
if target.startswith("."):
return "STRING_R"
return TargetType.STRING_R
if target.endswith("."):
return "STRING_L"
return "STRING"
return TargetType.STRING_L
return TargetType.STRING


def sort_by_rating(proxy):
rating = 0
for target in proxy.targets:
rating += target.rating
return rating
return min([t.rating for t in proxy.targets])
36 changes: 22 additions & 14 deletions src/pypacer/pypacerconfig.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
import ipaddress
from dataclasses import dataclass, field

from pypacer.helpers import get_target_type
from pypacer.helpers import get_target_type, TargetType


class Target:
def __init__(self, target: str):
self.target = str(target)
self.rating = 0
self.type = get_target_type(self.target)
self.rating = self.type.value
self.netmask = None
if self.type == "NETWORK":
if self.type == TargetType.NETWORK:
self.netmask = ipaddress.ip_network(self.target).with_netmask.split("/")

def recognize_overlaps(self, targets: list):
if self.type == "HOSTS":
if self.type == TargetType.HOSTS:
for target in targets:
if target.type == "HOST" and target.target.endswith(self.target):
target.rating = target.rating - 1
if self.type == "IP":
self.rating = 1
if self.type == "NETWORK":
self.rating = 2
if target.type == TargetType.HOST and target.target.endswith(self.target):
target.rating = target.type.value - 1
if self.type == TargetType.NETWORK:
for target in targets:
if target.type == TargetType.IP:
if ipaddress.ip_address(target.target) in ipaddress.ip_network(self.target):
target.rating = target.type.value - 1


@dataclass
Expand All @@ -35,7 +36,8 @@ def __post_init__(self):
self.targets = [Target(t) for t in self.targets]
if "catch-plain-hostnames" in self.tags:
target = Target("")
target.type = "PLAIN_HOSTNAME"
target.type = TargetType.PLAIN_HOSTNAME
target.rating = target.type.value
self.targets.append(target)


Expand All @@ -56,16 +58,22 @@ def validate(self):

def reorganize_proxies(self):
# dns queries should be at the end. to get this done, proxies with mixed destinations need to be split
i = 0
for index, proxy in enumerate(self.proxies):
i += 1
targets = [t for t in proxy.targets]
nw = ["IP", "NETWORK"]
nw = [TargetType.IP, TargetType.NETWORK]
if any([t.type in nw for t in targets]) and any([t.type not in nw for t in targets]):
new_proxy = Proxy(route=proxy.route, description=proxy.description)
new_proxy.targets = [t for t in targets if t.type in nw]
self.proxies.append(new_proxy)
proxy.targets = [t for t in targets if t.type not in nw]
targets = [t for t in proxy.targets]
if any([t.type == TargetType.NETWORK for t in targets]) and any([t.type == TargetType.IP for t in targets]):
new_proxy = Proxy(route=proxy.route, description=proxy.description)
new_proxy.targets = [t for t in targets if t.type == TargetType.NETWORK]
self.proxies.append(new_proxy)
proxy.targets = [t for t in targets if t.type == TargetType.IP]
for proxy in self.proxies:
proxy.targets.sort(key=lambda x: x.type.value)

def recognize_overlaps(self):
for index, proxy in enumerate(self.proxies):
Expand Down
16 changes: 8 additions & 8 deletions src/pypacer/template.js.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ function FindProxyForURL(url, host) {
if (
{%- for target in proxy.targets %}
{% if loop.index > 1 %}|| {% else %} {% endif -%}
{%- if target.type == "HOST" -%}localHostOrDomainIs(host, "{{ target.target }}")
{%- elif target.type == "HOSTS" -%}dnsDomainIs(host, "{{ target.target }}")
{%- elif target.type == "NETWORK" -%}isInNet(host, "{{ target.netmask[0] }}", "{{ target.netmask[1] }}")
{%- elif target.type == "IP" -%}dnsResolve(host) === "{{ target.target }}"
{%- elif target.type == "STRING_L" -%}host.substring(0, {{ target.target|length }}) === "{{ target.target }}"
{%- elif target.type == "STRING_R" -%}host.substring(host.length - {{ target.target|length }}) === "{{ target.target }}"
{%- elif target.type == "PLAIN_HOSTNAME" -%}isPlainHostName(host)
{%- if target.type.name == "HOST" -%}localHostOrDomainIs(host, "{{ target.target }}")
{%- elif target.type.name == "HOSTS" -%}dnsDomainIs(host, "{{ target.target }}")
{%- elif target.type.name == "NETWORK" -%}isInNet(host, "{{ target.netmask[0] }}", "{{ target.netmask[1] }}")
{%- elif target.type.name == "IP" -%}dnsResolve(host) === "{{ target.target }}"
{%- elif target.type.name == "STRING_L" -%}host.substring(0, {{ target.target|length }}) === "{{ target.target }}"
{%- elif target.type.name == "STRING_R" -%}host.substring(host.length - {{ target.target|length }}) === "{{ target.target }}"
{%- elif target.type.name == "PLAIN_HOSTNAME" -%}isPlainHostName(host)
{%- else -%}host === "{{ target.target }}"
{%- endif -%}
{%- endif -%} // rating: {{ target.rating }}
{%- endfor %}
) {
/* {{ proxy.description }} */
Expand Down
21 changes: 11 additions & 10 deletions src/tests/helpers_test.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import unittest

from pypacer.helpers import get_target_type, sort_by_rating, is_ipaddress, is_network, \
is_hostname
is_hostname, TargetType
from pypacer.pypacerconfig import PyPacerConfig


Expand All @@ -20,26 +20,26 @@ def test_is_network(self):
self.assertTrue(is_network("2001:0db8:85a3:08d3::/64"))

def test_target_type_ip_mask(self):
self.assertEqual(get_target_type("127.0.0.0/24"), "NETWORK")
self.assertEqual(get_target_type("127.0.0.0/24"), TargetType.NETWORK)

def test_target_type_ip(self):
self.assertEqual(get_target_type("127.0.0.1"), "IP")
self.assertEqual(get_target_type("127.0.0.1"), TargetType.IP)

def test_target_type_hosts(self):
self.assertEqual(get_target_type(".example.com"), "HOSTS")
self.assertEqual(get_target_type(".example.com"), TargetType.HOSTS)

def test_target_type_host(self):
self.assertEqual(get_target_type("example.com"), "HOST")
self.assertEqual(get_target_type("example.com."), "HOST")
self.assertEqual(get_target_type("example.com"), TargetType.HOST)
self.assertEqual(get_target_type("example.com."), TargetType.HOST)

def test_target_type_string_l(self):
self.assertEqual(get_target_type("10."), "STRING_L")
self.assertEqual(get_target_type("10."), TargetType.STRING_L)

def test_target_type_string_r(self):
self.assertEqual(get_target_type(".102.123"), "STRING_R")
self.assertEqual(get_target_type(".102.123"), TargetType.STRING_R)

def test_target_type_string(self):
self.assertEqual(get_target_type("10"), "STRING")
self.assertEqual(get_target_type("10"), TargetType.STRING)

def test_is_hostname(self):
self.assertTrue(is_hostname("example.com"))
Expand All @@ -57,6 +57,7 @@ def test_sort_by_rating(self):
config = PyPacerConfig(**config)
config.recognize_overlaps()
self.assertEqual(config.proxies[0].route, "A")
self.assertEqual(config.proxies[1].targets[0].rating, -1)
self.assertEqual(config.proxies[1].targets[0].rating, 1)
self.assertEqual(config.proxies[0].targets[0].rating, 4)
config.proxies.sort(key=sort_by_rating)
self.assertEqual(config.proxies[0].route, "B")

0 comments on commit 66cecef

Please sign in to comment.