Skip to content

Commit 57258c5

Browse files
authored
Merge pull request #236 from SenseUnit/pac_ex
JS: Microsoft's PAC extensions for IPv6
2 parents 3873eb4 + 1746921 commit 57258c5

4 files changed

Lines changed: 141 additions & 1 deletion

File tree

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,15 +459,20 @@ Following builtin functions are addionally available within JS scripts:
459459
* `timeRange(...): boolean` - returns true if current time matches specified time range. See [full documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Proxy_servers_and_tunneling/Proxy_Auto-Configuration_PAC_file#timerange).
460460
* Address manipulation:
461461
* `myIpAddress(): string` - returns the IP address of the machine dumbproxy is running on.
462+
* `myIpAddressEx(): string` - returns semi-colon delimited string containing all IP addresses for localhost (IPv6 and/or IPv4), or an empty string if unable to resolve localhost to an IP address.
462463
* `isPlainHostName(host: string): boolean` - returns true if and only if there is no domain name in the hostname (no dots).
463464
* `dnsDomainIs(host: string, domain: string): boolean` - returns true if and only if the domain of hostname matches.
464465
* `localHostOrDomainIs(host: string, hostDom: string): boolean` - returns true if the hostname matches exactly the specified hostname, or if there is no domain name part in the hostname, but the unqualified hostname matches.
465466
* `dnsDomainLevels(host: string): number` - returns the number (integer) of DNS domain levels (number of dots) in the hostname. Trailing dot representing DNS root is stripped from domain name before counting.
466467
* `convert_addr(IP: string): number | BigInt` - returns numeric representation of IP address or `NaN` if argument can't be converted to an IP address. IPv6 addresses are converted to BigInt.
467468
* `shExpMatch(str: string, shExp: string): boolean` - returns true if the string matches the specified shell glob expression.
468469
* `dnsResolve(host: string): string | null` - resolves the given DNS hostname into an IP address, and returns it in the dot-separated format as a string. **WARNING:** expect heavy performance penalty from use of this function.
469-
* `isResolvable(host: string): boolean` - tries to resolve the hostname. Returns true if succeeds. **WARNING:** expect heavy performance penalty from use of this function.
470+
* `dnsResolveEx(host: string): string` - returns semicolon delimited string containing IPv6 and IPv4 addresses or an empty string if host is not resolvable. **WARNING:** expect heavy performance penalty from use of this function.
471+
* `isResolvable(host: string): boolean` - tries to resolve the hostname to IPv4 address. Returns true if succeeds. **WARNING:** expect heavy performance penalty from use of this function.
472+
* `isResolvableEx(host: string): boolean` - tries to resolve the hostname to IPv4 or IPv6 address. Returns true if succeeds. **WARNING:** expect heavy performance penalty from use of this function.
470473
* `isInNet(host: string, pattern: string, mask: string): boolean` - true if and only if the IP address of the host matches the specified IP address pattern. See also [full documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Proxy_servers_and_tunneling/Proxy_Auto-Configuration_PAC_file#isinnet). **WARNING:** expect heavy performance penalty from use of this function if invoked with domain name as host argument.
474+
* `isInNetEx(host: string, cidrPrefix: string): boolean` - true if and only if the IP address of the host matches the specified IP address prefix. See also [full documentation](https://learn.microsoft.com/en-us/windows/win32/winhttp/isinnetex).
475+
* `sortIpAddressList(host: string): string` - returns list of sorted semi-colon delimited IP addresses or an empty string if unable to sort the IP address list.
471476
* `newStopAddressIteration(): Exception` - create an exception which, once `throw`n, halts further invocations of JS function with different resolved addresses for that request. Useful to cut excess JS calls of access filter scripts which can conclude access denial without looking further into remaining resolved addresses.
472477

473478
Following objects are additionally available in global scope of JS scripts:

dialer/jsrouter.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ func NewJSRouter(filename string, instances int, factory func(string) (Dialer, e
8686
if err != nil {
8787
return fmt.Errorf("can't add ProxyObject.bindings into execution context: %w", err)
8888
}
89+
err = jsext.AddGetClientVersion(vm)
90+
if err != nil {
91+
return fmt.Errorf("can't add getClientVersion function: %w", err)
92+
}
8993
_, err = vm.RunString(string(script))
9094
if err != nil {
9195
return fmt.Errorf("script run failed: %w", err)

jsext/addrutil.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"math/big"
77
"net"
88
"net/netip"
9+
"slices"
10+
"strings"
911
"time"
1012

1113
"github.com/dop251/goja"
@@ -148,3 +150,119 @@ func AddIsInNet(vm *goja.Runtime) error {
148150
return vm.ToValue(r&m == p&m)
149151
})
150152
}
153+
154+
func myIPAddressEx() []netip.Addr {
155+
addrs, err := net.InterfaceAddrs()
156+
if err != nil {
157+
return nil
158+
}
159+
res := make([]netip.Addr, 0, len(addrs))
160+
for _, addr := range addrs {
161+
ipnet, ok := addr.(*net.IPNet)
162+
if !ok {
163+
continue
164+
}
165+
na, ok := netip.AddrFromSlice(ipnet.IP)
166+
if !ok {
167+
continue
168+
}
169+
res = append(res, na.Unmap())
170+
}
171+
return res
172+
}
173+
174+
func AddMyIPAddressEx(vm *goja.Runtime) error {
175+
return vm.GlobalObject().Set("myIpAddressEx", func(call goja.FunctionCall) goja.Value {
176+
res := mapSlice(myIPAddressEx(), func(a netip.Addr) string { return a.String() })
177+
return vm.ToValue(strings.Join(res, ";"))
178+
})
179+
}
180+
181+
func netipAddrCmp(a, b netip.Addr) int {
182+
if a.Is6() == b.Is6() {
183+
if a.Less(b) {
184+
return -1
185+
} else if b.Less(a) {
186+
return 1
187+
}
188+
return 0
189+
} else {
190+
if a.Is6() {
191+
return -1
192+
} else {
193+
return 1
194+
}
195+
}
196+
}
197+
198+
func AddSortIPAddressList(vm *goja.Runtime) error {
199+
return vm.GlobalObject().Set("sortIpAddressList", func(call goja.FunctionCall) goja.Value {
200+
if len(call.Arguments) != 1 {
201+
panic(vm.NewTypeError("sortIpAddressList expects exactly 1 argument"))
202+
}
203+
addrParts := strings.Split(call.Argument(0).String(), ";")
204+
addrs := make([]netip.Addr, 0, len(addrParts))
205+
for _, part := range addrParts {
206+
addr, err := netip.ParseAddr(part)
207+
if err != nil {
208+
return vm.ToValue("")
209+
}
210+
addrs = append(addrs, addr)
211+
}
212+
slices.SortFunc(addrs, netipAddrCmp)
213+
res := mapSlice(addrs, func(a netip.Addr) string { return a.String() })
214+
return vm.ToValue(strings.Join(res, ";"))
215+
})
216+
}
217+
218+
func dnsResolveEx(host string) []netip.Addr {
219+
ctx, cl := context.WithTimeout(context.Background(), 5*time.Second)
220+
defer cl()
221+
// lookup "ip" network for better cache coherence with other lookups,
222+
// even though we actually interested only in IPv4 only
223+
addrs, err := DefaultResolver.LookupNetIP(ctx, "ip", host)
224+
if err != nil {
225+
return nil
226+
}
227+
for i := range addrs {
228+
addrs[i] = addrs[i].Unmap()
229+
}
230+
return addrs
231+
}
232+
233+
func AddDNSResolveEx(vm *goja.Runtime) error {
234+
return vm.GlobalObject().Set("dnsResolveEx", func(call goja.FunctionCall) goja.Value {
235+
if len(call.Arguments) != 1 {
236+
panic(vm.NewTypeError("dnsResolveEx expects exactly 1 argument"))
237+
}
238+
res := mapSlice(dnsResolveEx(call.Argument(0).String()), func(a netip.Addr) string { return a.String() })
239+
return vm.ToValue(strings.Join(res, ";"))
240+
})
241+
}
242+
243+
func AddIsResolvableEx(vm *goja.Runtime) error {
244+
return vm.GlobalObject().Set("isResolvableEx", func(call goja.FunctionCall) goja.Value {
245+
if len(call.Arguments) != 1 {
246+
panic(vm.NewTypeError("isResolvableEx expects exactly 1 argument"))
247+
}
248+
res := dnsResolveEx(call.Argument(0).String())
249+
return vm.ToValue(len(res) > 0)
250+
})
251+
}
252+
253+
func AddIsInNetEx(vm *goja.Runtime) error {
254+
return vm.GlobalObject().Set("isInNetEx", func(call goja.FunctionCall) goja.Value {
255+
if len(call.Arguments) != 2 {
256+
panic(vm.NewTypeError("isInNet expects exactly 2 arguments"))
257+
}
258+
host, err := netip.ParseAddr(call.Argument(0).String())
259+
if err != nil {
260+
return vm.ToValue(false)
261+
}
262+
pfx, err := netip.ParsePrefix(call.Argument(1).String())
263+
if err != nil {
264+
return vm.ToValue(false)
265+
}
266+
return vm.ToValue(pfx.Contains(host))
267+
})
268+
}

jsext/jsext.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,26 @@ func ConfigureRuntime(vm *goja.Runtime) error {
2828
AddDateRange,
2929
AddTimeRange,
3030
AddMyIPAddress,
31+
AddMyIPAddressEx,
3132
AddDNSResolve,
33+
AddDNSResolveEx,
3234
AddIsResolvable,
35+
AddIsResolvableEx,
3336
AddIsInNet,
37+
AddIsInNetEx,
38+
AddSortIPAddressList,
3439
} {
3540
if err := f(vm); err != nil {
3641
return fmt.Errorf("JS runtime init part #%d failed: %w", idx+1, err)
3742
}
3843
}
3944
return nil
4045
}
46+
47+
const PACClientVersion = "1.0"
48+
49+
func AddGetClientVersion(vm *goja.Runtime) error {
50+
return vm.GlobalObject().Set("getClientVersion", func(call goja.FunctionCall) goja.Value {
51+
return vm.ToValue(PACClientVersion)
52+
})
53+
}

0 commit comments

Comments
 (0)