Compare commits
292 Commits
Author | SHA1 | Date |
---|---|---|
Frank Denis | 0059194a9e | |
Frank Denis | 35d7aa0603 | |
Frank Denis | 8dadd61730 | |
dependabot[bot] | f7da81cf29 | |
Frank Denis | 0efce55895 | |
Frank Denis | 5a1d94d506 | |
Frank Denis | 271943c158 | |
Frank Denis | 34a1f2ebf5 | |
Frank Denis | f8ce22d9b9 | |
Frank Denis | 249dba391d | |
Frank Denis | 987ae216e3 | |
Frank Denis | 7fba32651b | |
Frank Denis | 6ae388e646 | |
Frank Denis | 0af88bc875 | |
Frank Denis | d36edeb612 | |
Frank Denis | 041a6c7d7f | |
cuibuwei | 2c6416d5ae | |
Frank Denis | 4d1cd67d4d | |
Frank Denis | 363d44919f | |
Frank Denis | a88076d06f | |
Frank Denis | 119bc0b660 | |
Robert Edmonds | 49000cd4f4 | |
Frank Denis | ec46e09360 | |
dependabot[bot] | ea5808e024 | |
Frank Denis | 79a1aa8325 | |
Alison Winters | 4c442f5dbb | |
Alison Winters | f7e13502c0 | |
YX Hao | 8d43ce9b56 | |
YX Hao | ac5087315c | |
Frank Denis | ad80d81d43 | |
dependabot[bot] | a7fb13ba4e | |
Frank Denis | 006619837f | |
Frank Denis | 093936f7ab | |
Frank Denis | 7462961980 | |
Frank Denis | 0b559bb54f | |
Frank Denis | 658835b4ff | |
Frank Denis | 90c3017793 | |
dependabot[bot] | e371138b86 | |
Frank Denis | bcbf2db4ff | |
YX Hao | 64fa90839c | |
Frank Denis | f2484f5bd5 | |
Frank Denis | 63f8d9b30d | |
Xiaotong Liu | 49e3570c2c | |
lifenjoiner | 3be53642fe | |
YX Hao | 13e7077200 | |
Frank Denis | f5912d7ca9 | |
dependabot[bot] | 0196d7d2ab | |
Frank Denis | 898ded9c52 | |
Frank Denis | e782207911 | |
Frank Denis | 0f1f635ec1 | |
keatonLiu | 956a14ee21 | |
dependabot[bot] | 22731786a2 | |
Frank Denis | 42b6ae9052 | |
YX Hao | 0d5e52bb16 | |
Frank Denis | 0ba728b6ce | |
Frank Denis | cb80bf33e8 | |
Frank Denis | 88207560a7 | |
Jeffrey Damick | 4a361dbb05 | |
Frank Denis | b37a5c991a | |
Frank Denis | 0232870870 | |
Frank Denis | 1a9bf8a286 | |
Frank Denis | 7fb58720fb | |
Frank Denis | f85b3e81ec | |
Frank Denis | 79779cf744 | |
Frank Denis | 8bea679e7b | |
Frank Denis | 96f21f1bff | |
dependabot[bot] | 21097686c1 | |
Frank Denis | 87571d4a7f | |
Frank Denis | f531c8fffb | |
Frank Denis | 5ae83c1592 | |
Frank Denis | c86e9a90cc | |
Frank Denis | d48c811ea9 | |
Frank Denis | f2b1edcec2 | |
Frank Denis | 1b65fe62b0 | |
Frank Denis | 194752e829 | |
Frank Denis | 808f2dfa0e | |
Frank Denis | 7dd79d5f96 | |
Frank Denis | 5088d8fae1 | |
Frank Denis | aff09648bb | |
Frank Denis | 7bca9a6c0a | |
Frank Denis | 98d0938815 | |
Frank Denis | 50780421a8 | |
RadhaKrishna | be7d5d1277 | |
Frank Denis | c3dd761b81 | |
Frank Denis | d8aec47a72 | |
Frank Denis | cfd6ced134 | |
Frank Denis | bdf27330c9 | |
Frank Denis | a108d048d8 | |
Frank Denis | afcfd566c9 | |
Frank Denis | ce55d1c5bb | |
Frank Denis | 2481fbebd7 | |
Frank Denis | 32aad7bb34 | |
Frank Denis | 7033f242c0 | |
Frank Denis | 2675d73b13 | |
Frank Denis | 5085a22903 | |
Frank Denis | 7cc5a051c7 | |
Frank Denis | 894d20191f | |
Frank Denis | 0a98be94a7 | |
Frank Denis | 1792c06bc7 | |
Expertcoderz | 63e414021b | |
Frank Denis | d659a801c2 | |
Frank Denis | a4eda39563 | |
Expertcoderz | 4114f032c3 | |
Frank Denis | a352a3035c | |
Frank Denis | 60684f8ee4 | |
YX Hao | be369a1f7a | |
YX Hao | 89ccc59f0e | |
Frank Denis | 16b2c84147 | |
Carlo Teubner | b46775ae0c | |
Frank Denis | cef4b041d7 | |
Carlo Teubner | d8b1f4e7cd | |
Frank Denis | 23a6cd7504 | |
Frank Denis | f42b7dad17 | |
Frank Denis | 4f3ce0cbae | |
Frank Denis | 0f1e3b4ba8 | |
Frank Denis | 62ef5c9d02 | |
Frank Denis | f9f68cf0a3 | |
Frank Denis | 0c26d1637a | |
Frank Denis | 9f86ffdd1e | |
lifenjoiner | 9b2c674744 | |
Frank Denis | d381af5510 | |
Frank Denis | c66023c7d7 | |
Frank Denis | 5b8e7d4114 | |
KOLANICH | f4007f709d | |
lifenjoiner | dd1c066724 | |
lifenjoiner | 5d551e54ce | |
Thad Guidry | fbc7817366 | |
Frank Denis | 9b61b73852 | |
Frank Denis | af6340df09 | |
Frank Denis | 9c73ab3070 | |
Frank Denis | ea3625bcfd | |
Frank Denis | f567f57150 | |
Frank Denis | c03f1a31eb | |
Frank Denis | c3c51bb435 | |
Frank Denis | 0f30b3b028 | |
lifenjoiner | 6d826afac5 | |
Frank Denis | b341c21dbd | |
Frank Denis | 92ed5b95e0 | |
Frank Denis | b898e07066 | |
dependabot[bot] | 92063aa76d | |
dependabot[bot] | 4be5264529 | |
Frank Denis | 13d78c042b | |
Frank Denis | 36c17eb59a | |
Frank Denis | b9f8f78c6e | |
Frank Denis | fc16e3c31c | |
lifenjoiner | b3318a94b7 | |
Frank Denis | ca0f353087 | |
Frank Denis | cf7d60a704 | |
Frank Denis | a47f7fe750 | |
Frank Denis | beb002335f | |
Frank Denis | 15c87a68a1 | |
Frank Denis | 47e6a56b16 | |
Frank Denis | 03c6f92a5f | |
lifenjoiner | 24a301b1af | |
lifenjoiner | a8d1c2fd24 | |
Frank Denis | 96ffb21228 | |
Frank Denis | acc25fcefb | |
Frank Denis | 07b4ec33c5 | |
Frank Denis | 9f3ef735f2 | |
Frank Denis | d568e43937 | |
Frank Denis | 68f3ab249c | |
dependabot[bot] | 2edfdc48b8 | |
Frank Denis | 3f31c4d3e2 | |
Frank Denis | 84184bbad8 | |
Frank Denis | f630094e8d | |
lifenjoiner | 3517dec376 | |
lifenjoiner | 683aad75da | |
lifenjoiner | e1c7ea1770 | |
Frank Denis | 470460f069 | |
Frank Denis | 8694753866 | |
Frank Denis | b4b58366cc | |
Frank Denis | f7df72eafa | |
Frank Denis | fb15535282 | |
Frank Denis | c32aad3dfd | |
Frank Denis | 9e208e6090 | |
Frank Denis | 5f88a9146c | |
Frank Denis | 3f23ff5c08 | |
Frank Denis | 33c8027e0a | |
Frank Denis | 11e824bd13 | |
Deltadroid | c3fd855831 | |
Frank Denis | 5438eed2f4 | |
Frank Denis | a868e2b306 | |
Frank Denis | f21eca0764 | |
Frank Denis | c883949a97 | |
Frank Denis | e13b4842e8 | |
dependabot[bot] | 5705b60936 | |
Frank Denis | 5f4dfc5d6e | |
Frank Denis | a89d96144f | |
Frank Denis | 4aa415de6e | |
Frank Denis | f4389559ef | |
Frank Denis | 361455cd58 | |
cobratbq | 77059ce450 | |
Frank Denis | 09a6918226 | |
Frank Denis | c748630691 | |
Frank Denis | 94cba8cf78 | |
Maurizio Pasquinelli | ca253923eb | |
Frank Denis | 08d44241b9 | |
lifenjoiner | 4881186dcf | |
Frank Denis | 41f192a907 | |
Frank Denis | 937c1e63e2 | |
Frank Denis | e124623ffc | |
lifenjoiner | 55fc4c207b | |
Ian Bashford | baee50f1dc | |
Frank Denis | 6e1bc06477 | |
Frank Denis | 8523a92437 | |
Frank Denis | 442f2e15cb | |
Frank Denis | 0c88e2a1a0 | |
Frank Denis | 35063a1eec | |
Frank Denis | 07266e4d4f | |
Frank Denis | 5977de660b | |
lifenjoiner | 91388b148c | |
lifenjoiner | 8e46f44799 | |
Frank Denis | 3d641b758a | |
Frank Denis | 49ea894ce8 | |
lifenjoiner | 568f54fabb | |
pc-v2 | dc2fff05be | |
Frank Denis | 38e87f9a7b | |
lifenjoiner | 0e2bb13254 | |
Frank Denis | 59ce17e0ab | |
Frank Denis | ee5c9d67a4 | |
Frank Denis | 8c43118b03 | |
ignoramous | 7177a0ec74 | |
lifenjoiner | 72a602577a | |
lifenjoiner | 0a0b69d93d | |
lifenjoiner | 6916c047e1 | |
ignoramous | 8d737a69f5 | |
Frank Denis | 866954fbad | |
Frank Denis | e477d0e126 | |
Frank Denis | e24fdd2235 | |
livingentity | 74fb5dabb9 | |
Frank Denis | 1afd573b0d | |
Frank Denis | c367a82ac0 | |
dependabot[bot] | 9c8c327703 | |
livingentity | 207d44323d | |
Frank Denis | 3eac156789 | |
Frank Denis | 5fca7ea49e | |
Frank Denis | 77dc3b1e85 | |
Frank Denis | 66f019d886 | |
livingentity | f67e9cab32 | |
Frank Denis | 5d023d2a7c | |
Frank Denis | e931b234b7 | |
quindecim | ed2c880648 | |
Frank Denis | c467e20311 | |
Frank Denis | df3fb0c9f8 | |
Frank Denis | c0435772d4 | |
Frank Denis | 49c17f8e98 | |
Frank Denis | 0465cd35ef | |
dependabot[bot] | 08420917a5 | |
livingentity | 87d9653ec2 | |
BigDargon | d30c44a6a8 | |
dependabot[bot] | 911108149b | |
dependabot[bot] | a8aa4cb8e6 | |
Frank Denis | ca076ce133 | |
Frank Denis | 034d3bd424 | |
Frank Denis | c08852feb1 | |
Frank Denis | 9373cc7162 | |
Frank Denis | cb140673fa | |
Frank Denis | 7956ba5b10 | |
livingentity | 9ec8a35468 | |
livingentity | ac6abfb985 | |
quindecim | a20d1685b2 | |
livingentity | 62092726ec | |
Frank Denis | f38a5463b0 | |
quindecim | 7a54406415 | |
quindecim | bce0405c0a | |
quindecim | 29a3442306 | |
Frank Denis | 8ed98cacae | |
Peter Dave Hello | ef1c70e87d | |
Frank Denis | 4c67e790f6 | |
Frank Denis | 4eeed5816f | |
Frank Denis | c10e6e0635 | |
Frank Denis | e6089449b6 | |
mibere | 706c1ab286 | |
Frank Denis | f7e3381650 | |
cobratbq | 7a8bd35009 | |
Frank Denis | 06733f57ed | |
Frank Denis | 4fd26029c7 | |
Frank Denis | 351bced7c5 | |
Frank Denis | 916e84e798 | |
ValdikSS | 53f3a0e63d | |
Frank Denis | 4e9f0382ee | |
dependabot[bot] | 9121f4f359 | |
Frank Denis | e73459f558 | |
Frank Denis | fbfc2d57a7 | |
Frank Denis | b9d6b22ce1 | |
Ian Bashford | 1b6caba307 | |
CNMan | 27e93a53cf | |
Frank Denis | 9e7221c31c | |
Frank Denis | f6f63743ce | |
Frank Denis | 561e849889 | |
a1346054 | 766e149699 | |
Frank Denis | e2ada45598 |
|
@ -23,7 +23,7 @@ ln ../windows/* win64/
|
||||||
zip -9 -r dnscrypt-proxy-win64-${PACKAGE_VERSION:-dev}.zip win64
|
zip -9 -r dnscrypt-proxy-win64-${PACKAGE_VERSION:-dev}.zip win64
|
||||||
|
|
||||||
go clean
|
go clean
|
||||||
env GO386=387 GOOS=openbsd GOARCH=386 go build -mod vendor -ldflags="-s -w"
|
env GO386=softfloat GOOS=openbsd GOARCH=386 go build -mod vendor -ldflags="-s -w"
|
||||||
mkdir openbsd-i386
|
mkdir openbsd-i386
|
||||||
ln dnscrypt-proxy openbsd-i386/
|
ln dnscrypt-proxy openbsd-i386/
|
||||||
ln ../LICENSE example-dnscrypt-proxy.toml localhost.pem example-*.txt openbsd-i386/
|
ln ../LICENSE example-dnscrypt-proxy.toml localhost.pem example-*.txt openbsd-i386/
|
||||||
|
@ -141,6 +141,13 @@ ln dnscrypt-proxy linux-mips64le/
|
||||||
ln ../LICENSE example-dnscrypt-proxy.toml localhost.pem example-*.txt linux-mips64le/
|
ln ../LICENSE example-dnscrypt-proxy.toml localhost.pem example-*.txt linux-mips64le/
|
||||||
tar czpvf dnscrypt-proxy-linux_mips64le-${PACKAGE_VERSION:-dev}.tar.gz linux-mips64le
|
tar czpvf dnscrypt-proxy-linux_mips64le-${PACKAGE_VERSION:-dev}.tar.gz linux-mips64le
|
||||||
|
|
||||||
|
go clean
|
||||||
|
env CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -mod vendor -ldflags="-s -w"
|
||||||
|
mkdir linux-riscv64
|
||||||
|
ln dnscrypt-proxy linux-riscv64/
|
||||||
|
ln ../LICENSE example-dnscrypt-proxy.toml localhost.pem example-*.txt linux-riscv64/
|
||||||
|
tar czpvf dnscrypt-proxy-linux_riscv64-${PACKAGE_VERSION:-dev}.tar.gz linux-riscv64
|
||||||
|
|
||||||
go clean
|
go clean
|
||||||
env GOOS=darwin GOARCH=amd64 go build -mod vendor -ldflags="-s -w"
|
env GOOS=darwin GOARCH=amd64 go build -mod vendor -ldflags="-s -w"
|
||||||
mkdir macos-x86_64
|
mkdir macos-x86_64
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
#! /bin/sh
|
||||||
|
|
||||||
|
PACKAGE_VERSION="$1"
|
||||||
|
|
||||||
|
cd dnscrypt-proxy || exit 1
|
||||||
|
|
||||||
|
# setup the environment
|
||||||
|
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y wget wine dotnet-sdk-6.0
|
||||||
|
sudo dpkg --add-architecture i386 && sudo apt-get update && sudo apt-get install -y wine32
|
||||||
|
|
||||||
|
sudo apt-get install -y unzip
|
||||||
|
|
||||||
|
export WINEPREFIX="$HOME"/.wine32
|
||||||
|
export WINEARCH=win32
|
||||||
|
export WINEDEBUG=-all
|
||||||
|
|
||||||
|
wget https://dl.winehq.org/wine/wine-mono/8.1.0/wine-mono-8.1.0-x86.msi
|
||||||
|
WINEPREFIX="$HOME/.wine32" WINEARCH=win32 wineboot --init
|
||||||
|
WINEPREFIX="$HOME/.wine32" WINEARCH=win32 wine msiexec /i wine-mono-8.1.0-x86.msi
|
||||||
|
|
||||||
|
mkdir "$HOME"/.wine32/drive_c/temp
|
||||||
|
mkdir -p "$HOME"/.wine/drive_c/temp
|
||||||
|
wget https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip -nv -O wix.zip
|
||||||
|
|
||||||
|
unzip wix.zip -d "$HOME"/wix
|
||||||
|
rm -f wix.zip
|
||||||
|
|
||||||
|
builddir=$(pwd)
|
||||||
|
srcdir=$(
|
||||||
|
cd ..
|
||||||
|
pwd
|
||||||
|
)
|
||||||
|
version=$PACKAGE_VERSION
|
||||||
|
|
||||||
|
cd "$HOME"/wix || exit
|
||||||
|
|
||||||
|
ln -s "$builddir" "$HOME"/wix/build
|
||||||
|
ln -s "$srcdir"/contrib/msi "$HOME"/wix/wixproj
|
||||||
|
echo "builddir: $builddir"
|
||||||
|
|
||||||
|
# build the msi's
|
||||||
|
#################
|
||||||
|
for arch in x64 x86; do
|
||||||
|
binpath="win32"
|
||||||
|
if [ "$arch" = "x64" ]; then
|
||||||
|
binpath="win64"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo $arch
|
||||||
|
|
||||||
|
wine candle.exe -dVersion="$version" -dPlatform=$arch -dPath=build\\$binpath -arch $arch wixproj\\dnscrypt.wxs -out build\\dnscrypt-$arch.wixobj
|
||||||
|
wine light.exe -out build\\dnscrypt-proxy-$arch-"$version".msi build\\dnscrypt-$arch.wixobj -sval
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
cd "$builddir" || exit
|
|
@ -66,10 +66,15 @@ t || dig -p${DNS_PORT} +dnssec darpa.mil @127.0.0.1 2>&1 | grep -Fvq 'RRSIG' ||
|
||||||
t || dig -p${DNS_PORT} +dnssec www.darpa.mil @127.0.0.1 2>&1 | grep -Fvq 'RRSIG' || fail
|
t || dig -p${DNS_PORT} +dnssec www.darpa.mil @127.0.0.1 2>&1 | grep -Fvq 'RRSIG' || fail
|
||||||
|
|
||||||
section
|
section
|
||||||
t || dig -p${DNS_PORT} +short cloaked.com @127.0.0.1 | grep -Eq '1.1.1.1|1.0.0.1' || fail
|
t || dig -p${DNS_PORT} +short cloakedunregistered.com @127.0.0.1 | grep -Eq '1.1.1.1|1.0.0.1' || fail
|
||||||
t || dig -p${DNS_PORT} +short www.cloaked2.com @127.0.0.1 | grep -Eq '1.1.1.1|1.0.0.1' || fail
|
t || dig -p${DNS_PORT} +short MX cloakedunregistered.com @127.0.0.1 | grep -Fq 'locally blocked' || fail
|
||||||
|
t || dig -p${DNS_PORT} +short MX example.com @127.0.0.1 | grep -Fvq 'locally blocked' || fail
|
||||||
|
t || dig -p${DNS_PORT} NS cloakedunregistered.com @127.0.0.1 | grep -Fiq 'gtld-servers.net' || fail
|
||||||
|
t || dig -p${DNS_PORT} +short www.cloakedunregistered2.com @127.0.0.1 | grep -Eq '1.1.1.1|1.0.0.1' || fail
|
||||||
t || dig -p${DNS_PORT} +short www.dnscrypt-test @127.0.0.1 | grep -Fq '192.168.100.100' || fail
|
t || dig -p${DNS_PORT} +short www.dnscrypt-test @127.0.0.1 | grep -Fq '192.168.100.100' || fail
|
||||||
t || dig -p${DNS_PORT} a.www.dnscrypt-test @127.0.0.1 | grep -Fq 'NXDOMAIN' || fail
|
t || dig -p${DNS_PORT} a.www.dnscrypt-test @127.0.0.1 | grep -Fq 'NXDOMAIN' || fail
|
||||||
|
t || dig -p${DNS_PORT} +short ptr 101.100.168.192.in-addr.arpa. @127.0.0.1 | grep -Eq 'www.dnscrypt-test.com' || fail
|
||||||
|
t || dig -p${DNS_PORT} +short ptr 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.2.0.d.f.ip6.arpa. @127.0.0.1 | grep -Eq 'ipv6.dnscrypt-test.com' || fail
|
||||||
|
|
||||||
section
|
section
|
||||||
t || dig -p${DNS_PORT} telemetry.example @127.0.0.1 | grep -Fq 'locally blocked' || fail
|
t || dig -p${DNS_PORT} telemetry.example @127.0.0.1 | grep -Fq 'locally blocked' || fail
|
||||||
|
@ -117,8 +122,8 @@ t || grep -Eq 'invalid.*SYNTH' query.log || fail
|
||||||
t || grep -Eq '168.192.in-addr.arpa.*SYNTH' query.log || fail
|
t || grep -Eq '168.192.in-addr.arpa.*SYNTH' query.log || fail
|
||||||
t || grep -Eq 'darpa.mil.*FORWARD' query.log || fail
|
t || grep -Eq 'darpa.mil.*FORWARD' query.log || fail
|
||||||
t || grep -Eq 'www.darpa.mil.*FORWARD' query.log || fail
|
t || grep -Eq 'www.darpa.mil.*FORWARD' query.log || fail
|
||||||
t || grep -Eq 'cloaked.com.*CLOAK' query.log || fail
|
t || grep -Eq 'cloakedunregistered.com.*CLOAK' query.log || fail
|
||||||
t || grep -Eq 'www.cloaked2.com.*CLOAK' query.log || fail
|
t || grep -Eq 'www.cloakedunregistered2.com.*CLOAK' query.log || fail
|
||||||
t || grep -Eq 'www.dnscrypt-test.*CLOAK' query.log || fail
|
t || grep -Eq 'www.dnscrypt-test.*CLOAK' query.log || fail
|
||||||
t || grep -Eq 'a.www.dnscrypt-test.*NXDOMAIN' query.log || fail
|
t || grep -Eq 'a.www.dnscrypt-test.*NXDOMAIN' query.log || fail
|
||||||
t || grep -Eq 'telemetry.example.*REJECT' query.log || fail
|
t || grep -Eq 'telemetry.example.*REJECT' query.log || fail
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
cloaked.* one.one.one.one
|
cloakedunregistered.* one.one.one.one
|
||||||
*.cloaked2.* one.one.one.one # inline comment
|
*.cloakedunregistered2.* one.one.one.one # inline comment
|
||||||
=www.dnscrypt-test 192.168.100.100
|
=www.dnscrypt-test 192.168.100.100
|
||||||
|
=www.dnscrypt-test.com 192.168.100.101
|
||||||
|
=ipv6.dnscrypt-test.com fd02::1
|
||||||
|
|
|
@ -9,7 +9,7 @@ file = 'query.log'
|
||||||
stamp = 'sdns://BQcAAAAAAAAADm9kb2guY3J5cHRvLnN4Ci9kbnMtcXVlcnk'
|
stamp = 'sdns://BQcAAAAAAAAADm9kb2guY3J5cHRvLnN4Ci9kbnMtcXVlcnk'
|
||||||
|
|
||||||
[static.'odohrelay']
|
[static.'odohrelay']
|
||||||
stamp = 'sdns://hQcAAAAAAAAAACCi3jNJDEdtNW4tvHN8J3lpIklSa2Wrj7qaNCgEgci9_BpvZG9oLXJlbGF5LmVkZ2Vjb21wdXRlLmFwcAEv'
|
stamp = 'sdns://hQcAAAAAAAAADDg5LjM4LjEzMS4zOAAYb2RvaC1ubC5hbGVrYmVyZy5uZXQ6NDQzBi9wcm94eQ'
|
||||||
|
|
||||||
[anonymized_dns]
|
[anonymized_dns]
|
||||||
routes = [
|
routes = [
|
||||||
|
|
|
@ -10,6 +10,7 @@ block_unqualified = true
|
||||||
block_undelegated = true
|
block_undelegated = true
|
||||||
forwarding_rules = 'forwarding-rules.txt'
|
forwarding_rules = 'forwarding-rules.txt'
|
||||||
cloaking_rules = 'cloaking-rules.txt'
|
cloaking_rules = 'cloaking-rules.txt'
|
||||||
|
cloak_ptr = true
|
||||||
cache = true
|
cache = true
|
||||||
|
|
||||||
[local_doh]
|
[local_doh]
|
||||||
|
|
|
@ -13,9 +13,7 @@ cache = true
|
||||||
[query_log]
|
[query_log]
|
||||||
file = 'query.log'
|
file = 'query.log'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[static]
|
[static]
|
||||||
|
|
||||||
[static.'myserver']
|
[static.'myserver']
|
||||||
stamp = 'sdns://AQcAAAAAAAAADjIxMi40Ny4yMjguMTM2IOgBuE6mBr-wusDOQ0RbsV66ZLAvo8SqMa4QY2oHkDJNHzIuZG5zY3J5cHQtY2VydC5mci5kbnNjcnlwdC5vcmc'
|
stamp = 'sdns://AQcAAAAAAAAADjIxMi40Ny4yMjguMTM2IOgBuE6mBr-wusDOQ0RbsV66ZLAvo8SqMa4QY2oHkDJNHzIuZG5zY3J5cHQtY2VydC5mci5kbnNjcnlwdC5vcmc'
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
name: Autocloser
|
||||||
|
on: [issues]
|
||||||
|
jobs:
|
||||||
|
autoclose:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Autoclose issues that did not follow issue template
|
||||||
|
uses: roots/issue-closer@v1.2
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
issue-close-message: "This issue was automatically closed because it did not follow the issue template. We use the issue tracker exclusively for bug reports and feature additions that have been previously discussed. However, this issue appears to be a support request. Please use the discussion forums for support requests."
|
||||||
|
issue-pattern: ".*(do we replicate the issue|Expected behavior|raised as discussion|# Impact).*"
|
|
@ -13,15 +13,20 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: 'go.mod'
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v3
|
||||||
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v1
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@v3
|
||||||
|
|
|
@ -25,21 +25,21 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Get the version
|
- name: Get the version
|
||||||
id: get_version
|
id: get_version
|
||||||
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
|
run: echo "VERSION=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v2.1.4
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1
|
go-version: 1
|
||||||
|
check-latest: true
|
||||||
id: go
|
id: go
|
||||||
|
|
||||||
- name: Check out code into the Go module directory
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Test suite
|
- name: Test suite
|
||||||
run: |
|
run: |
|
||||||
go version
|
go version
|
||||||
go mod vendor
|
|
||||||
cd .ci
|
cd .ci
|
||||||
./ci-test.sh
|
./ci-test.sh
|
||||||
cd -
|
cd -
|
||||||
|
@ -49,6 +49,11 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
.ci/ci-build.sh "${{ steps.get_version.outputs.VERSION }}"
|
.ci/ci-build.sh "${{ steps.get_version.outputs.VERSION }}"
|
||||||
|
|
||||||
|
- name: Package
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
run: |
|
||||||
|
.ci/ci-package.sh "${{ steps.get_version.outputs.VERSION }}"
|
||||||
|
|
||||||
- name: Install minisign and sign
|
- name: Install minisign and sign
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
run: |
|
run: |
|
||||||
|
@ -78,7 +83,7 @@ jobs:
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
|
||||||
- name: Upload release assets
|
- name: Upload release assets
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@69320dbe05506a9a39fc8ae11030b214ec2d1f87
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
@ -87,3 +92,4 @@ jobs:
|
||||||
dnscrypt-proxy/*.zip
|
dnscrypt-proxy/*.zip
|
||||||
dnscrypt-proxy/*.tar.gz
|
dnscrypt-proxy/*.tar.gz
|
||||||
dnscrypt-proxy/*.minisig
|
dnscrypt-proxy/*.minisig
|
||||||
|
dnscrypt-proxy/*.msi
|
||||||
|
|
|
@ -6,7 +6,7 @@ jobs:
|
||||||
Scan-Build:
|
Scan-Build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Perform ShiftLeft Scan
|
- name: Perform ShiftLeft Scan
|
||||||
uses: ShiftLeftSecurity/scan-action@master
|
uses: ShiftLeftSecurity/scan-action@master
|
||||||
|
@ -18,6 +18,6 @@ jobs:
|
||||||
output: reports
|
output: reports
|
||||||
|
|
||||||
- name: Upload report
|
- name: Upload report
|
||||||
uses: github/codeql-action/upload-sarif@v1
|
uses: github/codeql-action/upload-sarif@v3
|
||||||
with:
|
with:
|
||||||
sarif_file: reports
|
sarif_file: reports
|
||||||
|
|
|
@ -14,3 +14,6 @@ dnscrypt-proxy/dnscrypt-proxy
|
||||||
.ci/*.md
|
.ci/*.md
|
||||||
.ci/*.md.minisig
|
.ci/*.md.minisig
|
||||||
.ci/test-dnscrypt-proxy.toml
|
.ci/test-dnscrypt-proxy.toml
|
||||||
|
contrib/msi/*.msi
|
||||||
|
contrib/msi/*.wixpdb
|
||||||
|
contrib/msi/*.wixobj
|
||||||
|
|
55
ChangeLog
55
ChangeLog
|
@ -1,3 +1,58 @@
|
||||||
|
# Version 2.1.5
|
||||||
|
- dnscrypt-proxy can be compiled with Go 1.21.0+
|
||||||
|
- Responses to blocked queries now include extended error codes
|
||||||
|
- Reliability of connections using HTTP/3 has been improved
|
||||||
|
- New configuration directive: `tls_key_log_file`. When defined, this
|
||||||
|
is the path to a file where TLS secret keys will be written to, so
|
||||||
|
that DoH traffic can be locally inspected.
|
||||||
|
|
||||||
|
# Version 2.1.4
|
||||||
|
- Fixes a regression from version 2.1.3: when cloaking was enabled,
|
||||||
|
blocked responses were returned for records that were not A/AAAA/PTR
|
||||||
|
even for names that were not in the cloaked list.
|
||||||
|
|
||||||
|
# Version 2.1.3
|
||||||
|
- DNS-over-HTTP/3 (QUIC) should be more reliable. In particular,
|
||||||
|
version 2.1.2 required another (non-QUIC) resolver to be present for
|
||||||
|
bootstrapping, or the resolver's IP address to be present in the
|
||||||
|
stamp. This is not the case any more.
|
||||||
|
- dnscrypt-proxy is now compatible with Go 1.20+
|
||||||
|
- Commands (-check, -show-certs, -list, -list-all) now ignore log
|
||||||
|
files and directly output the result to the standard output.
|
||||||
|
- The `cert_ignore_timestamp` configuration switch is now documented.
|
||||||
|
It allows ignoring timestamps for DNSCrypt certificate verification,
|
||||||
|
until a first server is available. This should only be used on devices
|
||||||
|
that don't have any ways to set the clock before DNS service is up.
|
||||||
|
However, a safer alternative remains to use an NTP server with a fixed
|
||||||
|
IP address (such as time.google.com), configured in the captive portals
|
||||||
|
file.
|
||||||
|
- Cloaking: when a name is cloaked, unsupported record types now
|
||||||
|
return a blocked response rather than the actual records.
|
||||||
|
- systemd: report Ready earlier as dnscrypt-proxy can itself manage
|
||||||
|
retries for updates/refreshes.
|
||||||
|
|
||||||
|
# Version 2.1.2
|
||||||
|
- Support for DoH over HTTP/3 (DoH3, HTTP over QUIC) has been added.
|
||||||
|
Compatible servers will automatically use it. Note that QUIC uses UDP
|
||||||
|
(usually over port 443, like DNSCrypt) instead of TCP.
|
||||||
|
- In previous versions, memory usage kept growing due to channels not
|
||||||
|
being properly closed, causing goroutines to pile up. This was fixed,
|
||||||
|
resulting in an important reduction of memory usage. Thanks to
|
||||||
|
@lifenjoiner for investigating and fixing this!
|
||||||
|
- DNS64: `CNAME` records are now translated like other responses.
|
||||||
|
Thanks to @ignoramous for this!
|
||||||
|
- A relay whose name has been configured, but doesn't exist in the
|
||||||
|
list of available relays is now a hard error. Thanks to @lifenjoiner!
|
||||||
|
- Mutexes/locking: bug fixes and improvements, by @ignoramous
|
||||||
|
- Official packages now include linux/riscv64 builds.
|
||||||
|
- `dnscrypt-proxy -resolve` now reports if ECS (EDNS-clientsubnet) is
|
||||||
|
supported by the server.
|
||||||
|
- `dnscrypt-proxy -list` now includes ODoH (Oblivious DoH) servers.
|
||||||
|
- Local DoH: queries made using the `GET` method are now handled.
|
||||||
|
- The service can now be installed on OpenRC-based systems.
|
||||||
|
- `PTR` queries are now supported for cloaked domains. Contributed by
|
||||||
|
Ian Bashford, thanks!
|
||||||
|
|
||||||
# Version 2.1.1
|
# Version 2.1.1
|
||||||
This is a bugfix only release, addressing regressions introduced in
|
This is a bugfix only release, addressing regressions introduced in
|
||||||
version 2.1.0:
|
version 2.1.0:
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
ISC License
|
ISC License
|
||||||
|
|
||||||
Copyright (c) 2018-2021, Frank Denis <j at pureftpd dot org>
|
Copyright (c) 2018-2023, Frank Denis <j at pureftpd dot org>
|
||||||
|
|
||||||
Permission to use, copy, modify, and/or distribute this software for any
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
purpose with or without fee is hereby granted, provided that the above
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
|
11
README.md
11
README.md
|
@ -2,14 +2,14 @@
|
||||||
|
|
||||||
[![Financial Contributors on Open Collective](https://opencollective.com/dnscrypt/all/badge.svg?label=financial+contributors)](https://opencollective.com/dnscrypt)
|
[![Financial Contributors on Open Collective](https://opencollective.com/dnscrypt/all/badge.svg?label=financial+contributors)](https://opencollective.com/dnscrypt)
|
||||||
[![DNSCrypt-Proxy Release](https://img.shields.io/github/release/dnscrypt/dnscrypt-proxy.svg?label=Latest%20Release&style=popout)](https://github.com/dnscrypt/dnscrypt-proxy/releases/latest)
|
[![DNSCrypt-Proxy Release](https://img.shields.io/github/release/dnscrypt/dnscrypt-proxy.svg?label=Latest%20Release&style=popout)](https://github.com/dnscrypt/dnscrypt-proxy/releases/latest)
|
||||||
[![Build Status](https://github.com/DNSCrypt/dnscrypt-proxy/workflows/CI%20and%20optionally%20publish/badge.svg)](https://github.com/DNSCrypt/dnscrypt-proxy/actions)
|
[![Build Status](https://github.com/DNSCrypt/dnscrypt-proxy/actions/workflows/releases.yml/badge.svg)](https://github.com/DNSCrypt/dnscrypt-proxy/actions/workflows/releases.yml)
|
||||||
![CodeQL scan](https://github.com/DNSCrypt/dnscrypt-proxy/workflows/CodeQL%20scan/badge.svg)
|
![CodeQL scan](https://github.com/DNSCrypt/dnscrypt-proxy/workflows/CodeQL%20scan/badge.svg)
|
||||||
![ShiftLeft Scan](https://github.com/DNSCrypt/dnscrypt-proxy/workflows/ShiftLeft%20Scan/badge.svg)
|
![ShiftLeft Scan](https://github.com/DNSCrypt/dnscrypt-proxy/workflows/ShiftLeft%20Scan/badge.svg)
|
||||||
[![#dnscrypt-proxy:matrix.org](https://img.shields.io/matrix/dnscrypt-proxy:matrix.org.svg?label=DNSCrypt-Proxy%20Matrix%20Chat&server_fqdn=matrix.org&style=popout)](https://matrix.to/#/#dnscrypt-proxy:matrix.org)
|
[![#dnscrypt-proxy:matrix.org](https://img.shields.io/matrix/dnscrypt-proxy:matrix.org.svg?label=DNSCrypt-Proxy%20Matrix%20Chat&server_fqdn=matrix.org&style=popout)](https://matrix.to/#/#dnscrypt-proxy:matrix.org)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
A flexible DNS proxy, with support for modern encrypted DNS protocols such as [DNSCrypt v2](https://dnscrypt.info/protocol), [DNS-over-HTTPS](https://www.rfc-editor.org/rfc/rfc8484.txt), [Anonymized DNSCrypt](https://github.com/DNSCrypt/dnscrypt-protocol/blob/master/ANONYMIZED-DNSCRYPT.txt) and [ODoH (Oblivious DoH)](https://github.com/DNSCrypt/dnscrypt-resolvers/blob/master/v3/odoh.md).
|
A flexible DNS proxy, with support for modern encrypted DNS protocols such as [DNSCrypt v2](https://dnscrypt.info/protocol), [DNS-over-HTTPS](https://www.rfc-editor.org/rfc/rfc8484.txt), [Anonymized DNSCrypt](https://github.com/DNSCrypt/dnscrypt-protocol/blob/master/ANONYMIZED-DNSCRYPT.txt) and [ODoH (Oblivious DoH)](https://github.com/DNSCrypt/dnscrypt-resolvers/blob/master/v3/odoh-servers.md).
|
||||||
|
|
||||||
* **[dnscrypt-proxy documentation](https://dnscrypt.info/doc) ← Start here**
|
* **[dnscrypt-proxy documentation](https://dnscrypt.info/doc) ← Start here**
|
||||||
* [DNSCrypt project home page](https://dnscrypt.info/)
|
* [DNSCrypt project home page](https://dnscrypt.info/)
|
||||||
|
@ -25,7 +25,7 @@ Available as source code and pre-built binaries for most operating systems and a
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* DNS traffic encryption and authentication. Supports DNS-over-HTTPS (DoH) using TLS 1.3, DNSCrypt, Anonymized DNS and ODoH
|
* DNS traffic encryption and authentication. Supports DNS-over-HTTPS (DoH) using TLS 1.3 and QUIC, DNSCrypt, Anonymized DNS and ODoH
|
||||||
* Client IP addresses can be hidden using Tor, SOCKS proxies or Anonymized DNS relays
|
* Client IP addresses can be hidden using Tor, SOCKS proxies or Anonymized DNS relays
|
||||||
* DNS query monitoring, with separate log files for regular and suspicious queries
|
* DNS query monitoring, with separate log files for regular and suspicious queries
|
||||||
* Filtering: block ads, malware, and other unwanted content. Compatible with all DNS services
|
* Filtering: block ads, malware, and other unwanted content. Compatible with all DNS services
|
||||||
|
@ -60,7 +60,8 @@ Up-to-date, pre-built binaries are available for:
|
||||||
* Linux/mips64le
|
* Linux/mips64le
|
||||||
* Linux/x86
|
* Linux/x86
|
||||||
* Linux/x86_64
|
* Linux/x86_64
|
||||||
* MacOS X
|
* macOS/arm64
|
||||||
|
* macOS/x86_64
|
||||||
* NetBSD/x86
|
* NetBSD/x86
|
||||||
* NetBSD/x86_64
|
* NetBSD/x86_64
|
||||||
* OpenBSD/x86
|
* OpenBSD/x86
|
||||||
|
@ -74,7 +75,7 @@ How to use these files, as well as how to verify their signatures, are documente
|
||||||
|
|
||||||
### Code Contributors
|
### Code Contributors
|
||||||
|
|
||||||
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
|
This project exists thanks to all the people who contribute.
|
||||||
<a href="https://github.com/dnscrypt/dnscrypt-proxy/graphs/contributors"><img src="https://opencollective.com/dnscrypt/contributors.svg?width=890&button=false" /></a>
|
<a href="https://github.com/dnscrypt/dnscrypt-proxy/graphs/contributors"><img src="https://opencollective.com/dnscrypt/contributors.svg?width=890&button=false" /></a>
|
||||||
|
|
||||||
### Financial Contributors
|
### Financial Contributors
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
FROM ubuntu:latest
|
||||||
|
MAINTAINER dnscrypt-authors
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y wget wine dotnet-sdk-6.0 && \
|
||||||
|
dpkg --add-architecture i386 && apt-get update && apt-get install -y wine32
|
||||||
|
|
||||||
|
|
||||||
|
ENV WINEPREFIX=/root/.wine32 WINEARCH=win32 WINEDEBUG=-all
|
||||||
|
|
||||||
|
RUN wget https://dl.winehq.org/wine/wine-mono/8.1.0/wine-mono-8.1.0-x86.msi && \
|
||||||
|
WINEPREFIX="$HOME/.wine32" WINEARCH=win32 wineboot --init && \
|
||||||
|
WINEPREFIX="$HOME/.wine32" WINEARCH=win32 wine msiexec /i wine-mono-8.1.0-x86.msi && \
|
||||||
|
mkdir $WINEPREFIX/drive_c/temp && \
|
||||||
|
apt-get install -y unzip && \
|
||||||
|
wget https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip -nv -O wix.zip && \
|
||||||
|
unzip wix.zip -d /wix && \
|
||||||
|
rm -f wix.zip
|
||||||
|
|
||||||
|
WORKDIR /wix
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Scripts and utilities related to building an .msi (Microsoft Standard Installer) file.
|
||||||
|
|
||||||
|
## Docker test image for building an MSI locally
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker build . -f Dockerfile -t ubuntu:dnscrypt-msi
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test building msi files for intel win32 & win64
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./build.sh
|
||||||
|
```
|
|
@ -0,0 +1,30 @@
|
||||||
|
#! /bin/sh
|
||||||
|
|
||||||
|
version=0.0.0
|
||||||
|
gitver=$(git describe --tags --always --match="[0-9]*.[0-9]*.[0-9]*" --exclude='*[^0-9.]*')
|
||||||
|
if [ "$gitver" != "" ]; then
|
||||||
|
version=$gitver
|
||||||
|
fi
|
||||||
|
|
||||||
|
# build the image by running: docker build . -f Dockerfile -t ubuntu:dnscrypt-msi
|
||||||
|
if [ "$(docker image list -q ubuntu:dnscrypt-msi)" = "" ]; then
|
||||||
|
docker build . -f Dockerfile -t ubuntu:dnscrypt-msi
|
||||||
|
fi
|
||||||
|
|
||||||
|
image=ubuntu:dnscrypt-msi
|
||||||
|
|
||||||
|
for arch in x64 x86; do
|
||||||
|
binpath="win32"
|
||||||
|
if [ "$arch" = "x64" ]; then
|
||||||
|
binpath="win64"
|
||||||
|
fi
|
||||||
|
src=$(
|
||||||
|
cd ../../dnscrypt-proxy/$binpath || exit
|
||||||
|
pwd
|
||||||
|
)
|
||||||
|
echo "$src"
|
||||||
|
|
||||||
|
docker run --rm -v "$(pwd)":/wixproj -v "$src":/src $image wine candle.exe -dVersion="$version" -dPlatform=$arch -dPath=\\src -arch $arch \\wixproj\\dnscrypt.wxs -out \\wixproj\\dnscrypt-$arch.wixobj
|
||||||
|
docker run --rm -v "$(pwd)":/wixproj -v "$src":/src $image wine light.exe -out \\wixproj\\dnscrypt-proxy-$arch-"$version".msi \\wixproj\\dnscrypt-$arch.wixobj -sval
|
||||||
|
|
||||||
|
done
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
|
||||||
|
<?if $(var.Platform)="x64" ?>
|
||||||
|
<?define Program_Files="ProgramFiles64Folder"?>
|
||||||
|
<?else ?>
|
||||||
|
<?define Program_Files="ProgramFilesFolder"?>
|
||||||
|
<?endif ?>
|
||||||
|
<?ifndef var.Version?>
|
||||||
|
<?error Undefined Version variable?>
|
||||||
|
<?endif ?>
|
||||||
|
<?ifndef var.Path?>
|
||||||
|
<?error Undefined Path variable?>
|
||||||
|
<?endif ?>
|
||||||
|
|
||||||
|
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||||
|
<Product Id="*"
|
||||||
|
UpgradeCode="fbf99dd8-c21e-4f9b-a632-de53bb64c45e"
|
||||||
|
Name="dnscrypt-proxy"
|
||||||
|
Version="$(var.Version)"
|
||||||
|
Manufacturer="DNSCrypt"
|
||||||
|
Language="1033">
|
||||||
|
|
||||||
|
<Package InstallerVersion="200" Compressed="yes" Comments="Windows Installer Package" InstallScope="perMachine" />
|
||||||
|
<Media Id="1" Cabinet="product.cab" EmbedCab="yes" />
|
||||||
|
<MajorUpgrade DowngradeErrorMessage="A later version of [ProductName] is already installed. Setup will now exit." />
|
||||||
|
|
||||||
|
<Upgrade Id="fbf99dd8-c21e-4f9b-a632-de53bb64c45e">
|
||||||
|
<UpgradeVersion Minimum="$(var.Version)" OnlyDetect="yes" Property="NEWERVERSIONDETECTED" />
|
||||||
|
<UpgradeVersion Minimum="2.1.0" Maximum="$(var.Version)" IncludeMinimum="yes" IncludeMaximum="no" Property="OLDERVERSIONBEINGUPGRADED" />
|
||||||
|
</Upgrade>
|
||||||
|
<Condition Message="A newer version of this software is already installed.">NOT NEWERVERSIONDETECTED</Condition>
|
||||||
|
|
||||||
|
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||||
|
<Directory Id="$(var.Program_Files)">
|
||||||
|
<Directory Id="INSTALLDIR" Name="DNSCrypt">
|
||||||
|
<Component Id="ApplicationFiles" Guid="7d693c0b-71d8-436a-9c84-60a11dc74092">
|
||||||
|
<File Id="dnscryptproxy.exe" KeyPath="yes" Source="$(var.Path)\dnscrypt-proxy.exe" DiskId="1"/>
|
||||||
|
<File Source="$(var.Path)\LICENSE"></File>
|
||||||
|
<File Source="$(var.Path)\service-install.bat"></File>
|
||||||
|
<File Source="$(var.Path)\service-restart.bat"></File>
|
||||||
|
<File Source="$(var.Path)\service-uninstall.bat"></File>
|
||||||
|
<File Source="$(var.Path)\example-dnscrypt-proxy.toml"></File>
|
||||||
|
</Component>
|
||||||
|
<Component Id="ConfigInstall" Guid="db7b691e-f7c7-4c9a-92e1-c6f21ce6430f" KeyPath="yes">
|
||||||
|
<Condition><![CDATA[CONFIGFILE]]></Condition>
|
||||||
|
<CopyFile Id="dnscryptproxytoml" DestinationDirectory="INSTALLDIR" DestinationName="dnscrypt-proxy.toml" SourceProperty="CONFIGFILE">
|
||||||
|
</CopyFile>
|
||||||
|
<RemoveFile Id="RemoveConfig" Directory="INSTALLDIR" Name="dnscrypt-proxy.toml" On="uninstall" />
|
||||||
|
</Component>
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
<Feature Id="Complete" Level="1">
|
||||||
|
<ComponentRef Id="ApplicationFiles" />
|
||||||
|
<ComponentRef Id="ConfigInstall" />
|
||||||
|
</Feature>
|
||||||
|
|
||||||
|
</Product>
|
||||||
|
</Wix>
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jedisct1/dlog"
|
"github.com/jedisct1/dlog"
|
||||||
|
@ -15,14 +16,13 @@ type CaptivePortalEntryIPs []net.IP
|
||||||
type CaptivePortalMap map[string]CaptivePortalEntryIPs
|
type CaptivePortalMap map[string]CaptivePortalEntryIPs
|
||||||
|
|
||||||
type CaptivePortalHandler struct {
|
type CaptivePortalHandler struct {
|
||||||
cancelChannels []chan struct{}
|
wg sync.WaitGroup
|
||||||
|
cancelChannel chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (captivePortalHandler *CaptivePortalHandler) Stop() {
|
func (captivePortalHandler *CaptivePortalHandler) Stop() {
|
||||||
for _, cancelChannel := range captivePortalHandler.cancelChannels {
|
close(captivePortalHandler.cancelChannel)
|
||||||
cancelChannel <- struct{}{}
|
captivePortalHandler.wg.Wait()
|
||||||
<-cancelChannel
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ipsMap *CaptivePortalMap) GetEntry(msg *dns.Msg) (*dns.Question, *CaptivePortalEntryIPs) {
|
func (ipsMap *CaptivePortalMap) GetEntry(msg *dns.Msg) (*dns.Question, *CaptivePortalEntryIPs) {
|
||||||
|
@ -116,20 +116,30 @@ func handleColdStartClient(clientPc *net.UDPConn, cancelChannel chan struct{}, i
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func addColdStartListener(proxy *Proxy, ipsMap *CaptivePortalMap, listenAddrStr string, cancelChannel chan struct{}) error {
|
func addColdStartListener(
|
||||||
listenUDPAddr, err := net.ResolveUDPAddr("udp", listenAddrStr)
|
ipsMap *CaptivePortalMap,
|
||||||
|
listenAddrStr string,
|
||||||
|
captivePortalHandler *CaptivePortalHandler,
|
||||||
|
) error {
|
||||||
|
network := "udp"
|
||||||
|
isIPv4 := isDigit(listenAddrStr[0])
|
||||||
|
if isIPv4 {
|
||||||
|
network = "udp4"
|
||||||
|
}
|
||||||
|
listenUDPAddr, err := net.ResolveUDPAddr(network, listenAddrStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
clientPc, err := net.ListenUDP("udp", listenUDPAddr)
|
clientPc, err := net.ListenUDP(network, listenUDPAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
captivePortalHandler.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
for !handleColdStartClient(clientPc, cancelChannel, ipsMap) {
|
for !handleColdStartClient(clientPc, captivePortalHandler.cancelChannel, ipsMap) {
|
||||||
}
|
}
|
||||||
clientPc.Close()
|
clientPc.Close()
|
||||||
cancelChannel <- struct{}{}
|
captivePortalHandler.wg.Done()
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -138,13 +148,13 @@ func ColdStart(proxy *Proxy) (*CaptivePortalHandler, error) {
|
||||||
if len(proxy.captivePortalMapFile) == 0 {
|
if len(proxy.captivePortalMapFile) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
bin, err := ReadTextFile(proxy.captivePortalMapFile)
|
lines, err := ReadTextFile(proxy.captivePortalMapFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dlog.Warn(err)
|
dlog.Warn(err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ipsMap := make(CaptivePortalMap)
|
ipsMap := make(CaptivePortalMap)
|
||||||
for lineNo, line := range strings.Split(string(bin), "\n") {
|
for lineNo, line := range strings.Split(lines, "\n") {
|
||||||
line = TrimAndStripInlineComments(line)
|
line = TrimAndStripInlineComments(line)
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
continue
|
continue
|
||||||
|
@ -175,16 +185,19 @@ func ColdStart(proxy *Proxy) (*CaptivePortalHandler, error) {
|
||||||
ipsMap[name] = ips
|
ipsMap[name] = ips
|
||||||
}
|
}
|
||||||
listenAddrStrs := proxy.listenAddresses
|
listenAddrStrs := proxy.listenAddresses
|
||||||
cancelChannels := make([]chan struct{}, 0)
|
captivePortalHandler := CaptivePortalHandler{
|
||||||
|
cancelChannel: make(chan struct{}),
|
||||||
|
}
|
||||||
|
ok := false
|
||||||
for _, listenAddrStr := range listenAddrStrs {
|
for _, listenAddrStr := range listenAddrStrs {
|
||||||
cancelChannel := make(chan struct{})
|
err = addColdStartListener(&ipsMap, listenAddrStr, &captivePortalHandler)
|
||||||
if err := addColdStartListener(proxy, &ipsMap, listenAddrStr, cancelChannel); err == nil {
|
if err == nil {
|
||||||
cancelChannels = append(cancelChannels, cancelChannel)
|
ok = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
captivePortalHandler := CaptivePortalHandler{
|
if ok {
|
||||||
cancelChannels: cancelChannels,
|
err = nil
|
||||||
}
|
}
|
||||||
proxy.captivePortalMap = &ipsMap
|
proxy.captivePortalMap = &ipsMap
|
||||||
return &captivePortalHandler, nil
|
return &captivePortalHandler, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,14 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/jedisct1/dlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CryptoConstruction uint16
|
type CryptoConstruction uint16
|
||||||
|
@ -96,20 +98,6 @@ func Max(a, b int) int {
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func MinF(a, b float64) float64 {
|
|
||||||
if a < b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func MaxF(a, b float64) float64 {
|
|
||||||
if a > b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func StringReverse(s string) string {
|
func StringReverse(s string) string {
|
||||||
r := []rune(s)
|
r := []rune(s)
|
||||||
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
|
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
|
||||||
|
@ -170,10 +158,40 @@ func ExtractHostAndPort(str string, defaultPort int) (host string, port int) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadTextFile(filename string) (string, error) {
|
func ReadTextFile(filename string) (string, error) {
|
||||||
bin, err := ioutil.ReadFile(filename)
|
bin, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
bin = bytes.TrimPrefix(bin, []byte{0xef, 0xbb, 0xbf})
|
bin = bytes.TrimPrefix(bin, []byte{0xef, 0xbb, 0xbf})
|
||||||
return string(bin), nil
|
return string(bin), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isDigit(b byte) bool { return b >= '0' && b <= '9' }
|
||||||
|
|
||||||
|
func maybeWritableByOtherUsers(p string) (bool, string, error) {
|
||||||
|
p = path.Clean(p)
|
||||||
|
for p != "/" && p != "." {
|
||||||
|
st, err := os.Stat(p)
|
||||||
|
if err != nil {
|
||||||
|
return false, p, err
|
||||||
|
}
|
||||||
|
mode := st.Mode()
|
||||||
|
if mode.Perm()&2 != 0 && !(st.IsDir() && mode&os.ModeSticky == os.ModeSticky) {
|
||||||
|
return true, p, nil
|
||||||
|
}
|
||||||
|
p = path.Dir(p)
|
||||||
|
}
|
||||||
|
return false, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WarnIfMaybeWritableByOtherUsers(p string) {
|
||||||
|
if ok, px, err := maybeWritableByOtherUsers(p); ok {
|
||||||
|
if px == p {
|
||||||
|
dlog.Criticalf("[%s] is writable by other system users - If this is not intentional, it is recommended to fix the access permissions", p)
|
||||||
|
} else {
|
||||||
|
dlog.Warnf("[%s] can be modified by other system users because [%s] is writable by other users - If this is not intentional, it is recommended to fix the access permissions", p, px)
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
dlog.Warnf("Error while checking if [%s] is accessible: [%s] : [%s]", p, px, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -38,9 +38,11 @@ type Config struct {
|
||||||
LocalDoH LocalDoHConfig `toml:"local_doh"`
|
LocalDoH LocalDoHConfig `toml:"local_doh"`
|
||||||
UserName string `toml:"user_name"`
|
UserName string `toml:"user_name"`
|
||||||
ForceTCP bool `toml:"force_tcp"`
|
ForceTCP bool `toml:"force_tcp"`
|
||||||
|
HTTP3 bool `toml:"http3"`
|
||||||
Timeout int `toml:"timeout"`
|
Timeout int `toml:"timeout"`
|
||||||
KeepAlive int `toml:"keepalive"`
|
KeepAlive int `toml:"keepalive"`
|
||||||
Proxy string `toml:"proxy"`
|
Proxy string `toml:"proxy"`
|
||||||
|
CertRefreshConcurrency int `toml:"cert_refresh_concurrency"`
|
||||||
CertRefreshDelay int `toml:"cert_refresh_delay"`
|
CertRefreshDelay int `toml:"cert_refresh_delay"`
|
||||||
CertIgnoreTimestamp bool `toml:"cert_ignore_timestamp"`
|
CertIgnoreTimestamp bool `toml:"cert_ignore_timestamp"`
|
||||||
EphemeralKeys bool `toml:"dnscrypt_ephemeral_keys"`
|
EphemeralKeys bool `toml:"dnscrypt_ephemeral_keys"`
|
||||||
|
@ -91,6 +93,7 @@ type Config struct {
|
||||||
LogMaxBackups int `toml:"log_files_max_backups"`
|
LogMaxBackups int `toml:"log_files_max_backups"`
|
||||||
TLSDisableSessionTickets bool `toml:"tls_disable_session_tickets"`
|
TLSDisableSessionTickets bool `toml:"tls_disable_session_tickets"`
|
||||||
TLSCipherSuite []uint16 `toml:"tls_cipher_suite"`
|
TLSCipherSuite []uint16 `toml:"tls_cipher_suite"`
|
||||||
|
TLSKeyLogFile string `toml:"tls_key_log_file"`
|
||||||
NetprobeAddress string `toml:"netprobe_address"`
|
NetprobeAddress string `toml:"netprobe_address"`
|
||||||
NetprobeTimeout int `toml:"netprobe_timeout"`
|
NetprobeTimeout int `toml:"netprobe_timeout"`
|
||||||
OfflineMode bool `toml:"offline_mode"`
|
OfflineMode bool `toml:"offline_mode"`
|
||||||
|
@ -98,6 +101,7 @@ type Config struct {
|
||||||
RefusedCodeInResponses bool `toml:"refused_code_in_responses"`
|
RefusedCodeInResponses bool `toml:"refused_code_in_responses"`
|
||||||
BlockedQueryResponse string `toml:"blocked_query_response"`
|
BlockedQueryResponse string `toml:"blocked_query_response"`
|
||||||
QueryMeta []string `toml:"query_meta"`
|
QueryMeta []string `toml:"query_meta"`
|
||||||
|
CloakedPTR bool `toml:"cloak_ptr"`
|
||||||
AnonymizedDNS AnonymizedDNSConfig `toml:"anonymized_dns"`
|
AnonymizedDNS AnonymizedDNSConfig `toml:"anonymized_dns"`
|
||||||
DoHClientX509Auth DoHClientX509AuthConfig `toml:"doh_client_x509_auth"`
|
DoHClientX509Auth DoHClientX509AuthConfig `toml:"doh_client_x509_auth"`
|
||||||
DoHClientX509AuthLegacy DoHClientX509AuthConfig `toml:"tls_client_auth"`
|
DoHClientX509AuthLegacy DoHClientX509AuthConfig `toml:"tls_client_auth"`
|
||||||
|
@ -113,7 +117,9 @@ func newConfig() Config {
|
||||||
LocalDoH: LocalDoHConfig{Path: "/dns-query"},
|
LocalDoH: LocalDoHConfig{Path: "/dns-query"},
|
||||||
Timeout: 5000,
|
Timeout: 5000,
|
||||||
KeepAlive: 5,
|
KeepAlive: 5,
|
||||||
|
CertRefreshConcurrency: 10,
|
||||||
CertRefreshDelay: 240,
|
CertRefreshDelay: 240,
|
||||||
|
HTTP3: false,
|
||||||
CertIgnoreTimestamp: false,
|
CertIgnoreTimestamp: false,
|
||||||
EphemeralKeys: false,
|
EphemeralKeys: false,
|
||||||
Cache: true,
|
Cache: true,
|
||||||
|
@ -140,6 +146,7 @@ func newConfig() Config {
|
||||||
LogMaxBackups: 1,
|
LogMaxBackups: 1,
|
||||||
TLSDisableSessionTickets: false,
|
TLSDisableSessionTickets: false,
|
||||||
TLSCipherSuite: nil,
|
TLSCipherSuite: nil,
|
||||||
|
TLSKeyLogFile: "",
|
||||||
NetprobeTimeout: 60,
|
NetprobeTimeout: 60,
|
||||||
OfflineMode: false,
|
OfflineMode: false,
|
||||||
RefusedCodeInResponses: false,
|
RefusedCodeInResponses: false,
|
||||||
|
@ -154,6 +161,7 @@ func newConfig() Config {
|
||||||
AnonymizedDNS: AnonymizedDNSConfig{
|
AnonymizedDNS: AnonymizedDNSConfig{
|
||||||
DirectCertFallback: true,
|
DirectCertFallback: true,
|
||||||
},
|
},
|
||||||
|
CloakedPTR: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +261,7 @@ type ServerSummary struct {
|
||||||
IPv6 bool `json:"ipv6"`
|
IPv6 bool `json:"ipv6"`
|
||||||
Addrs []string `json:"addrs,omitempty"`
|
Addrs []string `json:"addrs,omitempty"`
|
||||||
Ports []int `json:"ports"`
|
Ports []int `json:"ports"`
|
||||||
DNSSEC bool `json:"dnssec"`
|
DNSSEC *bool `json:"dnssec,omitempty"`
|
||||||
NoLog bool `json:"nolog"`
|
NoLog bool `json:"nolog"`
|
||||||
NoFilter bool `json:"nofilter"`
|
NoFilter bool `json:"nofilter"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
|
@ -284,6 +292,7 @@ type ConfigFlags struct {
|
||||||
Resolve *string
|
Resolve *string
|
||||||
List *bool
|
List *bool
|
||||||
ListAll *bool
|
ListAll *bool
|
||||||
|
IncludeRelays *bool
|
||||||
JSONOutput *bool
|
JSONOutput *bool
|
||||||
Check *bool
|
Check *bool
|
||||||
ConfigFile *string
|
ConfigFile *string
|
||||||
|
@ -312,8 +321,12 @@ func findConfigFile(configFile *string) (string, error) {
|
||||||
func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
|
func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
|
||||||
foundConfigFile, err := findConfigFile(flags.ConfigFile)
|
foundConfigFile, err := findConfigFile(flags.ConfigFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Unable to load the configuration file [%s] -- Maybe use the -config command-line switch?", *flags.ConfigFile)
|
return fmt.Errorf(
|
||||||
|
"Unable to load the configuration file [%s] -- Maybe use the -config command-line switch?",
|
||||||
|
*flags.ConfigFile,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
WarnIfMaybeWritableByOtherUsers(foundConfigFile)
|
||||||
config := newConfig()
|
config := newConfig()
|
||||||
md, err := toml.DecodeFile(foundConfigFile, &config)
|
md, err := toml.DecodeFile(foundConfigFile, &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -339,7 +352,10 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
|
||||||
dlog.SetLogLevel(dlog.SeverityInfo)
|
dlog.SetLogLevel(dlog.SeverityInfo)
|
||||||
}
|
}
|
||||||
dlog.TruncateLogFile(config.LogFileLatest)
|
dlog.TruncateLogFile(config.LogFileLatest)
|
||||||
if config.UseSyslog {
|
proxy.showCerts = *flags.ShowCerts || len(os.Getenv("SHOW_CERTS")) > 0
|
||||||
|
isCommandMode := *flags.Check || proxy.showCerts || *flags.List || *flags.ListAll
|
||||||
|
if isCommandMode {
|
||||||
|
} else if config.UseSyslog {
|
||||||
dlog.UseSyslog(true)
|
dlog.UseSyslog(true)
|
||||||
} else if config.LogFile != nil {
|
} else if config.LogFile != nil {
|
||||||
dlog.UseLogFile(*config.LogFile)
|
dlog.UseLogFile(*config.LogFile)
|
||||||
|
@ -369,6 +385,7 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
|
||||||
proxy.xTransport.tlsDisableSessionTickets = config.TLSDisableSessionTickets
|
proxy.xTransport.tlsDisableSessionTickets = config.TLSDisableSessionTickets
|
||||||
proxy.xTransport.tlsCipherSuite = config.TLSCipherSuite
|
proxy.xTransport.tlsCipherSuite = config.TLSCipherSuite
|
||||||
proxy.xTransport.mainProto = proxy.mainProto
|
proxy.xTransport.mainProto = proxy.mainProto
|
||||||
|
proxy.xTransport.http3 = config.HTTP3
|
||||||
if len(config.BootstrapResolvers) == 0 && len(config.BootstrapResolversLegacy) > 0 {
|
if len(config.BootstrapResolvers) == 0 && len(config.BootstrapResolversLegacy) > 0 {
|
||||||
dlog.Warnf("fallback_resolvers was renamed to bootstrap_resolvers - Please update your configuration")
|
dlog.Warnf("fallback_resolvers was renamed to bootstrap_resolvers - Please update your configuration")
|
||||||
config.BootstrapResolvers = config.BootstrapResolversLegacy
|
config.BootstrapResolvers = config.BootstrapResolversLegacy
|
||||||
|
@ -423,6 +440,7 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
|
||||||
if config.ForceTCP {
|
if config.ForceTCP {
|
||||||
proxy.mainProto = "tcp"
|
proxy.mainProto = "tcp"
|
||||||
}
|
}
|
||||||
|
proxy.certRefreshConcurrency = Max(1, config.CertRefreshConcurrency)
|
||||||
proxy.certRefreshDelay = time.Duration(Max(60, config.CertRefreshDelay)) * time.Minute
|
proxy.certRefreshDelay = time.Duration(Max(60, config.CertRefreshDelay)) * time.Minute
|
||||||
proxy.certRefreshDelayAfterFailure = time.Duration(10 * time.Second)
|
proxy.certRefreshDelayAfterFailure = time.Duration(10 * time.Second)
|
||||||
proxy.certIgnoreTimestamp = config.CertIgnoreTimestamp
|
proxy.certIgnoreTimestamp = config.CertIgnoreTimestamp
|
||||||
|
@ -484,6 +502,7 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
|
||||||
proxy.cacheMaxTTL = config.CacheMaxTTL
|
proxy.cacheMaxTTL = config.CacheMaxTTL
|
||||||
proxy.rejectTTL = config.RejectTTL
|
proxy.rejectTTL = config.RejectTTL
|
||||||
proxy.cloakTTL = config.CloakTTL
|
proxy.cloakTTL = config.CloakTTL
|
||||||
|
proxy.cloakedPTR = config.CloakedPTR
|
||||||
|
|
||||||
proxy.queryMeta = config.QueryMeta
|
proxy.queryMeta = config.QueryMeta
|
||||||
|
|
||||||
|
@ -616,6 +635,16 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
|
||||||
proxy.skipAnonIncompatibleResolvers = config.AnonymizedDNS.SkipIncompatible
|
proxy.skipAnonIncompatibleResolvers = config.AnonymizedDNS.SkipIncompatible
|
||||||
proxy.anonDirectCertFallback = config.AnonymizedDNS.DirectCertFallback
|
proxy.anonDirectCertFallback = config.AnonymizedDNS.DirectCertFallback
|
||||||
|
|
||||||
|
if len(config.TLSKeyLogFile) > 0 {
|
||||||
|
f, err := os.OpenFile(config.TLSKeyLogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
|
||||||
|
if err != nil {
|
||||||
|
dlog.Fatalf("Unable to create key log file [%s]: [%s]", config.TLSKeyLogFile, err)
|
||||||
|
}
|
||||||
|
dlog.Warnf("TLS key log file [%s] enabled", config.TLSKeyLogFile)
|
||||||
|
proxy.xTransport.keyLogWriter = f
|
||||||
|
proxy.xTransport.rebuildTransport()
|
||||||
|
}
|
||||||
|
|
||||||
if config.DoHClientX509AuthLegacy.Creds != nil {
|
if config.DoHClientX509AuthLegacy.Creds != nil {
|
||||||
return errors.New("[tls_client_auth] has been renamed to [doh_client_x509_auth] - Update your config file")
|
return errors.New("[tls_client_auth] has been renamed to [doh_client_x509_auth] - Update your config file")
|
||||||
}
|
}
|
||||||
|
@ -635,7 +664,9 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backwards compatibility
|
// Backwards compatibility
|
||||||
config.BrokenImplementations.FragmentsBlocked = append(config.BrokenImplementations.FragmentsBlocked, config.BrokenImplementations.BrokenQueryPadding...)
|
config.BrokenImplementations.FragmentsBlocked = append(
|
||||||
|
config.BrokenImplementations.FragmentsBlocked,
|
||||||
|
config.BrokenImplementations.BrokenQueryPadding...)
|
||||||
|
|
||||||
proxy.serversBlockingFragments = config.BrokenImplementations.FragmentsBlocked
|
proxy.serversBlockingFragments = config.BrokenImplementations.FragmentsBlocked
|
||||||
|
|
||||||
|
@ -686,8 +717,7 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
|
||||||
} else if len(config.BootstrapResolvers) > 0 {
|
} else if len(config.BootstrapResolvers) > 0 {
|
||||||
netprobeAddress = config.BootstrapResolvers[0]
|
netprobeAddress = config.BootstrapResolvers[0]
|
||||||
}
|
}
|
||||||
proxy.showCerts = *flags.ShowCerts || len(os.Getenv("SHOW_CERTS")) > 0
|
if !isCommandMode {
|
||||||
if !*flags.Check && !*flags.ShowCerts && !*flags.List && !*flags.ListAll {
|
|
||||||
if err := NetProbe(proxy, netprobeAddress, netprobeTimeout); err != nil {
|
if err := NetProbe(proxy, netprobeAddress, netprobeTimeout); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -704,7 +734,9 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
|
||||||
// if 'userName' is set and we are the parent process drop privilege and exit
|
// if 'userName' is set and we are the parent process drop privilege and exit
|
||||||
if len(proxy.userName) > 0 && !proxy.child {
|
if len(proxy.userName) > 0 && !proxy.child {
|
||||||
proxy.dropPrivilege(proxy.userName, FileDescriptors)
|
proxy.dropPrivilege(proxy.userName, FileDescriptors)
|
||||||
return errors.New("Dropping privileges is not supporting on this operating system. Unset `user_name` in the configuration file")
|
return errors.New(
|
||||||
|
"Dropping privileges is not supporting on this operating system. Unset `user_name` in the configuration file",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if !config.OfflineMode {
|
if !config.OfflineMode {
|
||||||
if err := config.loadSources(proxy); err != nil {
|
if err := config.loadSources(proxy); err != nil {
|
||||||
|
@ -715,7 +747,7 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if *flags.List || *flags.ListAll {
|
if *flags.List || *flags.ListAll {
|
||||||
if err := config.printRegisteredServers(proxy, *flags.JSONOutput); err != nil {
|
if err := config.printRegisteredServers(proxy, *flags.JSONOutput, *flags.IncludeRelays); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
@ -724,8 +756,12 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
|
||||||
hasSpecificRoutes := false
|
hasSpecificRoutes := false
|
||||||
for _, server := range proxy.registeredServers {
|
for _, server := range proxy.registeredServers {
|
||||||
if via, ok := (*proxy.routes)[server.name]; ok {
|
if via, ok := (*proxy.routes)[server.name]; ok {
|
||||||
if server.stamp.Proto != stamps.StampProtoTypeDNSCrypt && server.stamp.Proto != stamps.StampProtoTypeODoHTarget {
|
if server.stamp.Proto != stamps.StampProtoTypeDNSCrypt &&
|
||||||
dlog.Errorf("DNS anonymization is only supported with the DNSCrypt and ODoH protocols - Connections to [%v] cannot be anonymized", server.name)
|
server.stamp.Proto != stamps.StampProtoTypeODoHTarget {
|
||||||
|
dlog.Errorf(
|
||||||
|
"DNS anonymization is only supported with the DNSCrypt and ODoH protocols - Connections to [%v] cannot be anonymized",
|
||||||
|
server.name,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
dlog.Noticef("Anonymized DNS: routing [%v] via %v", server.name, via)
|
dlog.Noticef("Anonymized DNS: routing [%v] via %v", server.name, via)
|
||||||
}
|
}
|
||||||
|
@ -747,14 +783,54 @@ func ConfigLoad(proxy *Proxy, flags *ConfigFlags) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *Config) printRegisteredServers(proxy *Proxy, jsonOutput bool) error {
|
func (config *Config) printRegisteredServers(proxy *Proxy, jsonOutput bool, includeRelays bool) error {
|
||||||
var summary []ServerSummary
|
var summary []ServerSummary
|
||||||
|
if includeRelays {
|
||||||
|
for _, registeredRelay := range proxy.registeredRelays {
|
||||||
|
addrStr, port := registeredRelay.stamp.ServerAddrStr, stamps.DefaultPort
|
||||||
|
var hostAddr string
|
||||||
|
hostAddr, port = ExtractHostAndPort(addrStr, port)
|
||||||
|
addrs := make([]string, 0)
|
||||||
|
if (registeredRelay.stamp.Proto == stamps.StampProtoTypeDoH || registeredRelay.stamp.Proto == stamps.StampProtoTypeODoHTarget) &&
|
||||||
|
len(registeredRelay.stamp.ProviderName) > 0 {
|
||||||
|
providerName := registeredRelay.stamp.ProviderName
|
||||||
|
var host string
|
||||||
|
host, port = ExtractHostAndPort(providerName, port)
|
||||||
|
addrs = append(addrs, host)
|
||||||
|
}
|
||||||
|
if len(addrStr) > 0 {
|
||||||
|
addrs = append(addrs, hostAddr)
|
||||||
|
}
|
||||||
|
nolog := true
|
||||||
|
nofilter := true
|
||||||
|
if registeredRelay.stamp.Proto == stamps.StampProtoTypeODoHRelay {
|
||||||
|
nolog = registeredRelay.stamp.Props&stamps.ServerInformalPropertyNoLog != 0
|
||||||
|
}
|
||||||
|
serverSummary := ServerSummary{
|
||||||
|
Name: registeredRelay.name,
|
||||||
|
Proto: registeredRelay.stamp.Proto.String(),
|
||||||
|
IPv6: strings.HasPrefix(addrStr, "["),
|
||||||
|
Ports: []int{port},
|
||||||
|
Addrs: addrs,
|
||||||
|
NoLog: nolog,
|
||||||
|
NoFilter: nofilter,
|
||||||
|
Description: registeredRelay.description,
|
||||||
|
Stamp: registeredRelay.stamp.String(),
|
||||||
|
}
|
||||||
|
if jsonOutput {
|
||||||
|
summary = append(summary, serverSummary)
|
||||||
|
} else {
|
||||||
|
fmt.Println(serverSummary.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, registeredServer := range proxy.registeredServers {
|
for _, registeredServer := range proxy.registeredServers {
|
||||||
addrStr, port := registeredServer.stamp.ServerAddrStr, stamps.DefaultPort
|
addrStr, port := registeredServer.stamp.ServerAddrStr, stamps.DefaultPort
|
||||||
var hostAddr string
|
var hostAddr string
|
||||||
hostAddr, port = ExtractHostAndPort(addrStr, port)
|
hostAddr, port = ExtractHostAndPort(addrStr, port)
|
||||||
addrs := make([]string, 0)
|
addrs := make([]string, 0)
|
||||||
if registeredServer.stamp.Proto == stamps.StampProtoTypeDoH && len(registeredServer.stamp.ProviderName) > 0 {
|
if (registeredServer.stamp.Proto == stamps.StampProtoTypeDoH || registeredServer.stamp.Proto == stamps.StampProtoTypeODoHTarget) &&
|
||||||
|
len(registeredServer.stamp.ProviderName) > 0 {
|
||||||
providerName := registeredServer.stamp.ProviderName
|
providerName := registeredServer.stamp.ProviderName
|
||||||
var host string
|
var host string
|
||||||
host, port = ExtractHostAndPort(providerName, port)
|
host, port = ExtractHostAndPort(providerName, port)
|
||||||
|
@ -763,13 +839,14 @@ func (config *Config) printRegisteredServers(proxy *Proxy, jsonOutput bool) erro
|
||||||
if len(addrStr) > 0 {
|
if len(addrStr) > 0 {
|
||||||
addrs = append(addrs, hostAddr)
|
addrs = append(addrs, hostAddr)
|
||||||
}
|
}
|
||||||
|
dnssec := registeredServer.stamp.Props&stamps.ServerInformalPropertyDNSSEC != 0
|
||||||
serverSummary := ServerSummary{
|
serverSummary := ServerSummary{
|
||||||
Name: registeredServer.name,
|
Name: registeredServer.name,
|
||||||
Proto: registeredServer.stamp.Proto.String(),
|
Proto: registeredServer.stamp.Proto.String(),
|
||||||
IPv6: strings.HasPrefix(addrStr, "["),
|
IPv6: strings.HasPrefix(addrStr, "["),
|
||||||
Ports: []int{port},
|
Ports: []int{port},
|
||||||
Addrs: addrs,
|
Addrs: addrs,
|
||||||
DNSSEC: registeredServer.stamp.Props&stamps.ServerInformalPropertyDNSSEC != 0,
|
DNSSEC: &dnssec,
|
||||||
NoLog: registeredServer.stamp.Props&stamps.ServerInformalPropertyNoLog != 0,
|
NoLog: registeredServer.stamp.Props&stamps.ServerInformalPropertyNoLog != 0,
|
||||||
NoFilter: registeredServer.stamp.Props&stamps.ServerInformalPropertyNoFilter != 0,
|
NoFilter: registeredServer.stamp.Props&stamps.ServerInformalPropertyNoFilter != 0,
|
||||||
Description: registeredServer.description,
|
Description: registeredServer.description,
|
||||||
|
@ -829,7 +906,9 @@ func (config *Config) loadSources(proxy *Proxy) error {
|
||||||
}
|
}
|
||||||
proxy.registeredServers = append(proxy.registeredServers, RegisteredServer{name: serverName, stamp: stamp})
|
proxy.registeredServers = append(proxy.registeredServers, RegisteredServer{name: serverName, stamp: stamp})
|
||||||
}
|
}
|
||||||
proxy.updateRegisteredServers()
|
if err := proxy.updateRegisteredServers(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
rs1 := proxy.registeredServers
|
rs1 := proxy.registeredServers
|
||||||
rs2 := proxy.serversInfo.registeredServers
|
rs2 := proxy.serversInfo.registeredServers
|
||||||
rand.Shuffle(len(rs1), func(i, j int) {
|
rand.Shuffle(len(rs1), func(i, j int) {
|
||||||
|
@ -860,12 +939,20 @@ func (config *Config) loadSource(proxy *Proxy, cfgSourceName string, cfgSource *
|
||||||
}
|
}
|
||||||
if cfgSource.RefreshDelay <= 0 {
|
if cfgSource.RefreshDelay <= 0 {
|
||||||
cfgSource.RefreshDelay = 72
|
cfgSource.RefreshDelay = 72
|
||||||
} else if cfgSource.RefreshDelay > 168 {
|
|
||||||
cfgSource.RefreshDelay = 168
|
|
||||||
}
|
}
|
||||||
source, err := NewSource(cfgSourceName, proxy.xTransport, cfgSource.URLs, cfgSource.MinisignKeyStr, cfgSource.CacheFile, cfgSource.FormatStr, time.Duration(cfgSource.RefreshDelay)*time.Hour, cfgSource.Prefix)
|
cfgSource.RefreshDelay = Min(168, Max(24, cfgSource.RefreshDelay))
|
||||||
|
source, err := NewSource(
|
||||||
|
cfgSourceName,
|
||||||
|
proxy.xTransport,
|
||||||
|
cfgSource.URLs,
|
||||||
|
cfgSource.MinisignKeyStr,
|
||||||
|
cfgSource.CacheFile,
|
||||||
|
cfgSource.FormatStr,
|
||||||
|
time.Duration(cfgSource.RefreshDelay)*time.Hour,
|
||||||
|
cfgSource.Prefix,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(source.in) <= 0 {
|
if len(source.bin) <= 0 {
|
||||||
dlog.Criticalf("Unable to retrieve source [%s]: [%s]", cfgSourceName, err)
|
dlog.Criticalf("Unable to retrieve source [%s]: [%s]", cfgSourceName, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -891,7 +978,10 @@ func cdFileDir(fileName string) error {
|
||||||
func cdLocal() {
|
func cdLocal() {
|
||||||
exeFileName, err := os.Executable()
|
exeFileName, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dlog.Warnf("Unable to determine the executable directory: [%s] -- You will need to specify absolute paths in the configuration file", err)
|
dlog.Warnf(
|
||||||
|
"Unable to determine the executable directory: [%s] -- You will need to specify absolute paths in the configuration file",
|
||||||
|
err,
|
||||||
|
)
|
||||||
} else if err := os.Chdir(filepath.Dir(exeFileName)); err != nil {
|
} else if err := os.Chdir(filepath.Dir(exeFileName)); err != nil {
|
||||||
dlog.Warnf("Unable to change working directory to [%s]: %s", exeFileName, err)
|
dlog.Warnf("Unable to change working directory to [%s]: %s", exeFileName, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
crypto_rand "crypto/rand"
|
crypto_rand "crypto/rand"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"errors"
|
"errors"
|
||||||
"math/rand"
|
|
||||||
|
|
||||||
"github.com/jedisct1/dlog"
|
"github.com/jedisct1/dlog"
|
||||||
"github.com/jedisct1/xsecretbox"
|
"github.com/jedisct1/xsecretbox"
|
||||||
|
@ -45,7 +44,12 @@ func unpad(packet []byte) ([]byte, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ComputeSharedKey(cryptoConstruction CryptoConstruction, secretKey *[32]byte, serverPk *[32]byte, providerName *string) (sharedKey [32]byte) {
|
func ComputeSharedKey(
|
||||||
|
cryptoConstruction CryptoConstruction,
|
||||||
|
secretKey *[32]byte,
|
||||||
|
serverPk *[32]byte,
|
||||||
|
providerName *string,
|
||||||
|
) (sharedKey [32]byte) {
|
||||||
if cryptoConstruction == XChacha20Poly1305 {
|
if cryptoConstruction == XChacha20Poly1305 {
|
||||||
var err error
|
var err error
|
||||||
sharedKey, err = xsecretbox.SharedKey(*secretKey, *serverPk)
|
sharedKey, err = xsecretbox.SharedKey(*secretKey, *serverPk)
|
||||||
|
@ -68,9 +72,15 @@ func ComputeSharedKey(cryptoConstruction CryptoConstruction, secretKey *[32]byte
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (proxy *Proxy) Encrypt(serverInfo *ServerInfo, packet []byte, proto string) (sharedKey *[32]byte, encrypted []byte, clientNonce []byte, err error) {
|
func (proxy *Proxy) Encrypt(
|
||||||
|
serverInfo *ServerInfo,
|
||||||
|
packet []byte,
|
||||||
|
proto string,
|
||||||
|
) (sharedKey *[32]byte, encrypted []byte, clientNonce []byte, err error) {
|
||||||
nonce, clientNonce := make([]byte, NonceSize), make([]byte, HalfNonceSize)
|
nonce, clientNonce := make([]byte, NonceSize), make([]byte, HalfNonceSize)
|
||||||
crypto_rand.Read(clientNonce)
|
if _, err := crypto_rand.Read(clientNonce); err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
copy(nonce, clientNonce)
|
copy(nonce, clientNonce)
|
||||||
var publicKey *[PublicKeySize]byte
|
var publicKey *[PublicKeySize]byte
|
||||||
if proxy.ephemeralKeys {
|
if proxy.ephemeralKeys {
|
||||||
|
@ -93,14 +103,15 @@ func (proxy *Proxy) Encrypt(serverInfo *ServerInfo, packet []byte, proto string)
|
||||||
minQuestionSize = Max(proxy.questionSizeEstimator.MinQuestionSize(), minQuestionSize)
|
minQuestionSize = Max(proxy.questionSizeEstimator.MinQuestionSize(), minQuestionSize)
|
||||||
} else {
|
} else {
|
||||||
var xpad [1]byte
|
var xpad [1]byte
|
||||||
rand.Read(xpad[:])
|
if _, err := crypto_rand.Read(xpad[:]); err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
minQuestionSize += int(xpad[0])
|
minQuestionSize += int(xpad[0])
|
||||||
}
|
}
|
||||||
paddedLength := Min(MaxDNSUDPPacketSize, (Max(minQuestionSize, QueryOverhead)+1+63) & ^63)
|
paddedLength := Min(MaxDNSUDPPacketSize, (Max(minQuestionSize, QueryOverhead)+1+63) & ^63)
|
||||||
if proto == "udp" && serverInfo.knownBugs.fragmentsBlocked {
|
if serverInfo.knownBugs.fragmentsBlocked && proto == "udp" {
|
||||||
paddedLength = MaxDNSUDPSafePacketSize
|
paddedLength = MaxDNSUDPSafePacketSize
|
||||||
}
|
} else if serverInfo.Relay != nil && proto == "tcp" {
|
||||||
if serverInfo.Relay != nil && proto == "tcp" {
|
|
||||||
paddedLength = MaxDNSPacketSize
|
paddedLength = MaxDNSPacketSize
|
||||||
}
|
}
|
||||||
if QueryOverhead+len(packet)+1 > paddedLength {
|
if QueryOverhead+len(packet)+1 > paddedLength {
|
||||||
|
@ -120,7 +131,12 @@ func (proxy *Proxy) Encrypt(serverInfo *ServerInfo, packet []byte, proto string)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (proxy *Proxy) Decrypt(serverInfo *ServerInfo, sharedKey *[32]byte, encrypted []byte, nonce []byte) ([]byte, error) {
|
func (proxy *Proxy) Decrypt(
|
||||||
|
serverInfo *ServerInfo,
|
||||||
|
sharedKey *[32]byte,
|
||||||
|
encrypted []byte,
|
||||||
|
nonce []byte,
|
||||||
|
) ([]byte, error) {
|
||||||
serverMagicLen := len(ServerMagic)
|
serverMagicLen := len(ServerMagic)
|
||||||
responseHeaderLen := serverMagicLen + NonceSize
|
responseHeaderLen := serverMagicLen + NonceSize
|
||||||
if len(encrypted) < responseHeaderLen+TagSize+int(MinDNSPacketSize) ||
|
if len(encrypted) < responseHeaderLen+TagSize+int(MinDNSPacketSize) ||
|
||||||
|
|
|
@ -20,12 +20,22 @@ type CertInfo struct {
|
||||||
ForwardSecurity bool
|
ForwardSecurity bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func FetchCurrentDNSCryptCert(proxy *Proxy, serverName *string, proto string, pk ed25519.PublicKey, serverAddress string, providerName string, isNew bool, relay *DNSCryptRelay, knownBugs ServerBugs) (CertInfo, int, bool, error) {
|
func FetchCurrentDNSCryptCert(
|
||||||
|
proxy *Proxy,
|
||||||
|
serverName *string,
|
||||||
|
proto string,
|
||||||
|
pk ed25519.PublicKey,
|
||||||
|
serverAddress string,
|
||||||
|
providerName string,
|
||||||
|
isNew bool,
|
||||||
|
relay *DNSCryptRelay,
|
||||||
|
knownBugs ServerBugs,
|
||||||
|
) (CertInfo, int, bool, error) {
|
||||||
if len(pk) != ed25519.PublicKeySize {
|
if len(pk) != ed25519.PublicKeySize {
|
||||||
return CertInfo{}, 0, false, errors.New("Invalid public key length")
|
return CertInfo{}, 0, false, errors.New("Invalid public key length")
|
||||||
}
|
}
|
||||||
if !strings.HasSuffix(providerName, ".") {
|
if !strings.HasSuffix(providerName, ".") {
|
||||||
providerName = providerName + "."
|
providerName += "."
|
||||||
}
|
}
|
||||||
if serverName == nil {
|
if serverName == nil {
|
||||||
serverName = &providerName
|
serverName = &providerName
|
||||||
|
@ -34,7 +44,11 @@ func FetchCurrentDNSCryptCert(proxy *Proxy, serverName *string, proto string, pk
|
||||||
query.SetQuestion(providerName, dns.TypeTXT)
|
query.SetQuestion(providerName, dns.TypeTXT)
|
||||||
if !strings.HasPrefix(providerName, "2.dnscrypt-cert.") {
|
if !strings.HasPrefix(providerName, "2.dnscrypt-cert.") {
|
||||||
if relay != nil && !proxy.anonDirectCertFallback {
|
if relay != nil && !proxy.anonDirectCertFallback {
|
||||||
dlog.Warnf("[%v] uses a non-standard provider name, enable direct cert fallback to use with a relay ('%v' doesn't start with '2.dnscrypt-cert.')", *serverName, providerName)
|
dlog.Warnf(
|
||||||
|
"[%v] uses a non-standard provider name, enable direct cert fallback to use with a relay ('%v' doesn't start with '2.dnscrypt-cert.')",
|
||||||
|
*serverName,
|
||||||
|
providerName,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
dlog.Warnf("[%v] uses a non-standard provider name ('%v' doesn't start with '2.dnscrypt-cert.')", *serverName, providerName)
|
dlog.Warnf("[%v] uses a non-standard provider name ('%v' doesn't start with '2.dnscrypt-cert.')", *serverName, providerName)
|
||||||
relay = nil
|
relay = nil
|
||||||
|
@ -44,7 +58,15 @@ func FetchCurrentDNSCryptCert(proxy *Proxy, serverName *string, proto string, pk
|
||||||
if knownBugs.fragmentsBlocked {
|
if knownBugs.fragmentsBlocked {
|
||||||
tryFragmentsSupport = false
|
tryFragmentsSupport = false
|
||||||
}
|
}
|
||||||
in, rtt, fragmentsBlocked, err := DNSExchange(proxy, proto, &query, serverAddress, relay, serverName, tryFragmentsSupport)
|
in, rtt, fragmentsBlocked, err := DNSExchange(
|
||||||
|
proxy,
|
||||||
|
proto,
|
||||||
|
&query,
|
||||||
|
serverAddress,
|
||||||
|
relay,
|
||||||
|
serverName,
|
||||||
|
tryFragmentsSupport,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dlog.Noticef("[%s] TIMEOUT", *serverName)
|
dlog.Noticef("[%s] TIMEOUT", *serverName)
|
||||||
return CertInfo{}, 0, fragmentsBlocked, err
|
return CertInfo{}, 0, fragmentsBlocked, err
|
||||||
|
@ -95,10 +117,17 @@ func FetchCurrentDNSCryptCert(proxy *Proxy, serverName *string, proto string, pk
|
||||||
}
|
}
|
||||||
ttl := tsEnd - tsBegin
|
ttl := tsEnd - tsBegin
|
||||||
if ttl > 86400*7 {
|
if ttl > 86400*7 {
|
||||||
dlog.Infof("[%v] the key validity period for this server is excessively long (%d days), significantly reducing reliability and forward security.", *serverName, ttl/86400)
|
dlog.Infof(
|
||||||
|
"[%v] the key validity period for this server is excessively long (%d days), significantly reducing reliability and forward security.",
|
||||||
|
*serverName,
|
||||||
|
ttl/86400,
|
||||||
|
)
|
||||||
daysLeft := (tsEnd - now) / 86400
|
daysLeft := (tsEnd - now) / 86400
|
||||||
if daysLeft < 1 {
|
if daysLeft < 1 {
|
||||||
dlog.Criticalf("[%v] certificate will expire today -- Switch to a different resolver as soon as possible", *serverName)
|
dlog.Criticalf(
|
||||||
|
"[%v] certificate will expire today -- Switch to a different resolver as soon as possible",
|
||||||
|
*serverName,
|
||||||
|
)
|
||||||
} else if daysLeft <= 7 {
|
} else if daysLeft <= 7 {
|
||||||
dlog.Warnf("[%v] certificate is about to expire -- if you don't manage this server, tell the server operator about it", *serverName)
|
dlog.Warnf("[%v] certificate is about to expire -- if you don't manage this server, tell the server operator about it", *serverName)
|
||||||
} else if daysLeft <= 30 {
|
} else if daysLeft <= 30 {
|
||||||
|
@ -112,7 +141,13 @@ func FetchCurrentDNSCryptCert(proxy *Proxy, serverName *string, proto string, pk
|
||||||
}
|
}
|
||||||
if !proxy.certIgnoreTimestamp {
|
if !proxy.certIgnoreTimestamp {
|
||||||
if now > tsEnd || now < tsBegin {
|
if now > tsEnd || now < tsBegin {
|
||||||
dlog.Debugf("[%v] Certificate not valid at the current date (now: %v is not in [%v..%v])", *serverName, now, tsBegin, tsEnd)
|
dlog.Debugf(
|
||||||
|
"[%v] Certificate not valid at the current date (now: %v is not in [%v..%v])",
|
||||||
|
*serverName,
|
||||||
|
now,
|
||||||
|
tsBegin,
|
||||||
|
tsEnd,
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -148,7 +183,7 @@ func FetchCurrentDNSCryptCert(proxy *Proxy, serverName *string, proto string, pk
|
||||||
certCountStr = " - additional certificate"
|
certCountStr = " - additional certificate"
|
||||||
}
|
}
|
||||||
if certInfo.CryptoConstruction == UndefinedConstruction {
|
if certInfo.CryptoConstruction == UndefinedConstruction {
|
||||||
return certInfo, 0, fragmentsBlocked, errors.New("No useable certificate found")
|
return certInfo, 0, fragmentsBlocked, errors.New("No usable certificate found")
|
||||||
}
|
}
|
||||||
return certInfo, int(rtt.Nanoseconds() / 1000000), fragmentsBlocked, nil
|
return certInfo, int(rtt.Nanoseconds() / 1000000), fragmentsBlocked, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,11 @@ func TruncatedResponse(packet []byte) ([]byte, error) {
|
||||||
|
|
||||||
func RefusedResponseFromMessage(srcMsg *dns.Msg, refusedCode bool, ipv4 net.IP, ipv6 net.IP, ttl uint32) *dns.Msg {
|
func RefusedResponseFromMessage(srcMsg *dns.Msg, refusedCode bool, ipv4 net.IP, ipv6 net.IP, ttl uint32) *dns.Msg {
|
||||||
dstMsg := EmptyResponseFromMessage(srcMsg)
|
dstMsg := EmptyResponseFromMessage(srcMsg)
|
||||||
|
ede := new(dns.EDNS0_EDE)
|
||||||
|
if edns0 := dstMsg.IsEdns0(); edns0 != nil {
|
||||||
|
edns0.Option = append(edns0.Option, ede)
|
||||||
|
}
|
||||||
|
ede.InfoCode = dns.ExtendedErrorCodeFiltered
|
||||||
if refusedCode {
|
if refusedCode {
|
||||||
dstMsg.Rcode = dns.RcodeRefused
|
dstMsg.Rcode = dns.RcodeRefused
|
||||||
} else {
|
} else {
|
||||||
|
@ -58,6 +63,7 @@ func RefusedResponseFromMessage(srcMsg *dns.Msg, refusedCode bool, ipv4 net.IP,
|
||||||
if rr.A != nil {
|
if rr.A != nil {
|
||||||
dstMsg.Answer = []dns.RR{rr}
|
dstMsg.Answer = []dns.RR{rr}
|
||||||
sendHInfoResponse = false
|
sendHInfoResponse = false
|
||||||
|
ede.InfoCode = dns.ExtendedErrorCodeForgedAnswer
|
||||||
}
|
}
|
||||||
} else if ipv6 != nil && question.Qtype == dns.TypeAAAA {
|
} else if ipv6 != nil && question.Qtype == dns.TypeAAAA {
|
||||||
rr := new(dns.AAAA)
|
rr := new(dns.AAAA)
|
||||||
|
@ -66,18 +72,24 @@ func RefusedResponseFromMessage(srcMsg *dns.Msg, refusedCode bool, ipv4 net.IP,
|
||||||
if rr.AAAA != nil {
|
if rr.AAAA != nil {
|
||||||
dstMsg.Answer = []dns.RR{rr}
|
dstMsg.Answer = []dns.RR{rr}
|
||||||
sendHInfoResponse = false
|
sendHInfoResponse = false
|
||||||
|
ede.InfoCode = dns.ExtendedErrorCodeForgedAnswer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sendHInfoResponse {
|
if sendHInfoResponse {
|
||||||
hinfo := new(dns.HINFO)
|
hinfo := new(dns.HINFO)
|
||||||
hinfo.Hdr = dns.RR_Header{Name: question.Name, Rrtype: dns.TypeHINFO,
|
hinfo.Hdr = dns.RR_Header{
|
||||||
Class: dns.ClassINET, Ttl: ttl}
|
Name: question.Name, Rrtype: dns.TypeHINFO,
|
||||||
|
Class: dns.ClassINET, Ttl: ttl,
|
||||||
|
}
|
||||||
hinfo.Cpu = "This query has been locally blocked"
|
hinfo.Cpu = "This query has been locally blocked"
|
||||||
hinfo.Os = "by dnscrypt-proxy"
|
hinfo.Os = "by dnscrypt-proxy"
|
||||||
dstMsg.Answer = []dns.RR{hinfo}
|
dstMsg.Answer = []dns.RR{hinfo}
|
||||||
|
} else {
|
||||||
|
ede.ExtraText = "This query has been locally blocked by dnscrypt-proxy"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return dstMsg
|
return dstMsg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +147,8 @@ func NormalizeQName(str string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMinTTL(msg *dns.Msg, minTTL uint32, maxTTL uint32, cacheNegMinTTL uint32, cacheNegMaxTTL uint32) time.Duration {
|
func getMinTTL(msg *dns.Msg, minTTL uint32, maxTTL uint32, cacheNegMinTTL uint32, cacheNegMaxTTL uint32) time.Duration {
|
||||||
if (msg.Rcode != dns.RcodeSuccess && msg.Rcode != dns.RcodeNameError) || (len(msg.Answer) <= 0 && len(msg.Ns) <= 0) {
|
if (msg.Rcode != dns.RcodeSuccess && msg.Rcode != dns.RcodeNameError) ||
|
||||||
|
(len(msg.Answer) <= 0 && len(msg.Ns) <= 0) {
|
||||||
return time.Duration(cacheNegMinTTL) * time.Second
|
return time.Duration(cacheNegMinTTL) * time.Second
|
||||||
}
|
}
|
||||||
var ttl uint32
|
var ttl uint32
|
||||||
|
@ -261,8 +274,6 @@ func removeEDNS0Options(msg *dns.Msg) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func isDigit(b byte) bool { return b >= '0' && b <= '9' }
|
|
||||||
|
|
||||||
func dddToByte(s []byte) byte {
|
func dddToByte(s []byte) byte {
|
||||||
return byte((s[0]-'0')*100 + (s[1]-'0')*10 + (s[2] - '0'))
|
return byte((s[0]-'0')*100 + (s[1]-'0')*10 + (s[2] - '0'))
|
||||||
}
|
}
|
||||||
|
@ -304,44 +315,53 @@ type DNSExchangeResponse struct {
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func DNSExchange(proxy *Proxy, proto string, query *dns.Msg, serverAddress string, relay *DNSCryptRelay, serverName *string, tryFragmentsSupport bool) (*dns.Msg, time.Duration, bool, error) {
|
func DNSExchange(
|
||||||
|
proxy *Proxy,
|
||||||
|
proto string,
|
||||||
|
query *dns.Msg,
|
||||||
|
serverAddress string,
|
||||||
|
relay *DNSCryptRelay,
|
||||||
|
serverName *string,
|
||||||
|
tryFragmentsSupport bool,
|
||||||
|
) (*dns.Msg, time.Duration, bool, error) {
|
||||||
for {
|
for {
|
||||||
cancelChannel := make(chan struct{})
|
cancelChannel := make(chan struct{})
|
||||||
channel := make(chan DNSExchangeResponse)
|
maxTries := 3
|
||||||
|
channel := make(chan DNSExchangeResponse, 2*maxTries)
|
||||||
var err error
|
var err error
|
||||||
options := 0
|
options := 0
|
||||||
|
|
||||||
for tries := 0; tries < 3; tries++ {
|
for tries := 0; tries < maxTries; tries++ {
|
||||||
if tryFragmentsSupport {
|
if tryFragmentsSupport {
|
||||||
queryCopy := query.Copy()
|
queryCopy := query.Copy()
|
||||||
queryCopy.Id += uint16(options)
|
queryCopy.Id += uint16(options)
|
||||||
go func(query *dns.Msg, delay time.Duration) {
|
go func(query *dns.Msg, delay time.Duration) {
|
||||||
option := _dnsExchange(proxy, proto, query, serverAddress, relay, 1500)
|
time.Sleep(delay)
|
||||||
|
option := DNSExchangeResponse{err: errors.New("Canceled")}
|
||||||
|
select {
|
||||||
|
case <-cancelChannel:
|
||||||
|
default:
|
||||||
|
option = _dnsExchange(proxy, proto, query, serverAddress, relay, 1500)
|
||||||
|
}
|
||||||
option.fragmentsBlocked = false
|
option.fragmentsBlocked = false
|
||||||
option.priority = 0
|
option.priority = 0
|
||||||
channel <- option
|
channel <- option
|
||||||
time.Sleep(delay)
|
|
||||||
select {
|
|
||||||
case <-cancelChannel:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}(queryCopy, time.Duration(200*tries)*time.Millisecond)
|
}(queryCopy, time.Duration(200*tries)*time.Millisecond)
|
||||||
options++
|
options++
|
||||||
}
|
}
|
||||||
queryCopy := query.Copy()
|
queryCopy := query.Copy()
|
||||||
queryCopy.Id += uint16(options)
|
queryCopy.Id += uint16(options)
|
||||||
go func(query *dns.Msg, delay time.Duration) {
|
go func(query *dns.Msg, delay time.Duration) {
|
||||||
option := _dnsExchange(proxy, proto, query, serverAddress, relay, 480)
|
time.Sleep(delay)
|
||||||
|
option := DNSExchangeResponse{err: errors.New("Canceled")}
|
||||||
|
select {
|
||||||
|
case <-cancelChannel:
|
||||||
|
default:
|
||||||
|
option = _dnsExchange(proxy, proto, query, serverAddress, relay, 480)
|
||||||
|
}
|
||||||
option.fragmentsBlocked = true
|
option.fragmentsBlocked = true
|
||||||
option.priority = 1
|
option.priority = 1
|
||||||
channel <- option
|
channel <- option
|
||||||
time.Sleep(delay)
|
|
||||||
select {
|
|
||||||
case <-cancelChannel:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}(queryCopy, time.Duration(250*tries)*time.Millisecond)
|
}(queryCopy, time.Duration(250*tries)*time.Millisecond)
|
||||||
options++
|
options++
|
||||||
}
|
}
|
||||||
|
@ -375,12 +395,23 @@ func DNSExchange(proxy *Proxy, proto string, query *dns.Msg, serverAddress strin
|
||||||
}
|
}
|
||||||
return nil, 0, false, err
|
return nil, 0, false, err
|
||||||
}
|
}
|
||||||
dlog.Infof("Unable to get the public key for [%v] via relay [%v], retrying over a direct connection", *serverName, relay.RelayUDPAddr.IP)
|
dlog.Infof(
|
||||||
|
"Unable to get the public key for [%v] via relay [%v], retrying over a direct connection",
|
||||||
|
*serverName,
|
||||||
|
relay.RelayUDPAddr.IP,
|
||||||
|
)
|
||||||
relay = nil
|
relay = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func _dnsExchange(proxy *Proxy, proto string, query *dns.Msg, serverAddress string, relay *DNSCryptRelay, paddedLen int) DNSExchangeResponse {
|
func _dnsExchange(
|
||||||
|
proxy *Proxy,
|
||||||
|
proto string,
|
||||||
|
query *dns.Msg,
|
||||||
|
serverAddress string,
|
||||||
|
relay *DNSCryptRelay,
|
||||||
|
paddedLen int,
|
||||||
|
) DNSExchangeResponse {
|
||||||
var packet []byte
|
var packet []byte
|
||||||
var rtt time.Duration
|
var rtt time.Duration
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,6 @@ import (
|
||||||
"github.com/VividCortex/ewma"
|
"github.com/VividCortex/ewma"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
SizeEstimatorEwmaDecay = 100.0
|
|
||||||
)
|
|
||||||
|
|
||||||
type QuestionSizeEstimator struct {
|
type QuestionSizeEstimator struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
minQuestionSize int
|
minQuestionSize int
|
||||||
|
@ -17,7 +13,10 @@ type QuestionSizeEstimator struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewQuestionSizeEstimator() QuestionSizeEstimator {
|
func NewQuestionSizeEstimator() QuestionSizeEstimator {
|
||||||
return QuestionSizeEstimator{minQuestionSize: InitialMinQuestionSize, ewma: ewma.NewMovingAverage(SizeEstimatorEwmaDecay)}
|
return QuestionSizeEstimator{
|
||||||
|
minQuestionSize: InitialMinQuestionSize,
|
||||||
|
ewma: &ewma.SimpleEWMA{},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (questionSizeEstimator *QuestionSizeEstimator) MinQuestionSize() int {
|
func (questionSizeEstimator *QuestionSizeEstimator) MinQuestionSize() int {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
## going through a captive portal.
|
## going through a captive portal.
|
||||||
##
|
##
|
||||||
## This is a list of hard-coded IP addresses that will be returned when queries
|
## This is a list of hard-coded IP addresses that will be returned when queries
|
||||||
## for these names are received, even before the operating system an interface
|
## for these names are received, even before the operating system reports an interface
|
||||||
## as usable for reaching the Internet.
|
## as usable for reaching the Internet.
|
||||||
##
|
##
|
||||||
## Note that IPv6 addresses don't need to be specified within brackets,
|
## Note that IPv6 addresses don't need to be specified within brackets,
|
||||||
|
@ -21,3 +21,7 @@ dns.msftncsi.com 131.107.255.255, fd3e:4f5a:5b81::1
|
||||||
www.msftconnecttest.com 13.107.4.52
|
www.msftconnecttest.com 13.107.4.52
|
||||||
ipv6.msftconnecttest.com 2a01:111:2003::52
|
ipv6.msftconnecttest.com 2a01:111:2003::52
|
||||||
ipv4only.arpa 192.0.0.170, 192.0.0.171
|
ipv4only.arpa 192.0.0.170, 192.0.0.171
|
||||||
|
|
||||||
|
## Adding IP addresses of NTP servers is also a good idea
|
||||||
|
|
||||||
|
time.google.com 216.239.35.0, 2001:4860:4806::
|
||||||
|
|
|
@ -35,3 +35,10 @@ localhost ::1
|
||||||
# ads.* 192.168.100.1
|
# ads.* 192.168.100.1
|
||||||
# ads.* 192.168.100.2
|
# ads.* 192.168.100.2
|
||||||
# ads.* ::1
|
# ads.* ::1
|
||||||
|
|
||||||
|
# PTR records can be created by setting cloak_ptr in the main configuration file
|
||||||
|
# Entries with wild cards will not have PTR records created, but multiple
|
||||||
|
# names for the same IP are supported
|
||||||
|
|
||||||
|
# example.com 192.168.100.1
|
||||||
|
# my.example.com 192.168.100.1
|
||||||
|
|
|
@ -97,6 +97,13 @@ disabled_server_names = []
|
||||||
force_tcp = false
|
force_tcp = false
|
||||||
|
|
||||||
|
|
||||||
|
## Enable *experimental* support for HTTP/3 (DoH3, HTTP over QUIC)
|
||||||
|
## Note that, like DNSCrypt but unlike other HTTP versions, this uses
|
||||||
|
## UDP and (usually) port 443 instead of TCP.
|
||||||
|
|
||||||
|
http3 = false
|
||||||
|
|
||||||
|
|
||||||
## SOCKS proxy
|
## SOCKS proxy
|
||||||
## Uncomment the following line to route all TCP connections to a local Tor node
|
## Uncomment the following line to route all TCP connections to a local Tor node
|
||||||
## Tor doesn't support UDP, so set `force_tcp` to `true` as well.
|
## Tor doesn't support UDP, so set `force_tcp` to `true` as well.
|
||||||
|
@ -118,7 +125,7 @@ force_tcp = false
|
||||||
timeout = 5000
|
timeout = 5000
|
||||||
|
|
||||||
|
|
||||||
## Keepalive for HTTP (HTTPS, HTTP/2) queries, in seconds
|
## Keepalive for HTTP (HTTPS, HTTP/2, HTTP/3) queries, in seconds
|
||||||
|
|
||||||
keepalive = 30
|
keepalive = 30
|
||||||
|
|
||||||
|
@ -128,7 +135,7 @@ keepalive = 30
|
||||||
## Multiple networks can be listed; they will be randomly chosen.
|
## Multiple networks can be listed; they will be randomly chosen.
|
||||||
## These networks don't have to match your actual networks.
|
## These networks don't have to match your actual networks.
|
||||||
|
|
||||||
# edns_client_subnet = ["0.0.0.0/0", "2001:db8::/32"]
|
# edns_client_subnet = ['0.0.0.0/0', '2001:db8::/32']
|
||||||
|
|
||||||
|
|
||||||
## Response for blocked queries. Options are `refused`, `hinfo` (default) or
|
## Response for blocked queries. Options are `refused`, `hinfo` (default) or
|
||||||
|
@ -176,11 +183,24 @@ keepalive = 30
|
||||||
# use_syslog = true
|
# use_syslog = true
|
||||||
|
|
||||||
|
|
||||||
|
## The maximum concurrency to reload certificates from the resolvers.
|
||||||
|
## Default is 10.
|
||||||
|
|
||||||
|
# cert_refresh_concurrency = 10
|
||||||
|
|
||||||
|
|
||||||
## Delay, in minutes, after which certificates are reloaded
|
## Delay, in minutes, after which certificates are reloaded
|
||||||
|
|
||||||
cert_refresh_delay = 240
|
cert_refresh_delay = 240
|
||||||
|
|
||||||
|
|
||||||
|
## Initially don't check DNSCrypt server certificates for expiration, and
|
||||||
|
## only start checking them after a first successful connection to a resolver.
|
||||||
|
## This can be useful on routers with no battery-backed clock.
|
||||||
|
|
||||||
|
# cert_ignore_timestamp = false
|
||||||
|
|
||||||
|
|
||||||
## DNSCrypt: Create a new, unique key for every single DNS query
|
## DNSCrypt: Create a new, unique key for every single DNS query
|
||||||
## This may improve privacy but can also have a significant impact on CPU usage
|
## This may improve privacy but can also have a significant impact on CPU usage
|
||||||
## Only enable if you don't have a lot of network load
|
## Only enable if you don't have a lot of network load
|
||||||
|
@ -193,24 +213,30 @@ cert_refresh_delay = 240
|
||||||
# tls_disable_session_tickets = false
|
# tls_disable_session_tickets = false
|
||||||
|
|
||||||
|
|
||||||
## DoH: Use a specific cipher suite instead of the server preference
|
## DoH: Use TLS 1.2 and specific cipher suite instead of the server preference
|
||||||
## 49199 = TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
## 49199 = TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
||||||
## 49195 = TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
## 49195 = TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
||||||
## 52392 = TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
|
## 52392 = TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
|
||||||
## 52393 = TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
|
## 52393 = TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
|
||||||
## 4865 = TLS_AES_128_GCM_SHA256
|
|
||||||
## 4867 = TLS_CHACHA20_POLY1305_SHA256
|
|
||||||
##
|
##
|
||||||
## On non-Intel CPUs such as MIPS routers and ARM systems (Android, Raspberry Pi...),
|
## On non-Intel CPUs such as MIPS routers and ARM systems (Android, Raspberry Pi...),
|
||||||
## the following suite improves performance.
|
## the following suite improves performance.
|
||||||
## This may also help on Intel CPUs running 32-bit operating systems.
|
## This may also help on Intel CPUs running 32-bit operating systems.
|
||||||
##
|
##
|
||||||
## Keep tls_cipher_suite empty if you have issues fetching sources or
|
## Keep tls_cipher_suite empty if you have issues fetching sources or
|
||||||
## connecting to some DoH servers. Google and Cloudflare are fine with it.
|
## connecting to some DoH servers.
|
||||||
|
|
||||||
# tls_cipher_suite = [52392, 49199]
|
# tls_cipher_suite = [52392, 49199]
|
||||||
|
|
||||||
|
|
||||||
|
## Log TLS key material to a file, for debugging purposes only.
|
||||||
|
## This file will contain the TLS master key, which can be used to decrypt
|
||||||
|
## all TLS traffic to/from DoH servers.
|
||||||
|
## Never ever enable except for debugging purposes with a tool such as mitmproxy.
|
||||||
|
|
||||||
|
# tls_key_log_file = '/tmp/keylog.txt'
|
||||||
|
|
||||||
|
|
||||||
## Bootstrap resolvers
|
## Bootstrap resolvers
|
||||||
##
|
##
|
||||||
## These are normal, non-encrypted DNS resolvers, that will be only used
|
## These are normal, non-encrypted DNS resolvers, that will be only used
|
||||||
|
@ -241,10 +267,20 @@ cert_refresh_delay = 240
|
||||||
## not be sent there. If you're using DNSCrypt or Anonymized DNS and your
|
## not be sent there. If you're using DNSCrypt or Anonymized DNS and your
|
||||||
## lists are up to date, these resolvers will not even be used.
|
## lists are up to date, these resolvers will not even be used.
|
||||||
|
|
||||||
bootstrap_resolvers = ['9.9.9.9:53', '8.8.8.8:53']
|
bootstrap_resolvers = ['9.9.9.11:53', '8.8.8.8:53']
|
||||||
|
|
||||||
|
|
||||||
## Always use the bootstrap resolver before the system DNS settings.
|
## When internal DNS resolution is required, for example to retrieve
|
||||||
|
## the resolvers list:
|
||||||
|
##
|
||||||
|
## - queries will be sent to dnscrypt-proxy itself, if it is already
|
||||||
|
## running with active servers (*)
|
||||||
|
## - or else, queries will be sent to fallback servers
|
||||||
|
## - finally, if `ignore_system_dns` is `false`, queries will be sent
|
||||||
|
## to the system DNS
|
||||||
|
##
|
||||||
|
## (*) this is incompatible with systemd sockets.
|
||||||
|
## `listen_addrs` must not be empty.
|
||||||
|
|
||||||
ignore_system_dns = true
|
ignore_system_dns = true
|
||||||
|
|
||||||
|
@ -318,6 +354,7 @@ block_ipv6 = false
|
||||||
|
|
||||||
|
|
||||||
## Immediately respond to A and AAAA queries for host names without a domain name
|
## Immediately respond to A and AAAA queries for host names without a domain name
|
||||||
|
## This also prevents "dotless domain names" from being resolved upstream.
|
||||||
|
|
||||||
block_unqualified = true
|
block_unqualified = true
|
||||||
|
|
||||||
|
@ -352,6 +389,8 @@ reject_ttl = 10
|
||||||
## Cloaking returns a predefined address for a specific name.
|
## Cloaking returns a predefined address for a specific name.
|
||||||
## In addition to acting as a HOSTS file, it can also return the IP address
|
## In addition to acting as a HOSTS file, it can also return the IP address
|
||||||
## of a different name. It will also do CNAME flattening.
|
## of a different name. It will also do CNAME flattening.
|
||||||
|
## If 'cloak_ptr' is set, then PTR (reverse lookups) are enabled
|
||||||
|
## for cloaking rules that do not contain wild cards.
|
||||||
##
|
##
|
||||||
## See the `example-cloaking-rules.txt` file for an example
|
## See the `example-cloaking-rules.txt` file for an example
|
||||||
|
|
||||||
|
@ -360,6 +399,7 @@ reject_ttl = 10
|
||||||
## TTL used when serving entries in cloaking-rules.txt
|
## TTL used when serving entries in cloaking-rules.txt
|
||||||
|
|
||||||
# cloak_ttl = 600
|
# cloak_ttl = 600
|
||||||
|
# cloak_ptr = false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -436,6 +476,8 @@ cache_neg_max_ttl = 600
|
||||||
|
|
||||||
|
|
||||||
## Certificate file and key - Note that the certificate has to be trusted.
|
## Certificate file and key - Note that the certificate has to be trusted.
|
||||||
|
## Can be generated using the following command:
|
||||||
|
## openssl req -x509 -nodes -newkey rsa:2048 -days 5000 -sha256 -keyout localhost.pem -out localhost.pem
|
||||||
## See the documentation (wiki) for more information.
|
## See the documentation (wiki) for more information.
|
||||||
|
|
||||||
# cert_file = 'localhost.pem'
|
# cert_file = 'localhost.pem'
|
||||||
|
@ -451,20 +493,20 @@ cache_neg_max_ttl = 600
|
||||||
|
|
||||||
[query_log]
|
[query_log]
|
||||||
|
|
||||||
## Path to the query log file (absolute, or relative to the same directory as the config file)
|
## Path to the query log file (absolute, or relative to the same directory as the config file)
|
||||||
## Can be set to /dev/stdout in order to log to the standard output.
|
## Can be set to /dev/stdout in order to log to the standard output.
|
||||||
|
|
||||||
# file = 'query.log'
|
# file = 'query.log'
|
||||||
|
|
||||||
|
|
||||||
## Query log format (currently supported: tsv and ltsv)
|
## Query log format (currently supported: tsv and ltsv)
|
||||||
|
|
||||||
format = 'tsv'
|
format = 'tsv'
|
||||||
|
|
||||||
|
|
||||||
## Do not log these query types, to reduce verbosity. Keep empty to log everything.
|
## Do not log these query types, to reduce verbosity. Keep empty to log everything.
|
||||||
|
|
||||||
# ignored_qtypes = ['DNSKEY', 'NS']
|
# ignored_qtypes = ['DNSKEY', 'NS']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -478,19 +520,19 @@ cache_neg_max_ttl = 600
|
||||||
|
|
||||||
[nx_log]
|
[nx_log]
|
||||||
|
|
||||||
## Path to the query log file (absolute, or relative to the same directory as the config file)
|
## Path to the query log file (absolute, or relative to the same directory as the config file)
|
||||||
|
|
||||||
# file = 'nx.log'
|
# file = 'nx.log'
|
||||||
|
|
||||||
|
|
||||||
## Query log format (currently supported: tsv and ltsv)
|
## Query log format (currently supported: tsv and ltsv)
|
||||||
|
|
||||||
format = 'tsv'
|
format = 'tsv'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
# Pattern-based blocking (blocklists) #
|
# Pattern-based blocking (blocklists) #
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
## Blocklists are made of one pattern per line. Example of valid patterns:
|
## Blocklists are made of one pattern per line. Example of valid patterns:
|
||||||
|
@ -508,19 +550,19 @@ cache_neg_max_ttl = 600
|
||||||
|
|
||||||
[blocked_names]
|
[blocked_names]
|
||||||
|
|
||||||
## Path to the file of blocking rules (absolute, or relative to the same directory as the config file)
|
## Path to the file of blocking rules (absolute, or relative to the same directory as the config file)
|
||||||
|
|
||||||
# blocked_names_file = 'blocked-names.txt'
|
# blocked_names_file = 'blocked-names.txt'
|
||||||
|
|
||||||
|
|
||||||
## Optional path to a file logging blocked queries
|
## Optional path to a file logging blocked queries
|
||||||
|
|
||||||
# log_file = 'blocked-names.log'
|
# log_file = 'blocked-names.log'
|
||||||
|
|
||||||
|
|
||||||
## Optional log format: tsv or ltsv (default: tsv)
|
## Optional log format: tsv or ltsv (default: tsv)
|
||||||
|
|
||||||
# log_format = 'tsv'
|
# log_format = 'tsv'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -536,24 +578,24 @@ cache_neg_max_ttl = 600
|
||||||
|
|
||||||
[blocked_ips]
|
[blocked_ips]
|
||||||
|
|
||||||
## Path to the file of blocking rules (absolute, or relative to the same directory as the config file)
|
## Path to the file of blocking rules (absolute, or relative to the same directory as the config file)
|
||||||
|
|
||||||
# blocked_ips_file = 'blocked-ips.txt'
|
# blocked_ips_file = 'blocked-ips.txt'
|
||||||
|
|
||||||
|
|
||||||
## Optional path to a file logging blocked queries
|
## Optional path to a file logging blocked queries
|
||||||
|
|
||||||
# log_file = 'blocked-ips.log'
|
# log_file = 'blocked-ips.log'
|
||||||
|
|
||||||
|
|
||||||
## Optional log format: tsv or ltsv (default: tsv)
|
## Optional log format: tsv or ltsv (default: tsv)
|
||||||
|
|
||||||
# log_format = 'tsv'
|
# log_format = 'tsv'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
# Pattern-based allow lists (blocklists bypass) #
|
# Pattern-based allow lists (blocklists bypass) #
|
||||||
######################################################
|
######################################################
|
||||||
|
|
||||||
## Allowlists support the same patterns as blocklists
|
## Allowlists support the same patterns as blocklists
|
||||||
|
@ -564,19 +606,19 @@ cache_neg_max_ttl = 600
|
||||||
|
|
||||||
[allowed_names]
|
[allowed_names]
|
||||||
|
|
||||||
## Path to the file of allow list rules (absolute, or relative to the same directory as the config file)
|
## Path to the file of allow list rules (absolute, or relative to the same directory as the config file)
|
||||||
|
|
||||||
# allowed_names_file = 'allowed-names.txt'
|
# allowed_names_file = 'allowed-names.txt'
|
||||||
|
|
||||||
|
|
||||||
## Optional path to a file logging allowed queries
|
## Optional path to a file logging allowed queries
|
||||||
|
|
||||||
# log_file = 'allowed-names.log'
|
# log_file = 'allowed-names.log'
|
||||||
|
|
||||||
|
|
||||||
## Optional log format: tsv or ltsv (default: tsv)
|
## Optional log format: tsv or ltsv (default: tsv)
|
||||||
|
|
||||||
# log_format = 'tsv'
|
# log_format = 'tsv'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -585,25 +627,25 @@ cache_neg_max_ttl = 600
|
||||||
#########################################################
|
#########################################################
|
||||||
|
|
||||||
## Allowed IP lists support the same patterns as IP blocklists
|
## Allowed IP lists support the same patterns as IP blocklists
|
||||||
## If an IP response matches an allow ip entry, the corresponding session
|
## If an IP response matches an allowed entry, the corresponding session
|
||||||
## will bypass IP filters.
|
## will bypass IP filters.
|
||||||
##
|
##
|
||||||
## Time-based rules are also supported to make some websites only accessible at specific times of the day.
|
## Time-based rules are also supported to make some websites only accessible at specific times of the day.
|
||||||
|
|
||||||
[allowed_ips]
|
[allowed_ips]
|
||||||
|
|
||||||
## Path to the file of allowed ip rules (absolute, or relative to the same directory as the config file)
|
## Path to the file of allowed ip rules (absolute, or relative to the same directory as the config file)
|
||||||
|
|
||||||
# allowed_ips_file = 'allowed-ips.txt'
|
# allowed_ips_file = 'allowed-ips.txt'
|
||||||
|
|
||||||
|
|
||||||
## Optional path to a file logging allowed queries
|
## Optional path to a file logging allowed queries
|
||||||
|
|
||||||
# log_file = 'allowed-ips.log'
|
# log_file = 'allowed-ips.log'
|
||||||
|
|
||||||
## Optional log format: tsv or ltsv (default: tsv)
|
## Optional log format: tsv or ltsv (default: tsv)
|
||||||
|
|
||||||
# log_format = 'tsv'
|
# log_format = 'tsv'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -624,21 +666,21 @@ cache_neg_max_ttl = 600
|
||||||
|
|
||||||
[schedules]
|
[schedules]
|
||||||
|
|
||||||
# [schedules.'time-to-sleep']
|
# [schedules.time-to-sleep]
|
||||||
# mon = [{after='21:00', before='7:00'}]
|
# mon = [{after='21:00', before='7:00'}]
|
||||||
# tue = [{after='21:00', before='7:00'}]
|
# tue = [{after='21:00', before='7:00'}]
|
||||||
# wed = [{after='21:00', before='7:00'}]
|
# wed = [{after='21:00', before='7:00'}]
|
||||||
# thu = [{after='21:00', before='7:00'}]
|
# thu = [{after='21:00', before='7:00'}]
|
||||||
# fri = [{after='23:00', before='7:00'}]
|
# fri = [{after='23:00', before='7:00'}]
|
||||||
# sat = [{after='23:00', before='7:00'}]
|
# sat = [{after='23:00', before='7:00'}]
|
||||||
# sun = [{after='21:00', before='7:00'}]
|
# sun = [{after='21:00', before='7:00'}]
|
||||||
|
|
||||||
# [schedules.'work']
|
# [schedules.work]
|
||||||
# mon = [{after='9:00', before='18:00'}]
|
# mon = [{after='9:00', before='18:00'}]
|
||||||
# tue = [{after='9:00', before='18:00'}]
|
# tue = [{after='9:00', before='18:00'}]
|
||||||
# wed = [{after='9:00', before='18:00'}]
|
# wed = [{after='9:00', before='18:00'}]
|
||||||
# thu = [{after='9:00', before='18:00'}]
|
# thu = [{after='9:00', before='18:00'}]
|
||||||
# fri = [{after='9:00', before='17:00'}]
|
# fri = [{after='9:00', before='17:00'}]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -660,46 +702,46 @@ cache_neg_max_ttl = 600
|
||||||
## If the `urls` property is missing, cache files and valid signatures
|
## If the `urls` property is missing, cache files and valid signatures
|
||||||
## must already be present. This doesn't prevent these cache files from
|
## must already be present. This doesn't prevent these cache files from
|
||||||
## expiring after `refresh_delay` hours.
|
## expiring after `refresh_delay` hours.
|
||||||
## Cache freshness is checked every 24 hours, so values for 'refresh_delay'
|
## `refreshed_delay` must be in the [24..168] interval.
|
||||||
## of less than 24 hours will have no effect.
|
## The minimum delay of 24 hours (1 day) avoids unnecessary requests to servers.
|
||||||
## A maximum delay of 168 hours (1 week) is imposed to ensure cache freshness.
|
## The maximum delay of 168 hours (1 week) ensures cache freshness.
|
||||||
|
|
||||||
[sources]
|
[sources]
|
||||||
|
|
||||||
## An example of a remote source from https://github.com/DNSCrypt/dnscrypt-resolvers
|
### An example of a remote source from https://github.com/DNSCrypt/dnscrypt-resolvers
|
||||||
|
|
||||||
[sources.'public-resolvers']
|
[sources.public-resolvers]
|
||||||
urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md', 'https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/public-resolvers.md', 'https://download.dnscrypt.net/resolvers-list/v3/public-resolvers.md']
|
urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/public-resolvers.md', 'https://download.dnscrypt.info/resolvers-list/v3/public-resolvers.md']
|
||||||
cache_file = 'public-resolvers.md'
|
cache_file = 'public-resolvers.md'
|
||||||
minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
|
minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
|
||||||
refresh_delay = 72
|
refresh_delay = 72
|
||||||
prefix = ''
|
prefix = ''
|
||||||
|
|
||||||
## Anonymized DNS relays
|
### Anonymized DNS relays
|
||||||
|
|
||||||
[sources.'relays']
|
[sources.relays]
|
||||||
urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/relays.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/relays.md', 'https://download.dnscrypt.net/resolvers-list/v3/relays.md']
|
urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/relays.md']
|
||||||
cache_file = 'relays.md'
|
cache_file = 'relays.md'
|
||||||
minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
|
minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
|
||||||
refresh_delay = 72
|
refresh_delay = 72
|
||||||
prefix = ''
|
prefix = ''
|
||||||
|
|
||||||
## ODoH (Oblivious DoH) servers and relays
|
### ODoH (Oblivious DoH) servers and relays
|
||||||
|
|
||||||
# [sources.'odoh-servers']
|
# [sources.odoh-servers]
|
||||||
# urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/odoh-servers.md', 'https://download.dnscrypt.info/resolvers-list/v3/odoh-servers.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/odoh-servers.md', 'https://download.dnscrypt.net/resolvers-list/v3/odoh-servers.md']
|
# urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/odoh-servers.md', 'https://download.dnscrypt.info/resolvers-list/v3/odoh-servers.md']
|
||||||
# cache_file = 'odoh-servers.md'
|
# cache_file = 'odoh-servers.md'
|
||||||
# minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
|
# minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
|
||||||
# refresh_delay = 24
|
# refresh_delay = 24
|
||||||
# prefix = ''
|
# prefix = ''
|
||||||
# [sources.'odoh-relays']
|
# [sources.odoh-relays]
|
||||||
# urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/odoh-relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/odoh-relays.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/odoh-relays.md', 'https://download.dnscrypt.net/resolvers-list/v3/odoh-relays.md']
|
# urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/odoh-relays.md', 'https://download.dnscrypt.info/resolvers-list/v3/odoh-relays.md']
|
||||||
# cache_file = 'odoh-relays.md'
|
# cache_file = 'odoh-relays.md'
|
||||||
# minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
|
# minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
|
||||||
# refresh_delay = 24
|
# refresh_delay = 24
|
||||||
# prefix = ''
|
# prefix = ''
|
||||||
|
|
||||||
## Quad9
|
### Quad9
|
||||||
|
|
||||||
# [sources.quad9-resolvers]
|
# [sources.quad9-resolvers]
|
||||||
# urls = ['https://www.quad9.net/quad9-resolvers.md']
|
# urls = ['https://www.quad9.net/quad9-resolvers.md']
|
||||||
|
@ -707,14 +749,22 @@ cache_neg_max_ttl = 600
|
||||||
# cache_file = 'quad9-resolvers.md'
|
# cache_file = 'quad9-resolvers.md'
|
||||||
# prefix = 'quad9-'
|
# prefix = 'quad9-'
|
||||||
|
|
||||||
## Another example source, with resolvers censoring some websites not appropriate for children
|
### Another example source, with resolvers censoring some websites not appropriate for children
|
||||||
## This is a subset of the `public-resolvers` list, so enabling both is useless
|
### This is a subset of the `public-resolvers` list, so enabling both is useless.
|
||||||
|
|
||||||
# [sources.'parental-control']
|
# [sources.parental-control]
|
||||||
# urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/parental-control.md', 'https://download.dnscrypt.info/resolvers-list/v3/parental-control.md', 'https://ipv6.download.dnscrypt.info/resolvers-list/v3/parental-control.md', 'https://download.dnscrypt.net/resolvers-list/v3/parental-control.md']
|
# urls = ['https://raw.githubusercontent.com/DNSCrypt/dnscrypt-resolvers/master/v3/parental-control.md', 'https://download.dnscrypt.info/resolvers-list/v3/parental-control.md']
|
||||||
# cache_file = 'parental-control.md'
|
# cache_file = 'parental-control.md'
|
||||||
# minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
|
# minisign_key = 'RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3'
|
||||||
|
|
||||||
|
### dnscry.pt servers - See https://www.dnscry.pt
|
||||||
|
|
||||||
|
# [sources.dnscry-pt-resolvers]
|
||||||
|
# urls = ["https://www.dnscry.pt/resolvers.md"]
|
||||||
|
# minisign_key = "RWQM31Nwkqh01x88SvrBL8djp1NH56Rb4mKLHz16K7qsXgEomnDv6ziQ"
|
||||||
|
# cache_file = "dnscry.pt-resolvers.md"
|
||||||
|
# refresh_delay = 72
|
||||||
|
# prefix = "dnscry.pt-"
|
||||||
|
|
||||||
|
|
||||||
#########################################
|
#########################################
|
||||||
|
@ -723,16 +773,16 @@ cache_neg_max_ttl = 600
|
||||||
|
|
||||||
[broken_implementations]
|
[broken_implementations]
|
||||||
|
|
||||||
# Cisco servers currently cannot handle queries larger than 1472 bytes, and don't
|
## Cisco servers currently cannot handle queries larger than 1472 bytes, and don't
|
||||||
# truncate reponses larger than questions as expected by the DNSCrypt protocol.
|
## truncate responses larger than questions as expected by the DNSCrypt protocol.
|
||||||
# This prevents large responses from being received over UDP and over relays.
|
## This prevents large responses from being received over UDP and over relays.
|
||||||
#
|
##
|
||||||
# Older versions of the `dnsdist` server software had a bug with queries larger
|
## Older versions of the `dnsdist` server software had a bug with queries larger
|
||||||
# than 1500 bytes. This is fixed since `dnsdist` version 1.5.0, but
|
## than 1500 bytes. This is fixed since `dnsdist` version 1.5.0, but
|
||||||
# some server may still run an outdated version.
|
## some server may still run an outdated version.
|
||||||
#
|
##
|
||||||
# The list below enables workarounds to make non-relayed usage more reliable
|
## The list below enables workarounds to make non-relayed usage more reliable
|
||||||
# until the servers are fixed.
|
## until the servers are fixed.
|
||||||
|
|
||||||
fragments_blocked = ['cisco', 'cisco-ipv6', 'cisco-familyshield', 'cisco-familyshield-ipv6', 'cleanbrowsing-adult', 'cleanbrowsing-adult-ipv6', 'cleanbrowsing-family', 'cleanbrowsing-family-ipv6', 'cleanbrowsing-security', 'cleanbrowsing-security-ipv6']
|
fragments_blocked = ['cisco', 'cisco-ipv6', 'cisco-familyshield', 'cisco-familyshield-ipv6', 'cleanbrowsing-adult', 'cleanbrowsing-adult-ipv6', 'cleanbrowsing-family', 'cleanbrowsing-family-ipv6', 'cleanbrowsing-security', 'cleanbrowsing-security-ipv6']
|
||||||
|
|
||||||
|
@ -742,15 +792,14 @@ fragments_blocked = ['cisco', 'cisco-ipv6', 'cisco-familyshield', 'cisco-familys
|
||||||
# Certificate-based client authentication for DoH #
|
# Certificate-based client authentication for DoH #
|
||||||
#################################################################
|
#################################################################
|
||||||
|
|
||||||
# Use a X509 certificate to authenticate yourself when connecting to DoH servers.
|
## Use a X509 certificate to authenticate yourself when connecting to DoH servers.
|
||||||
# This is only useful if you are operating your own, private DoH server(s).
|
## This is only useful if you are operating your own, private DoH server(s).
|
||||||
# 'creds' maps servers to certificates, and supports multiple entries.
|
## 'creds' maps servers to certificates, and supports multiple entries.
|
||||||
# If you are not using the standard root CA, an optional "root_ca"
|
## If you are not using the standard root CA, an optional "root_ca"
|
||||||
# property set to the path to a root CRT file can be added to a server entry.
|
## property set to the path to a root CRT file can be added to a server entry.
|
||||||
|
|
||||||
[doh_client_x509_auth]
|
[doh_client_x509_auth]
|
||||||
|
|
||||||
#
|
|
||||||
# creds = [
|
# creds = [
|
||||||
# { server_name='*', client_cert='client.crt', client_key='client.key' }
|
# { server_name='*', client_cert='client.crt', client_key='client.key' }
|
||||||
# ]
|
# ]
|
||||||
|
@ -798,14 +847,14 @@ fragments_blocked = ['cisco', 'cisco-ipv6', 'cisco-familyshield', 'cisco-familys
|
||||||
# ]
|
# ]
|
||||||
|
|
||||||
|
|
||||||
# Skip resolvers incompatible with anonymization instead of using them directly
|
## Skip resolvers incompatible with anonymization instead of using them directly
|
||||||
|
|
||||||
skip_incompatible = false
|
skip_incompatible = false
|
||||||
|
|
||||||
|
|
||||||
# If public server certificates for a non-conformant server cannot be
|
## If public server certificates for a non-conformant server cannot be
|
||||||
# retrieved via a relay, try getting them directly. Actual queries
|
## retrieved via a relay, try getting them directly. Actual queries
|
||||||
# will then always go through relays.
|
## will then always go through relays.
|
||||||
|
|
||||||
# direct_cert_fallback = false
|
# direct_cert_fallback = false
|
||||||
|
|
||||||
|
@ -833,13 +882,15 @@ skip_incompatible = false
|
||||||
|
|
||||||
[dns64]
|
[dns64]
|
||||||
|
|
||||||
## (Option 1) Static prefix(es) as Pref64::/n CIDRs.
|
## Static prefix(es) as Pref64::/n CIDRs
|
||||||
|
|
||||||
# prefix = ['64:ff9b::/96']
|
# prefix = ['64:ff9b::/96']
|
||||||
|
|
||||||
## (Option 2) DNS64-enabled resolver(s) to discover Pref64::/n CIDRs.
|
## DNS64-enabled resolver(s) to discover Pref64::/n CIDRs
|
||||||
## These resolvers are used to query for Well-Known IPv4-only Name (WKN) "ipv4only.arpa." to discover only.
|
## These resolvers are used to query for Well-Known IPv4-only Name (WKN) "ipv4only.arpa." to discover only.
|
||||||
## Set with your ISP's resolvers in case of custom prefixes (other than Well-Known Prefix 64:ff9b::/96).
|
## Set with your ISP's resolvers in case of custom prefixes (other than Well-Known Prefix 64:ff9b::/96).
|
||||||
## IMPORTANT: Default resolvers listed below support Well-Known Prefix 64:ff9b::/96 only.
|
## IMPORTANT: Default resolvers listed below support Well-Known Prefix 64:ff9b::/96 only.
|
||||||
|
|
||||||
# resolver = ['[2606:4700:4700::64]:53', '[2001:4860:4860::64]:53']
|
# resolver = ['[2606:4700:4700::64]:53', '[2001:4860:4860::64]:53']
|
||||||
|
|
||||||
|
|
||||||
|
@ -853,5 +904,5 @@ skip_incompatible = false
|
||||||
|
|
||||||
[static]
|
[static]
|
||||||
|
|
||||||
# [static.'myserver']
|
# [static.myserver]
|
||||||
# stamp = 'sdns://AQcAAAAAAAAAAAAQMi5kbnNjcnlwdC1jZXJ0Lg'
|
# stamp = 'sdns://AQcAAAAAAAAAAAAQMi5kbnNjcnlwdC1jZXJ0Lg'
|
||||||
|
|
|
@ -14,12 +14,23 @@
|
||||||
## If this happens, set `block_ipv6` to `false` in the main config file.
|
## If this happens, set `block_ipv6` to `false` in the main config file.
|
||||||
|
|
||||||
## Forward *.lan, *.local, *.home, *.home.arpa, *.internal and *.localdomain to 192.168.1.1
|
## Forward *.lan, *.local, *.home, *.home.arpa, *.internal and *.localdomain to 192.168.1.1
|
||||||
# lan 192.168.1.1
|
# lan 192.168.1.1
|
||||||
# local 192.168.1.1
|
# local 192.168.1.1
|
||||||
# home 192.168.1.1
|
# home 192.168.1.1
|
||||||
# home.arpa 192.168.1.1
|
# home.arpa 192.168.1.1
|
||||||
# internal 192.168.1.1
|
# internal 192.168.1.1
|
||||||
# localdomain 192.168.1.1
|
# localdomain 192.168.1.1
|
||||||
|
# 192.in-addr.arpa 192.168.1.1
|
||||||
|
|
||||||
## Forward queries for example.com and *.example.com to 9.9.9.9 and 8.8.8.8
|
## Forward queries for example.com and *.example.com to 9.9.9.9 and 8.8.8.8
|
||||||
# example.com 9.9.9.9,8.8.8.8
|
# example.com 9.9.9.9,8.8.8.8
|
||||||
|
|
||||||
|
## Forward queries to a resolver using IPv6
|
||||||
|
# ipv6.example.com [2001:DB8::42]:53
|
||||||
|
|
||||||
|
## Forward queries for .onion names to a local Tor client
|
||||||
|
## Tor must be configured with the following in the torrc file:
|
||||||
|
## DNSPort 9053
|
||||||
|
## AutomapHostsOnResolve 1
|
||||||
|
|
||||||
|
# onion 127.0.0.1:9053
|
||||||
|
|
|
@ -5,8 +5,9 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
stamps "github.com/jedisct1/go-dnsstamps"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
stamps "github.com/jedisct1/go-dnsstamps"
|
||||||
)
|
)
|
||||||
|
|
||||||
func FuzzParseODoHTargetConfigs(f *testing.F) {
|
func FuzzParseODoHTargetConfigs(f *testing.F) {
|
||||||
|
@ -23,8 +24,12 @@ func FuzzParseODoHTargetConfigs(f *testing.F) {
|
||||||
func FuzzParseStampParser(f *testing.F) {
|
func FuzzParseStampParser(f *testing.F) {
|
||||||
f.Add("sdns://AgcAAAAAAAAACzEwNC4yMS42Ljc4AA1kb2guY3J5cHRvLnN4Ci9kbnMtcXVlcnk")
|
f.Add("sdns://AgcAAAAAAAAACzEwNC4yMS42Ljc4AA1kb2guY3J5cHRvLnN4Ci9kbnMtcXVlcnk")
|
||||||
f.Add("sdns://AgcAAAAAAAAAGlsyNjA2OjQ3MDA6MzAzNzo6NjgxNTo2NGVdABJkb2gtaXB2Ni5jcnlwdG8uc3gKL2Rucy1xdWVyeQ")
|
f.Add("sdns://AgcAAAAAAAAAGlsyNjA2OjQ3MDA6MzAzNzo6NjgxNTo2NGVdABJkb2gtaXB2Ni5jcnlwdG8uc3gKL2Rucy1xdWVyeQ")
|
||||||
f.Add("sdns://AQcAAAAAAAAADTUxLjE1LjEyMi4yNTAg6Q3ZfapcbHgiHKLF7QFoli0Ty1Vsz3RXs1RUbxUrwZAcMi5kbnNjcnlwdC1jZXJ0LnNjYWxld2F5LWFtcw")
|
f.Add(
|
||||||
f.Add("sdns://AQcAAAAAAAAAFlsyMDAxOmJjODoxODIwOjUwZDo6MV0g6Q3ZfapcbHgiHKLF7QFoli0Ty1Vsz3RXs1RUbxUrwZAcMi5kbnNjcnlwdC1jZXJ0LnNjYWxld2F5LWFtcw")
|
"sdns://AQcAAAAAAAAADTUxLjE1LjEyMi4yNTAg6Q3ZfapcbHgiHKLF7QFoli0Ty1Vsz3RXs1RUbxUrwZAcMi5kbnNjcnlwdC1jZXJ0LnNjYWxld2F5LWFtcw",
|
||||||
|
)
|
||||||
|
f.Add(
|
||||||
|
"sdns://AQcAAAAAAAAAFlsyMDAxOmJjODoxODIwOjUwZDo6MV0g6Q3ZfapcbHgiHKLF7QFoli0Ty1Vsz3RXs1RUbxUrwZAcMi5kbnNjcnlwdC1jZXJ0LnNjYWxld2F5LWFtcw",
|
||||||
|
)
|
||||||
f.Add("sdns://gQ8xNjMuMTcyLjE4MC4xMjU")
|
f.Add("sdns://gQ8xNjMuMTcyLjE4MC4xMjU")
|
||||||
f.Add("sdns://BQcAAAAAAAAADm9kb2guY3J5cHRvLnN4Ci9kbnMtcXVlcnk")
|
f.Add("sdns://BQcAAAAAAAAADm9kb2guY3J5cHRvLnN4Ci9kbnMtcXVlcnk")
|
||||||
f.Add("sdns://hQcAAAAAAAAAACCi3jNJDEdtNW4tvHN8J3lpIklSa2Wrj7qaNCgEgci9_BpvZG9oLXJlbGF5LmVkZ2Vjb21wdXRlLmFwcAEv")
|
f.Add("sdns://hQcAAAAAAAAAACCi3jNJDEdtNW4tvHN8J3lpIklSa2Wrj7qaNCgEgci9_BpvZG9oLXJlbGF5LmVkZ2Vjb21wdXRlLmFwcAEv")
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -29,7 +30,27 @@ func (handler localDoHHandler) ServeHTTP(writer http.ResponseWriter, request *ht
|
||||||
writer.WriteHeader(404)
|
writer.WriteHeader(404)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if request.Header.Get("Content-Type") != dataType {
|
packet := []byte{}
|
||||||
|
var err error
|
||||||
|
start := time.Now()
|
||||||
|
if request.Method == "POST" &&
|
||||||
|
request.Header.Get("Content-Type") == dataType {
|
||||||
|
packet, err = io.ReadAll(io.LimitReader(request.Body, int64(MaxDNSPacketSize)))
|
||||||
|
if err != nil {
|
||||||
|
dlog.Warnf("No body in a local DoH query")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if request.Method == "GET" && request.Header.Get("Accept") == dataType {
|
||||||
|
encodedPacket := request.URL.Query().Get("dns")
|
||||||
|
if len(encodedPacket) >= MinDNSPacketSize*4/3 && len(encodedPacket) <= MaxDNSPacketSize*4/3 {
|
||||||
|
packet, err = base64.RawURLEncoding.DecodeString(encodedPacket)
|
||||||
|
if err != nil {
|
||||||
|
dlog.Warnf("Invalid base64 in a local DoH query")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(packet) < MinDNSPacketSize {
|
||||||
writer.Header().Set("Content-Type", "text/plain")
|
writer.Header().Set("Content-Type", "text/plain")
|
||||||
writer.WriteHeader(400)
|
writer.WriteHeader(400)
|
||||||
writer.Write([]byte("dnscrypt-proxy local DoH server\n"))
|
writer.Write([]byte("dnscrypt-proxy local DoH server\n"))
|
||||||
|
@ -41,12 +62,6 @@ func (handler localDoHHandler) ServeHTTP(writer http.ResponseWriter, request *ht
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
xClientAddr := net.Addr(clientAddr)
|
xClientAddr := net.Addr(clientAddr)
|
||||||
start := time.Now()
|
|
||||||
packet, err := ioutil.ReadAll(io.LimitReader(request.Body, MaxHTTPBodyLength))
|
|
||||||
if err != nil {
|
|
||||||
dlog.Warnf("No body in a local DoH query")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hasEDNS0Padding, err := hasEDNS0Padding(packet)
|
hasEDNS0Padding, err := hasEDNS0Padding(packet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writer.WriteHeader(400)
|
writer.WriteHeader(400)
|
||||||
|
@ -76,6 +91,7 @@ func (handler localDoHHandler) ServeHTTP(writer http.ResponseWriter, request *ht
|
||||||
writer.Header().Set("X-Pad", pad)
|
writer.Header().Set("X-Pad", pad)
|
||||||
}
|
}
|
||||||
writer.Header().Set("Content-Type", dataType)
|
writer.Header().Set("Content-Type", dataType)
|
||||||
|
writer.Header().Set("Content-Length", fmt.Sprint(len(response)))
|
||||||
writer.WriteHeader(200)
|
writer.WriteHeader(200)
|
||||||
writer.Write(response)
|
writer.Write(response)
|
||||||
}
|
}
|
||||||
|
@ -97,7 +113,25 @@ func (proxy *Proxy) localDoHListener(acceptPc *net.TCPListener) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func dohPaddedLen(unpaddedLen int) int {
|
func dohPaddedLen(unpaddedLen int) int {
|
||||||
boundaries := [...]int{64, 128, 192, 256, 320, 384, 512, 704, 768, 896, 960, 1024, 1088, 1152, 2688, 4080, MaxDNSPacketSize}
|
boundaries := [...]int{
|
||||||
|
64,
|
||||||
|
128,
|
||||||
|
192,
|
||||||
|
256,
|
||||||
|
320,
|
||||||
|
384,
|
||||||
|
512,
|
||||||
|
704,
|
||||||
|
768,
|
||||||
|
896,
|
||||||
|
960,
|
||||||
|
1024,
|
||||||
|
1088,
|
||||||
|
1152,
|
||||||
|
2688,
|
||||||
|
4080,
|
||||||
|
MaxDNSPacketSize,
|
||||||
|
}
|
||||||
for _, boundary := range boundaries {
|
for _, boundary := range boundaries {
|
||||||
if boundary >= unpaddedLen {
|
if boundary >= unpaddedLen {
|
||||||
return boundary
|
return boundary
|
||||||
|
|
|
@ -16,13 +16,25 @@ func Logger(logMaxSize int, logMaxAge int, logMaxBackups int, fileName string) i
|
||||||
if st.Mode().IsDir() {
|
if st.Mode().IsDir() {
|
||||||
dlog.Fatalf("[%v] is a directory", fileName)
|
dlog.Fatalf("[%v] is a directory", fileName)
|
||||||
}
|
}
|
||||||
fp, err := os.OpenFile(fileName, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
|
fp, err := os.OpenFile(fileName, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dlog.Fatalf("Unable to access [%v]: [%v]", fileName, err)
|
dlog.Fatalf("Unable to access [%v]: [%v]", fileName, err)
|
||||||
}
|
}
|
||||||
return fp
|
return fp
|
||||||
}
|
}
|
||||||
logger := &lumberjack.Logger{LocalTime: true, MaxSize: logMaxSize, MaxAge: logMaxAge, MaxBackups: logMaxBackups, Filename: fileName, Compress: true}
|
if fp, err := os.OpenFile(fileName, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o644); err == nil {
|
||||||
|
fp.Close()
|
||||||
|
} else {
|
||||||
|
dlog.Errorf("Unable to create [%v]: [%v]", fileName, err)
|
||||||
|
}
|
||||||
|
logger := &lumberjack.Logger{
|
||||||
|
LocalTime: true,
|
||||||
|
MaxSize: logMaxSize,
|
||||||
|
MaxAge: logMaxAge,
|
||||||
|
MaxBackups: logMaxBackups,
|
||||||
|
Filename: fileName,
|
||||||
|
Compress: true,
|
||||||
|
}
|
||||||
|
|
||||||
return logger
|
return logger
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AppVersion = "2.1.1"
|
AppVersion = "2.1.5"
|
||||||
DefaultConfigFileName = "dnscrypt-proxy.toml"
|
DefaultConfigFileName = "dnscrypt-proxy.toml"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,13 +27,18 @@ type App struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
TimezoneSetup()
|
tzErr := TimezoneSetup()
|
||||||
dlog.Init("dnscrypt-proxy", dlog.SeverityNotice, "DAEMON")
|
dlog.Init("dnscrypt-proxy", dlog.SeverityNotice, "DAEMON")
|
||||||
|
if tzErr != nil {
|
||||||
|
dlog.Warnf("Timezone setup failed: [%v]", tzErr)
|
||||||
|
}
|
||||||
runtime.MemProfileRate = 0
|
runtime.MemProfileRate = 0
|
||||||
|
|
||||||
seed := make([]byte, 8)
|
seed := make([]byte, 8)
|
||||||
crypto_rand.Read(seed)
|
if _, err := crypto_rand.Read(seed); err != nil {
|
||||||
rand.Seed(int64(binary.LittleEndian.Uint64(seed[:])))
|
dlog.Fatal(err)
|
||||||
|
}
|
||||||
|
rand.Seed(int64(binary.LittleEndian.Uint64(seed)))
|
||||||
|
|
||||||
pwd, err := os.Getwd()
|
pwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -46,6 +51,7 @@ func main() {
|
||||||
flags.Resolve = flag.String("resolve", "", "resolve a DNS name (string can be <name> or <name>,<resolver address>)")
|
flags.Resolve = flag.String("resolve", "", "resolve a DNS name (string can be <name> or <name>,<resolver address>)")
|
||||||
flags.List = flag.Bool("list", false, "print the list of available resolvers for the enabled filters")
|
flags.List = flag.Bool("list", false, "print the list of available resolvers for the enabled filters")
|
||||||
flags.ListAll = flag.Bool("list-all", false, "print the complete list of available resolvers, ignoring filters")
|
flags.ListAll = flag.Bool("list-all", false, "print the complete list of available resolvers, ignoring filters")
|
||||||
|
flags.IncludeRelays = flag.Bool("include-relays", false, "include the list of available relays in the output of -list and -list-all")
|
||||||
flags.JSONOutput = flag.Bool("json", false, "output list as JSON")
|
flags.JSONOutput = flag.Bool("json", false, "output list as JSON")
|
||||||
flags.Check = flag.Bool("check", false, "check the configuration file and exit")
|
flags.Check = flag.Bool("check", false, "check the configuration file and exit")
|
||||||
flags.ConfigFile = flag.String("config", DefaultConfigFileName, "Path to the configuration file")
|
flags.ConfigFile = flag.String("config", DefaultConfigFileName, "Path to the configuration file")
|
||||||
|
@ -60,6 +66,10 @@ func main() {
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if fullexecpath, err := os.Executable(); err == nil {
|
||||||
|
WarnIfMaybeWritableByOtherUsers(fullexecpath)
|
||||||
|
}
|
||||||
|
|
||||||
app := &App{
|
app := &App{
|
||||||
flags: &flags,
|
flags: &flags,
|
||||||
}
|
}
|
||||||
|
@ -124,7 +134,7 @@ func (app *App) AppMain() {
|
||||||
dlog.Fatal(err)
|
dlog.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := PidFileCreate(); err != nil {
|
if err := PidFileCreate(); err != nil {
|
||||||
dlog.Criticalf("Unable to create the PID file: %v", err)
|
dlog.Errorf("Unable to create the PID file: [%v]", err)
|
||||||
}
|
}
|
||||||
if err := app.proxy.InitPluginsGlobals(); err != nil {
|
if err := app.proxy.InitPluginsGlobals(); err != nil {
|
||||||
dlog.Fatal(err)
|
dlog.Fatal(err)
|
||||||
|
@ -139,7 +149,9 @@ func (app *App) AppMain() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *App) Stop(service service.Service) error {
|
func (app *App) Stop(service service.Service) error {
|
||||||
PidFileRemove()
|
if err := PidFileRemove(); err != nil {
|
||||||
|
dlog.Warnf("Failed to remove the PID file: [%v]", err)
|
||||||
|
}
|
||||||
dlog.Notice("Stopped.")
|
dlog.Notice("Stopped.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,8 +32,9 @@ func NetProbe(proxy *Proxy, address string, timeout int) error {
|
||||||
pc, err := net.DialUDP("udp", nil, remoteUDPAddr)
|
pc, err := net.DialUDP("udp", nil, remoteUDPAddr)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Write at least 1 byte. This ensures that sockets are ready to use for writing.
|
// Write at least 1 byte. This ensures that sockets are ready to use for writing.
|
||||||
// Windows specific: during the system startup, sockets can be created but the underlying buffers may not be setup yet. If this is the case
|
// Windows specific: during the system startup, sockets can be created but the underlying buffers may not be
|
||||||
// Write fails with WSAENOBUFS: "An operation on a socket could not be performed because the system lacked sufficient buffer space or because a queue was full"
|
// setup yet. If this is the case Write fails with WSAENOBUFS: "An operation on a socket could not be
|
||||||
|
// performed because the system lacked sufficient buffer space or because a queue was full"
|
||||||
_, err = pc.Write([]byte{0})
|
_, err = pc.Write([]byte{0})
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -181,7 +181,7 @@ func (q ODoHQuery) decryptResponse(response []byte) ([]byte, error) {
|
||||||
responseLength := binary.BigEndian.Uint16(responsePlaintext[0:2])
|
responseLength := binary.BigEndian.Uint16(responsePlaintext[0:2])
|
||||||
valid := 1
|
valid := 1
|
||||||
for i := 4 + int(responseLength); i < len(responsePlaintext); i++ {
|
for i := 4 + int(responseLength); i < len(responsePlaintext); i++ {
|
||||||
valid = valid & subtle.ConstantTimeByteEq(response[i], 0x00)
|
valid &= subtle.ConstantTimeByteEq(response[i], 0x00)
|
||||||
}
|
}
|
||||||
if valid != 1 {
|
if valid != 1 {
|
||||||
return nil, fmt.Errorf("Malformed response")
|
return nil, fmt.Errorf("Malformed response")
|
||||||
|
|
|
@ -15,10 +15,10 @@ func PidFileCreate() error {
|
||||||
if pidFile == nil || len(*pidFile) == 0 {
|
if pidFile == nil || len(*pidFile) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(filepath.Dir(*pidFile), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(*pidFile), 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return safefile.WriteFile(*pidFile, []byte(strconv.Itoa(os.Getpid())), 0644)
|
return safefile.WriteFile(*pidFile, []byte(strconv.Itoa(os.Getpid())), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func PidFileRemove() error {
|
func PidFileRemove() error {
|
||||||
|
|
|
@ -30,13 +30,13 @@ func (plugin *PluginAllowedIP) Description() string {
|
||||||
|
|
||||||
func (plugin *PluginAllowedIP) Init(proxy *Proxy) error {
|
func (plugin *PluginAllowedIP) Init(proxy *Proxy) error {
|
||||||
dlog.Noticef("Loading the set of allowed IP rules from [%s]", proxy.allowedIPFile)
|
dlog.Noticef("Loading the set of allowed IP rules from [%s]", proxy.allowedIPFile)
|
||||||
bin, err := ReadTextFile(proxy.allowedIPFile)
|
lines, err := ReadTextFile(proxy.allowedIPFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
plugin.allowedPrefixes = iradix.New()
|
plugin.allowedPrefixes = iradix.New()
|
||||||
plugin.allowedIPs = make(map[string]interface{})
|
plugin.allowedIPs = make(map[string]interface{})
|
||||||
for lineNo, line := range strings.Split(string(bin), "\n") {
|
for lineNo, line := range strings.Split(lines, "\n") {
|
||||||
line = TrimAndStripInlineComments(line)
|
line = TrimAndStripInlineComments(line)
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
continue
|
continue
|
||||||
|
@ -119,10 +119,14 @@ func (plugin *PluginAllowedIP) Eval(pluginsState *PluginsState, msg *dns.Msg) er
|
||||||
if plugin.logger != nil {
|
if plugin.logger != nil {
|
||||||
qName := pluginsState.qName
|
qName := pluginsState.qName
|
||||||
var clientIPStr string
|
var clientIPStr string
|
||||||
if pluginsState.clientProto == "udp" {
|
switch pluginsState.clientProto {
|
||||||
|
case "udp":
|
||||||
clientIPStr = (*pluginsState.clientAddr).(*net.UDPAddr).IP.String()
|
clientIPStr = (*pluginsState.clientAddr).(*net.UDPAddr).IP.String()
|
||||||
} else {
|
case "tcp", "local_doh":
|
||||||
clientIPStr = (*pluginsState.clientAddr).(*net.TCPAddr).IP.String()
|
clientIPStr = (*pluginsState.clientAddr).(*net.TCPAddr).IP.String()
|
||||||
|
default:
|
||||||
|
// Ignore internal flow.
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
var line string
|
var line string
|
||||||
if plugin.format == "tsv" {
|
if plugin.format == "tsv" {
|
||||||
|
@ -130,7 +134,14 @@ func (plugin *PluginAllowedIP) Eval(pluginsState *PluginsState, msg *dns.Msg) er
|
||||||
year, month, day := now.Date()
|
year, month, day := now.Date()
|
||||||
hour, minute, second := now.Clock()
|
hour, minute, second := now.Clock()
|
||||||
tsStr := fmt.Sprintf("[%d-%02d-%02d %02d:%02d:%02d]", year, int(month), day, hour, minute, second)
|
tsStr := fmt.Sprintf("[%d-%02d-%02d %02d:%02d:%02d]", year, int(month), day, hour, minute, second)
|
||||||
line = fmt.Sprintf("%s\t%s\t%s\t%s\t%s\n", tsStr, clientIPStr, StringQuote(qName), StringQuote(ipStr), StringQuote(reason))
|
line = fmt.Sprintf(
|
||||||
|
"%s\t%s\t%s\t%s\t%s\n",
|
||||||
|
tsStr,
|
||||||
|
clientIPStr,
|
||||||
|
StringQuote(qName),
|
||||||
|
StringQuote(ipStr),
|
||||||
|
StringQuote(reason),
|
||||||
|
)
|
||||||
} else if plugin.format == "ltsv" {
|
} else if plugin.format == "ltsv" {
|
||||||
line = fmt.Sprintf("time:%d\thost:%s\tqname:%s\tip:%s\tmessage:%s\n", time.Now().Unix(), clientIPStr, StringQuote(qName), StringQuote(ipStr), StringQuote(reason))
|
line = fmt.Sprintf("time:%d\thost:%s\tqname:%s\tip:%s\tmessage:%s\n", time.Now().Unix(), clientIPStr, StringQuote(qName), StringQuote(ipStr), StringQuote(reason))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -29,13 +29,13 @@ func (plugin *PluginAllowName) Description() string {
|
||||||
|
|
||||||
func (plugin *PluginAllowName) Init(proxy *Proxy) error {
|
func (plugin *PluginAllowName) Init(proxy *Proxy) error {
|
||||||
dlog.Noticef("Loading the set of allowed names from [%s]", proxy.allowNameFile)
|
dlog.Noticef("Loading the set of allowed names from [%s]", proxy.allowNameFile)
|
||||||
bin, err := ReadTextFile(proxy.allowNameFile)
|
lines, err := ReadTextFile(proxy.allowNameFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
plugin.allWeeklyRanges = proxy.allWeeklyRanges
|
plugin.allWeeklyRanges = proxy.allWeeklyRanges
|
||||||
plugin.patternMatcher = NewPatternMatcher()
|
plugin.patternMatcher = NewPatternMatcher()
|
||||||
for lineNo, line := range strings.Split(string(bin), "\n") {
|
for lineNo, line := range strings.Split(lines, "\n") {
|
||||||
line = TrimAndStripInlineComments(line)
|
line = TrimAndStripInlineComments(line)
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
continue
|
continue
|
||||||
|
@ -96,10 +96,14 @@ func (plugin *PluginAllowName) Eval(pluginsState *PluginsState, msg *dns.Msg) er
|
||||||
pluginsState.sessionData["whitelisted"] = true
|
pluginsState.sessionData["whitelisted"] = true
|
||||||
if plugin.logger != nil {
|
if plugin.logger != nil {
|
||||||
var clientIPStr string
|
var clientIPStr string
|
||||||
if pluginsState.clientProto == "udp" {
|
switch pluginsState.clientProto {
|
||||||
|
case "udp":
|
||||||
clientIPStr = (*pluginsState.clientAddr).(*net.UDPAddr).IP.String()
|
clientIPStr = (*pluginsState.clientAddr).(*net.UDPAddr).IP.String()
|
||||||
} else {
|
case "tcp", "local_doh":
|
||||||
clientIPStr = (*pluginsState.clientAddr).(*net.TCPAddr).IP.String()
|
clientIPStr = (*pluginsState.clientAddr).(*net.TCPAddr).IP.String()
|
||||||
|
default:
|
||||||
|
// Ignore internal flow.
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
var line string
|
var line string
|
||||||
if plugin.format == "tsv" {
|
if plugin.format == "tsv" {
|
||||||
|
|
|
@ -30,13 +30,13 @@ func (plugin *PluginBlockIP) Description() string {
|
||||||
|
|
||||||
func (plugin *PluginBlockIP) Init(proxy *Proxy) error {
|
func (plugin *PluginBlockIP) Init(proxy *Proxy) error {
|
||||||
dlog.Noticef("Loading the set of IP blocking rules from [%s]", proxy.blockIPFile)
|
dlog.Noticef("Loading the set of IP blocking rules from [%s]", proxy.blockIPFile)
|
||||||
bin, err := ReadTextFile(proxy.blockIPFile)
|
lines, err := ReadTextFile(proxy.blockIPFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
plugin.blockedPrefixes = iradix.New()
|
plugin.blockedPrefixes = iradix.New()
|
||||||
plugin.blockedIPs = make(map[string]interface{})
|
plugin.blockedIPs = make(map[string]interface{})
|
||||||
for lineNo, line := range strings.Split(string(bin), "\n") {
|
for lineNo, line := range strings.Split(lines, "\n") {
|
||||||
line = TrimAndStripInlineComments(line)
|
line = TrimAndStripInlineComments(line)
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
continue
|
continue
|
||||||
|
@ -123,10 +123,14 @@ func (plugin *PluginBlockIP) Eval(pluginsState *PluginsState, msg *dns.Msg) erro
|
||||||
if plugin.logger != nil {
|
if plugin.logger != nil {
|
||||||
qName := pluginsState.qName
|
qName := pluginsState.qName
|
||||||
var clientIPStr string
|
var clientIPStr string
|
||||||
if pluginsState.clientProto == "udp" {
|
switch pluginsState.clientProto {
|
||||||
|
case "udp":
|
||||||
clientIPStr = (*pluginsState.clientAddr).(*net.UDPAddr).IP.String()
|
clientIPStr = (*pluginsState.clientAddr).(*net.UDPAddr).IP.String()
|
||||||
} else {
|
case "tcp", "local_doh":
|
||||||
clientIPStr = (*pluginsState.clientAddr).(*net.TCPAddr).IP.String()
|
clientIPStr = (*pluginsState.clientAddr).(*net.TCPAddr).IP.String()
|
||||||
|
default:
|
||||||
|
// Ignore internal flow.
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
var line string
|
var line string
|
||||||
if plugin.format == "tsv" {
|
if plugin.format == "tsv" {
|
||||||
|
@ -134,7 +138,14 @@ func (plugin *PluginBlockIP) Eval(pluginsState *PluginsState, msg *dns.Msg) erro
|
||||||
year, month, day := now.Date()
|
year, month, day := now.Date()
|
||||||
hour, minute, second := now.Clock()
|
hour, minute, second := now.Clock()
|
||||||
tsStr := fmt.Sprintf("[%d-%02d-%02d %02d:%02d:%02d]", year, int(month), day, hour, minute, second)
|
tsStr := fmt.Sprintf("[%d-%02d-%02d %02d:%02d:%02d]", year, int(month), day, hour, minute, second)
|
||||||
line = fmt.Sprintf("%s\t%s\t%s\t%s\t%s\n", tsStr, clientIPStr, StringQuote(qName), StringQuote(ipStr), StringQuote(reason))
|
line = fmt.Sprintf(
|
||||||
|
"%s\t%s\t%s\t%s\t%s\n",
|
||||||
|
tsStr,
|
||||||
|
clientIPStr,
|
||||||
|
StringQuote(qName),
|
||||||
|
StringQuote(ipStr),
|
||||||
|
StringQuote(reason),
|
||||||
|
)
|
||||||
} else if plugin.format == "ltsv" {
|
} else if plugin.format == "ltsv" {
|
||||||
line = fmt.Sprintf("time:%d\thost:%s\tqname:%s\tip:%s\tmessage:%s\n", time.Now().Unix(), clientIPStr, StringQuote(qName), StringQuote(ipStr), StringQuote(reason))
|
line = fmt.Sprintf("time:%d\thost:%s\tqname:%s\tip:%s\tmessage:%s\n", time.Now().Unix(), clientIPStr, StringQuote(qName), StringQuote(ipStr), StringQuote(reason))
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -35,8 +35,10 @@ func (plugin *PluginBlockIPv6) Eval(pluginsState *PluginsState, msg *dns.Msg) er
|
||||||
}
|
}
|
||||||
synth := EmptyResponseFromMessage(msg)
|
synth := EmptyResponseFromMessage(msg)
|
||||||
hinfo := new(dns.HINFO)
|
hinfo := new(dns.HINFO)
|
||||||
hinfo.Hdr = dns.RR_Header{Name: question.Name, Rrtype: dns.TypeHINFO,
|
hinfo.Hdr = dns.RR_Header{
|
||||||
Class: dns.ClassINET, Ttl: 86400}
|
Name: question.Name, Rrtype: dns.TypeHINFO,
|
||||||
|
Class: dns.ClassINET, Ttl: 86400,
|
||||||
|
}
|
||||||
hinfo.Cpu = "AAAA queries have been locally blocked by dnscrypt-proxy"
|
hinfo.Cpu = "AAAA queries have been locally blocked by dnscrypt-proxy"
|
||||||
hinfo.Os = "Set block_ipv6 to false to disable that feature"
|
hinfo.Os = "Set block_ipv6 to false to disable that feature"
|
||||||
synth.Answer = []dns.RR{hinfo}
|
synth.Answer = []dns.RR{hinfo}
|
||||||
|
@ -54,8 +56,10 @@ func (plugin *PluginBlockIPv6) Eval(pluginsState *PluginsState, msg *dns.Msg) er
|
||||||
soa.Minttl = 2400
|
soa.Minttl = 2400
|
||||||
soa.Expire = 604800
|
soa.Expire = 604800
|
||||||
soa.Retry = 300
|
soa.Retry = 300
|
||||||
soa.Hdr = dns.RR_Header{Name: parentZone, Rrtype: dns.TypeSOA,
|
soa.Hdr = dns.RR_Header{
|
||||||
Class: dns.ClassINET, Ttl: 60}
|
Name: parentZone, Rrtype: dns.TypeSOA,
|
||||||
|
Class: dns.ClassINET, Ttl: 60,
|
||||||
|
}
|
||||||
synth.Ns = []dns.RR{soa}
|
synth.Ns = []dns.RR{soa}
|
||||||
pluginsState.synthResponse = synth
|
pluginsState.synthResponse = synth
|
||||||
pluginsState.action = PluginsActionSynth
|
pluginsState.action = PluginsActionSynth
|
||||||
|
|
|
@ -44,10 +44,14 @@ func (blockedNames *BlockedNames) check(pluginsState *PluginsState, qName string
|
||||||
pluginsState.returnCode = PluginsReturnCodeReject
|
pluginsState.returnCode = PluginsReturnCodeReject
|
||||||
if blockedNames.logger != nil {
|
if blockedNames.logger != nil {
|
||||||
var clientIPStr string
|
var clientIPStr string
|
||||||
if pluginsState.clientProto == "udp" {
|
switch pluginsState.clientProto {
|
||||||
|
case "udp":
|
||||||
clientIPStr = (*pluginsState.clientAddr).(*net.UDPAddr).IP.String()
|
clientIPStr = (*pluginsState.clientAddr).(*net.UDPAddr).IP.String()
|
||||||
} else {
|
case "tcp", "local_doh":
|
||||||
clientIPStr = (*pluginsState.clientAddr).(*net.TCPAddr).IP.String()
|
clientIPStr = (*pluginsState.clientAddr).(*net.TCPAddr).IP.String()
|
||||||
|
default:
|
||||||
|
// Ignore internal flow.
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
var line string
|
var line string
|
||||||
if blockedNames.format == "tsv" {
|
if blockedNames.format == "tsv" {
|
||||||
|
@ -71,8 +75,7 @@ func (blockedNames *BlockedNames) check(pluginsState *PluginsState, qName string
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
type PluginBlockName struct {
|
type PluginBlockName struct{}
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *PluginBlockName) Name() string {
|
func (plugin *PluginBlockName) Name() string {
|
||||||
return "block_name"
|
return "block_name"
|
||||||
|
@ -84,7 +87,7 @@ func (plugin *PluginBlockName) Description() string {
|
||||||
|
|
||||||
func (plugin *PluginBlockName) Init(proxy *Proxy) error {
|
func (plugin *PluginBlockName) Init(proxy *Proxy) error {
|
||||||
dlog.Noticef("Loading the set of blocking rules from [%s]", proxy.blockNameFile)
|
dlog.Noticef("Loading the set of blocking rules from [%s]", proxy.blockNameFile)
|
||||||
bin, err := ReadTextFile(proxy.blockNameFile)
|
lines, err := ReadTextFile(proxy.blockNameFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -92,7 +95,7 @@ func (plugin *PluginBlockName) Init(proxy *Proxy) error {
|
||||||
allWeeklyRanges: proxy.allWeeklyRanges,
|
allWeeklyRanges: proxy.allWeeklyRanges,
|
||||||
patternMatcher: NewPatternMatcher(),
|
patternMatcher: NewPatternMatcher(),
|
||||||
}
|
}
|
||||||
for lineNo, line := range strings.Split(string(bin), "\n") {
|
for lineNo, line := range strings.Split(lines, "\n") {
|
||||||
line = TrimAndStripInlineComments(line)
|
line = TrimAndStripInlineComments(line)
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
continue
|
continue
|
||||||
|
@ -148,8 +151,7 @@ func (plugin *PluginBlockName) Eval(pluginsState *PluginsState, msg *dns.Msg) er
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
type PluginBlockNameResponse struct {
|
type PluginBlockNameResponse struct{}
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *PluginBlockNameResponse) Name() string {
|
func (plugin *PluginBlockNameResponse) Name() string {
|
||||||
return "block_name"
|
return "block_name"
|
||||||
|
|
|
@ -119,9 +119,11 @@ var undelegatedSet = []string{
|
||||||
"envoy",
|
"envoy",
|
||||||
"example",
|
"example",
|
||||||
"f.f.ip6.arpa",
|
"f.f.ip6.arpa",
|
||||||
|
"fritz.box",
|
||||||
"grp",
|
"grp",
|
||||||
"gw==",
|
"gw==",
|
||||||
"home",
|
"home",
|
||||||
|
"home.arpa",
|
||||||
"hub",
|
"hub",
|
||||||
"internal",
|
"internal",
|
||||||
"intra",
|
"intra",
|
||||||
|
@ -134,6 +136,7 @@ var undelegatedSet = []string{
|
||||||
"localdomain",
|
"localdomain",
|
||||||
"localhost",
|
"localhost",
|
||||||
"localnet",
|
"localnet",
|
||||||
|
"mail",
|
||||||
"modem",
|
"modem",
|
||||||
"mynet",
|
"mynet",
|
||||||
"myrouter",
|
"myrouter",
|
||||||
|
|
|
@ -6,8 +6,7 @@ import (
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PluginBlockUnqualified struct {
|
type PluginBlockUnqualified struct{}
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *PluginBlockUnqualified) Name() string {
|
func (plugin *PluginBlockUnqualified) Name() string {
|
||||||
return "block_unqualified"
|
return "block_unqualified"
|
||||||
|
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
lru "github.com/hashicorp/golang-lru"
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
sieve "github.com/opencoff/go-sieve"
|
||||||
)
|
)
|
||||||
|
|
||||||
const StaleResponseTTL = 30 * time.Second
|
const StaleResponseTTL = 30 * time.Second
|
||||||
|
@ -19,7 +19,7 @@ type CachedResponse struct {
|
||||||
|
|
||||||
type CachedResponses struct {
|
type CachedResponses struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
cache *lru.ARCCache
|
cache *sieve.Sieve[[32]byte, CachedResponse]
|
||||||
}
|
}
|
||||||
|
|
||||||
var cachedResponses CachedResponses
|
var cachedResponses CachedResponses
|
||||||
|
@ -45,8 +45,7 @@ func computeCacheKey(pluginsState *PluginsState, msg *dns.Msg) [32]byte {
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
type PluginCache struct {
|
type PluginCache struct{}
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *PluginCache) Name() string {
|
func (plugin *PluginCache) Name() string {
|
||||||
return "cache"
|
return "cache"
|
||||||
|
@ -76,12 +75,11 @@ func (plugin *PluginCache) Eval(pluginsState *PluginsState, msg *dns.Msg) error
|
||||||
cachedResponses.RUnlock()
|
cachedResponses.RUnlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
cachedAny, ok := cachedResponses.cache.Get(cacheKey)
|
cached, ok := cachedResponses.cache.Get(cacheKey)
|
||||||
if !ok {
|
if !ok {
|
||||||
cachedResponses.RUnlock()
|
cachedResponses.RUnlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
cached := cachedAny.(CachedResponse)
|
|
||||||
expiration := cached.expiration
|
expiration := cached.expiration
|
||||||
synth := cached.msg.Copy()
|
synth := cached.msg.Copy()
|
||||||
cachedResponses.RUnlock()
|
cachedResponses.RUnlock()
|
||||||
|
@ -108,8 +106,7 @@ func (plugin *PluginCache) Eval(pluginsState *PluginsState, msg *dns.Msg) error
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
type PluginCacheResponse struct {
|
type PluginCacheResponse struct{}
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *PluginCacheResponse) Name() string {
|
func (plugin *PluginCacheResponse) Name() string {
|
||||||
return "cache_response"
|
return "cache_response"
|
||||||
|
@ -139,7 +136,13 @@ func (plugin *PluginCacheResponse) Eval(pluginsState *PluginsState, msg *dns.Msg
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
cacheKey := computeCacheKey(pluginsState, msg)
|
cacheKey := computeCacheKey(pluginsState, msg)
|
||||||
ttl := getMinTTL(msg, pluginsState.cacheMinTTL, pluginsState.cacheMaxTTL, pluginsState.cacheNegMinTTL, pluginsState.cacheNegMaxTTL)
|
ttl := getMinTTL(
|
||||||
|
msg,
|
||||||
|
pluginsState.cacheMinTTL,
|
||||||
|
pluginsState.cacheMaxTTL,
|
||||||
|
pluginsState.cacheNegMinTTL,
|
||||||
|
pluginsState.cacheNegMaxTTL,
|
||||||
|
)
|
||||||
cachedResponse := CachedResponse{
|
cachedResponse := CachedResponse{
|
||||||
expiration: time.Now().Add(ttl),
|
expiration: time.Now().Add(ttl),
|
||||||
msg: *msg,
|
msg: *msg,
|
||||||
|
@ -147,8 +150,8 @@ func (plugin *PluginCacheResponse) Eval(pluginsState *PluginsState, msg *dns.Msg
|
||||||
cachedResponses.Lock()
|
cachedResponses.Lock()
|
||||||
if cachedResponses.cache == nil {
|
if cachedResponses.cache == nil {
|
||||||
var err error
|
var err error
|
||||||
cachedResponses.cache, err = lru.NewARC(pluginsState.cacheSize)
|
cachedResponses.cache = sieve.New[[32]byte, CachedResponse](pluginsState.cacheSize)
|
||||||
if err != nil {
|
if cachedResponses.cache == nil {
|
||||||
cachedResponses.Unlock()
|
cachedResponses.Unlock()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,12 +19,14 @@ type CloakedName struct {
|
||||||
lastUpdate *time.Time
|
lastUpdate *time.Time
|
||||||
lineNo int
|
lineNo int
|
||||||
isIP bool
|
isIP bool
|
||||||
|
PTR []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginCloak struct {
|
type PluginCloak struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
patternMatcher *PatternMatcher
|
patternMatcher *PatternMatcher
|
||||||
ttl uint32
|
ttl uint32
|
||||||
|
createPTR bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (plugin *PluginCloak) Name() string {
|
func (plugin *PluginCloak) Name() string {
|
||||||
|
@ -37,14 +39,15 @@ func (plugin *PluginCloak) Description() string {
|
||||||
|
|
||||||
func (plugin *PluginCloak) Init(proxy *Proxy) error {
|
func (plugin *PluginCloak) Init(proxy *Proxy) error {
|
||||||
dlog.Noticef("Loading the set of cloaking rules from [%s]", proxy.cloakFile)
|
dlog.Noticef("Loading the set of cloaking rules from [%s]", proxy.cloakFile)
|
||||||
bin, err := ReadTextFile(proxy.cloakFile)
|
lines, err := ReadTextFile(proxy.cloakFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
plugin.ttl = proxy.cloakTTL
|
plugin.ttl = proxy.cloakTTL
|
||||||
|
plugin.createPTR = proxy.cloakedPTR
|
||||||
plugin.patternMatcher = NewPatternMatcher()
|
plugin.patternMatcher = NewPatternMatcher()
|
||||||
cloakedNames := make(map[string]*CloakedName)
|
cloakedNames := make(map[string]*CloakedName)
|
||||||
for lineNo, line := range strings.Split(string(bin), "\n") {
|
for lineNo, line := range strings.Split(lines, "\n") {
|
||||||
line = TrimAndStripInlineComments(line)
|
line = TrimAndStripInlineComments(line)
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
continue
|
continue
|
||||||
|
@ -67,11 +70,12 @@ func (plugin *PluginCloak) Init(proxy *Proxy) error {
|
||||||
if !found {
|
if !found {
|
||||||
cloakedName = &CloakedName{}
|
cloakedName = &CloakedName{}
|
||||||
}
|
}
|
||||||
if ip := net.ParseIP(target); ip != nil {
|
ip := net.ParseIP(target)
|
||||||
|
if ip != nil {
|
||||||
if ipv4 := ip.To4(); ipv4 != nil {
|
if ipv4 := ip.To4(); ipv4 != nil {
|
||||||
cloakedName.ipv4 = append((*cloakedName).ipv4, ipv4)
|
cloakedName.ipv4 = append(cloakedName.ipv4, ipv4)
|
||||||
} else if ipv6 := ip.To16(); ipv6 != nil {
|
} else if ipv6 := ip.To16(); ipv6 != nil {
|
||||||
cloakedName.ipv6 = append((*cloakedName).ipv6, ipv6)
|
cloakedName.ipv6 = append(cloakedName.ipv6, ipv6)
|
||||||
} else {
|
} else {
|
||||||
dlog.Errorf("Invalid IP address in cloaking rule at line %d", 1+lineNo)
|
dlog.Errorf("Invalid IP address in cloaking rule at line %d", 1+lineNo)
|
||||||
continue
|
continue
|
||||||
|
@ -82,6 +86,28 @@ func (plugin *PluginCloak) Init(proxy *Proxy) error {
|
||||||
}
|
}
|
||||||
cloakedName.lineNo = lineNo + 1
|
cloakedName.lineNo = lineNo + 1
|
||||||
cloakedNames[line] = cloakedName
|
cloakedNames[line] = cloakedName
|
||||||
|
|
||||||
|
if !plugin.createPTR || strings.Contains(line, "*") || !cloakedName.isIP {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var ptrLine string
|
||||||
|
if ipv4 := ip.To4(); ipv4 != nil {
|
||||||
|
reversed, _ := dns.ReverseAddr(ip.To4().String())
|
||||||
|
ptrLine = strings.TrimSuffix(reversed, ".")
|
||||||
|
} else {
|
||||||
|
reversed, _ := dns.ReverseAddr(cloakedName.ipv6[0].To16().String())
|
||||||
|
ptrLine = strings.TrimSuffix(reversed, ".")
|
||||||
|
}
|
||||||
|
ptrQueryLine := ptrEntryToQuery(ptrLine)
|
||||||
|
ptrCloakedName, found := cloakedNames[ptrQueryLine]
|
||||||
|
if !found {
|
||||||
|
ptrCloakedName = &CloakedName{}
|
||||||
|
}
|
||||||
|
ptrCloakedName.isIP = true
|
||||||
|
ptrCloakedName.PTR = append((*ptrCloakedName).PTR, ptrNameToFQDN(line))
|
||||||
|
ptrCloakedName.lineNo = lineNo + 1
|
||||||
|
cloakedNames[ptrQueryLine] = ptrCloakedName
|
||||||
}
|
}
|
||||||
for line, cloakedName := range cloakedNames {
|
for line, cloakedName := range cloakedNames {
|
||||||
if err := plugin.patternMatcher.Add(line, cloakedName, cloakedName.lineNo); err != nil {
|
if err := plugin.patternMatcher.Add(line, cloakedName, cloakedName.lineNo); err != nil {
|
||||||
|
@ -91,6 +117,15 @@ func (plugin *PluginCloak) Init(proxy *Proxy) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ptrEntryToQuery(ptrEntry string) string {
|
||||||
|
return "=" + ptrEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func ptrNameToFQDN(ptrLine string) string {
|
||||||
|
ptrLine = strings.TrimPrefix(ptrLine, "=")
|
||||||
|
return ptrLine + "."
|
||||||
|
}
|
||||||
|
|
||||||
func (plugin *PluginCloak) Drop() error {
|
func (plugin *PluginCloak) Drop() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -101,7 +136,7 @@ func (plugin *PluginCloak) Reload() error {
|
||||||
|
|
||||||
func (plugin *PluginCloak) Eval(pluginsState *PluginsState, msg *dns.Msg) error {
|
func (plugin *PluginCloak) Eval(pluginsState *PluginsState, msg *dns.Msg) error {
|
||||||
question := msg.Question[0]
|
question := msg.Question[0]
|
||||||
if question.Qclass != dns.ClassINET || (question.Qtype != dns.TypeA && question.Qtype != dns.TypeAAAA) {
|
if question.Qclass != dns.ClassINET || question.Qtype == dns.TypeNS || question.Qtype == dns.TypeSOA {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
@ -111,6 +146,12 @@ func (plugin *PluginCloak) Eval(pluginsState *PluginsState, msg *dns.Msg) error
|
||||||
plugin.RUnlock()
|
plugin.RUnlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if question.Qtype != dns.TypeA && question.Qtype != dns.TypeAAAA && question.Qtype != dns.TypePTR {
|
||||||
|
plugin.RUnlock()
|
||||||
|
pluginsState.action = PluginsActionReject
|
||||||
|
pluginsState.returnCode = PluginsReturnCodeCloak
|
||||||
|
return nil
|
||||||
|
}
|
||||||
cloakedName := xcloakedName.(*CloakedName)
|
cloakedName := xcloakedName.(*CloakedName)
|
||||||
ttl, expired := plugin.ttl, false
|
ttl, expired := plugin.ttl, false
|
||||||
if cloakedName.lastUpdate != nil {
|
if cloakedName.lastUpdate != nil {
|
||||||
|
@ -157,15 +198,25 @@ func (plugin *PluginCloak) Eval(pluginsState *PluginsState, msg *dns.Msg) error
|
||||||
rr.A = ip
|
rr.A = ip
|
||||||
synth.Answer = append(synth.Answer, rr)
|
synth.Answer = append(synth.Answer, rr)
|
||||||
}
|
}
|
||||||
} else {
|
} else if question.Qtype == dns.TypeAAAA {
|
||||||
for _, ip := range cloakedName.ipv6 {
|
for _, ip := range cloakedName.ipv6 {
|
||||||
rr := new(dns.AAAA)
|
rr := new(dns.AAAA)
|
||||||
rr.Hdr = dns.RR_Header{Name: question.Name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: ttl}
|
rr.Hdr = dns.RR_Header{Name: question.Name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: ttl}
|
||||||
rr.AAAA = ip
|
rr.AAAA = ip
|
||||||
synth.Answer = append(synth.Answer, rr)
|
synth.Answer = append(synth.Answer, rr)
|
||||||
}
|
}
|
||||||
|
} else if question.Qtype == dns.TypePTR {
|
||||||
|
for _, ptr := range cloakedName.PTR {
|
||||||
|
rr := new(dns.PTR)
|
||||||
|
rr.Hdr = dns.RR_Header{Name: question.Name, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: ttl}
|
||||||
|
rr.Ptr = ptr
|
||||||
|
synth.Answer = append(synth.Answer, rr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
rand.Shuffle(len(synth.Answer), func(i, j int) { synth.Answer[i], synth.Answer[j] = synth.Answer[j], synth.Answer[i] })
|
rand.Shuffle(
|
||||||
|
len(synth.Answer),
|
||||||
|
func(i, j int) { synth.Answer[i], synth.Answer[j] = synth.Answer[j], synth.Answer[i] },
|
||||||
|
)
|
||||||
pluginsState.synthResponse = synth
|
pluginsState.synthResponse = synth
|
||||||
pluginsState.action = PluginsActionSynth
|
pluginsState.action = PluginsActionSynth
|
||||||
pluginsState.returnCode = PluginsReturnCodeCloak
|
pluginsState.returnCode = PluginsReturnCodeCloak
|
||||||
|
|
|
@ -34,19 +34,22 @@ func (plugin *PluginDNS64) Description() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (plugin *PluginDNS64) Init(proxy *Proxy) error {
|
func (plugin *PluginDNS64) Init(proxy *Proxy) error {
|
||||||
plugin.ipv4Resolver = proxy.listenAddresses[0] //recursively to ourselves
|
if len(proxy.listenAddresses) == 0 {
|
||||||
|
return errors.New("At least one listening IP address must be configured for the DNS64 plugin to work")
|
||||||
|
}
|
||||||
|
plugin.ipv4Resolver = proxy.listenAddresses[0] // query is sent to ourselves
|
||||||
plugin.pref64Mutex = new(sync.RWMutex)
|
plugin.pref64Mutex = new(sync.RWMutex)
|
||||||
plugin.proxy = proxy
|
plugin.proxy = proxy
|
||||||
|
|
||||||
if len(proxy.dns64Prefixes) != 0 {
|
if len(proxy.dns64Prefixes) != 0 {
|
||||||
plugin.pref64Mutex.RLock()
|
plugin.pref64Mutex.Lock()
|
||||||
defer plugin.pref64Mutex.RUnlock()
|
defer plugin.pref64Mutex.Unlock()
|
||||||
for _, prefStr := range proxy.dns64Prefixes {
|
for _, prefStr := range proxy.dns64Prefixes {
|
||||||
_, pref, err := net.ParseCIDR(prefStr)
|
_, pref, err := net.ParseCIDR(prefStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dlog.Infof("Registered DNS64 prefix [%s]", pref.String())
|
dlog.Noticef("Registered DNS64 prefix [%s]", pref.String())
|
||||||
plugin.pref64 = append(plugin.pref64, pref)
|
plugin.pref64 = append(plugin.pref64, pref)
|
||||||
}
|
}
|
||||||
} else if len(proxy.dns64Resolvers) != 0 {
|
} else if len(proxy.dns64Resolvers) != 0 {
|
||||||
|
@ -54,7 +57,10 @@ func (plugin *PluginDNS64) Init(proxy *Proxy) error {
|
||||||
if err := plugin.refreshPref64(); err != nil {
|
if err := plugin.refreshPref64(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
dlog.Notice("DNS64 map enabled")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -87,14 +93,22 @@ func (plugin *PluginDNS64) Eval(pluginsState *PluginsState, msg *dns.Msg) error
|
||||||
if !plugin.proxy.clientsCountInc() {
|
if !plugin.proxy.clientsCountInc() {
|
||||||
return errors.New("Too many concurrent connections to handle DNS64 subqueries")
|
return errors.New("Too many concurrent connections to handle DNS64 subqueries")
|
||||||
}
|
}
|
||||||
respPacket := plugin.proxy.processIncomingQuery("trampoline", plugin.proxy.mainProto, msgAPacket, nil, nil, time.Now(), false)
|
respPacket := plugin.proxy.processIncomingQuery(
|
||||||
|
"trampoline",
|
||||||
|
plugin.proxy.mainProto,
|
||||||
|
msgAPacket,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
time.Now(),
|
||||||
|
false,
|
||||||
|
)
|
||||||
plugin.proxy.clientsCountDec()
|
plugin.proxy.clientsCountDec()
|
||||||
resp := dns.Msg{}
|
resp := dns.Msg{}
|
||||||
if err := resp.Unpack(respPacket); err != nil {
|
if err := resp.Unpack(respPacket); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil || resp.Rcode != dns.RcodeSuccess {
|
if resp.Rcode != dns.RcodeSuccess {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,10 +124,12 @@ func (plugin *PluginDNS64) Eval(pluginsState *PluginsState, msg *dns.Msg) error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
synthAAAAs := make([]dns.RR, 0)
|
synth64 := make([]dns.RR, 0)
|
||||||
for _, answer := range resp.Answer {
|
for _, answer := range resp.Answer {
|
||||||
header := answer.Header()
|
header := answer.Header()
|
||||||
if header.Rrtype == dns.TypeA {
|
if header.Rrtype == dns.TypeCNAME {
|
||||||
|
synth64 = append(synth64, answer)
|
||||||
|
} else if header.Rrtype == dns.TypeA {
|
||||||
ttl := initialTTL
|
ttl := initialTTL
|
||||||
if ttl > header.Ttl {
|
if ttl > header.Ttl {
|
||||||
ttl = header.Ttl
|
ttl = header.Ttl
|
||||||
|
@ -121,24 +137,28 @@ func (plugin *PluginDNS64) Eval(pluginsState *PluginsState, msg *dns.Msg) error
|
||||||
|
|
||||||
ipv4 := answer.(*dns.A).A.To4()
|
ipv4 := answer.(*dns.A).A.To4()
|
||||||
if ipv4 != nil {
|
if ipv4 != nil {
|
||||||
plugin.pref64Mutex.Lock()
|
plugin.pref64Mutex.RLock()
|
||||||
for _, prefix := range plugin.pref64 {
|
for _, prefix := range plugin.pref64 {
|
||||||
ipv6 := translateToIPv6(ipv4, prefix)
|
ipv6 := translateToIPv6(ipv4, prefix)
|
||||||
synthAAAA := new(dns.AAAA)
|
synthAAAA := new(dns.AAAA)
|
||||||
synthAAAA.Hdr = dns.RR_Header{Name: header.Name, Rrtype: dns.TypeAAAA, Class: header.Class, Ttl: ttl}
|
synthAAAA.Hdr = dns.RR_Header{
|
||||||
|
Name: header.Name,
|
||||||
|
Rrtype: dns.TypeAAAA,
|
||||||
|
Class: header.Class,
|
||||||
|
Ttl: ttl,
|
||||||
|
}
|
||||||
synthAAAA.AAAA = ipv6
|
synthAAAA.AAAA = ipv6
|
||||||
synthAAAAs = append(synthAAAAs, synthAAAA)
|
synth64 = append(synth64, synthAAAA)
|
||||||
}
|
}
|
||||||
plugin.pref64Mutex.Unlock()
|
plugin.pref64Mutex.RUnlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
synth := EmptyResponseFromMessage(msg)
|
msg.Answer = synth64
|
||||||
synth.Answer = append(synth.Answer, synthAAAAs...)
|
msg.AuthenticatedData = false
|
||||||
|
msg.SetEdns0(uint16(MaxDNSUDPSafePacketSize), false)
|
||||||
|
|
||||||
pluginsState.synthResponse = synth
|
|
||||||
pluginsState.action = PluginsActionSynth
|
|
||||||
pluginsState.returnCode = PluginsReturnCodeCloak
|
pluginsState.returnCode = PluginsReturnCodeCloak
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -173,7 +193,6 @@ func (plugin *PluginDNS64) fetchPref64(resolver string) error {
|
||||||
|
|
||||||
client := new(dns.Client)
|
client := new(dns.Client)
|
||||||
resp, _, err := client.Exchange(msg, resolver)
|
resp, _, err := client.Exchange(msg, resolver)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -190,17 +209,18 @@ func (plugin *PluginDNS64) fetchPref64(resolver string) error {
|
||||||
if ipv6 != nil && len(ipv6) == net.IPv6len {
|
if ipv6 != nil && len(ipv6) == net.IPv6len {
|
||||||
prefEnd := 0
|
prefEnd := 0
|
||||||
|
|
||||||
if wka := net.IPv4(ipv6[12], ipv6[13], ipv6[14], ipv6[15]); wka.Equal(rfc7050WKA1) || wka.Equal(rfc7050WKA2) { //96
|
if wka := net.IPv4(ipv6[12], ipv6[13], ipv6[14], ipv6[15]); wka.Equal(rfc7050WKA1) ||
|
||||||
|
wka.Equal(rfc7050WKA2) { // 96
|
||||||
prefEnd = 12
|
prefEnd = 12
|
||||||
} else if wka := net.IPv4(ipv6[9], ipv6[10], ipv6[11], ipv6[12]); wka.Equal(rfc7050WKA1) || wka.Equal(rfc7050WKA2) { //64
|
} else if wka := net.IPv4(ipv6[9], ipv6[10], ipv6[11], ipv6[12]); wka.Equal(rfc7050WKA1) || wka.Equal(rfc7050WKA2) { // 64
|
||||||
prefEnd = 8
|
prefEnd = 8
|
||||||
} else if wka := net.IPv4(ipv6[7], ipv6[9], ipv6[10], ipv6[11]); wka.Equal(rfc7050WKA1) || wka.Equal(rfc7050WKA2) { //56
|
} else if wka := net.IPv4(ipv6[7], ipv6[9], ipv6[10], ipv6[11]); wka.Equal(rfc7050WKA1) || wka.Equal(rfc7050WKA2) { // 56
|
||||||
prefEnd = 7
|
prefEnd = 7
|
||||||
} else if wka := net.IPv4(ipv6[6], ipv6[7], ipv6[9], ipv6[10]); wka.Equal(rfc7050WKA1) || wka.Equal(rfc7050WKA2) { //48
|
} else if wka := net.IPv4(ipv6[6], ipv6[7], ipv6[9], ipv6[10]); wka.Equal(rfc7050WKA1) || wka.Equal(rfc7050WKA2) { // 48
|
||||||
prefEnd = 6
|
prefEnd = 6
|
||||||
} else if wka := net.IPv4(ipv6[5], ipv6[6], ipv6[7], ipv6[9]); wka.Equal(rfc7050WKA1) || wka.Equal(rfc7050WKA2) { //40
|
} else if wka := net.IPv4(ipv6[5], ipv6[6], ipv6[7], ipv6[9]); wka.Equal(rfc7050WKA1) || wka.Equal(rfc7050WKA2) { // 40
|
||||||
prefEnd = 5
|
prefEnd = 5
|
||||||
} else if wka := net.IPv4(ipv6[4], ipv6[5], ipv6[6], ipv6[7]); wka.Equal(rfc7050WKA1) || wka.Equal(rfc7050WKA2) { //32
|
} else if wka := net.IPv4(ipv6[4], ipv6[5], ipv6[6], ipv6[7]); wka.Equal(rfc7050WKA1) || wka.Equal(rfc7050WKA2) { // 32
|
||||||
prefEnd = 4
|
prefEnd = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,8 +242,8 @@ func (plugin *PluginDNS64) fetchPref64(resolver string) error {
|
||||||
return errors.New("Empty Pref64 list")
|
return errors.New("Empty Pref64 list")
|
||||||
}
|
}
|
||||||
|
|
||||||
plugin.pref64Mutex.RLock()
|
plugin.pref64Mutex.Lock()
|
||||||
defer plugin.pref64Mutex.RUnlock()
|
defer plugin.pref64Mutex.Unlock()
|
||||||
plugin.pref64 = prefixes
|
plugin.pref64 = prefixes
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -235,8 +255,8 @@ func (plugin *PluginDNS64) refreshPref64() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plugin.pref64Mutex.Lock()
|
plugin.pref64Mutex.RLock()
|
||||||
defer plugin.pref64Mutex.Unlock()
|
defer plugin.pref64Mutex.RUnlock()
|
||||||
if len(plugin.pref64) == 0 {
|
if len(plugin.pref64) == 0 {
|
||||||
return errors.New("Empty Pref64 list")
|
return errors.New("Empty Pref64 list")
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,7 @@ import (
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PluginFirefox struct {
|
type PluginFirefox struct{}
|
||||||
}
|
|
||||||
|
|
||||||
func (plugin *PluginFirefox) Name() string {
|
func (plugin *PluginFirefox) Name() string {
|
||||||
return "firefox"
|
return "firefox"
|
||||||
|
|
|
@ -29,11 +29,11 @@ func (plugin *PluginForward) Description() string {
|
||||||
|
|
||||||
func (plugin *PluginForward) Init(proxy *Proxy) error {
|
func (plugin *PluginForward) Init(proxy *Proxy) error {
|
||||||
dlog.Noticef("Loading the set of forwarding rules from [%s]", proxy.forwardFile)
|
dlog.Noticef("Loading the set of forwarding rules from [%s]", proxy.forwardFile)
|
||||||
bin, err := ReadTextFile(proxy.forwardFile)
|
lines, err := ReadTextFile(proxy.forwardFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for lineNo, line := range strings.Split(string(bin), "\n") {
|
for lineNo, line := range strings.Split(lines, "\n") {
|
||||||
line = TrimAndStripInlineComments(line)
|
line = TrimAndStripInlineComments(line)
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
continue
|
continue
|
||||||
|
@ -49,9 +49,16 @@ func (plugin *PluginForward) Init(proxy *Proxy) error {
|
||||||
var servers []string
|
var servers []string
|
||||||
for _, server := range strings.Split(serversStr, ",") {
|
for _, server := range strings.Split(serversStr, ",") {
|
||||||
server = strings.TrimSpace(server)
|
server = strings.TrimSpace(server)
|
||||||
if net.ParseIP(server) != nil {
|
server = strings.TrimPrefix(server, "[")
|
||||||
server = fmt.Sprintf("%s:%d", server, 53)
|
server = strings.TrimSuffix(server, "]")
|
||||||
|
if ip := net.ParseIP(server); ip != nil {
|
||||||
|
if ip.To4() != nil {
|
||||||
|
server = fmt.Sprintf("%s:%d", server, 53)
|
||||||
|
} else {
|
||||||
|
server = fmt.Sprintf("[%s]:%d", server, 53)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
dlog.Infof("Forwarding [%s] to %s", domain, server)
|
||||||
servers = append(servers, server)
|
servers = append(servers, server)
|
||||||
}
|
}
|
||||||
if len(servers) == 0 {
|
if len(servers) == 0 {
|
||||||
|
@ -82,7 +89,9 @@ func (plugin *PluginForward) Eval(pluginsState *PluginsState, msg *dns.Msg) erro
|
||||||
if candidateLen > qNameLen {
|
if candidateLen > qNameLen {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if qName[qNameLen-candidateLen:] == candidate.domain && (candidateLen == qNameLen || (qName[qNameLen-candidateLen-1] == '.')) {
|
if (qName[qNameLen-candidateLen:] == candidate.domain &&
|
||||||
|
(candidateLen == qNameLen || (qName[qNameLen-candidateLen-1] == '.'))) ||
|
||||||
|
(candidate.domain == ".") {
|
||||||
servers = candidate.servers
|
servers = candidate.servers
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,12 +30,18 @@ func (plugin *PluginGetSetPayloadSize) Eval(pluginsState *PluginsState, msg *dns
|
||||||
dnssec := false
|
dnssec := false
|
||||||
if edns0 != nil {
|
if edns0 != nil {
|
||||||
pluginsState.maxUnencryptedUDPSafePayloadSize = int(edns0.UDPSize())
|
pluginsState.maxUnencryptedUDPSafePayloadSize = int(edns0.UDPSize())
|
||||||
pluginsState.originalMaxPayloadSize = Max(pluginsState.maxUnencryptedUDPSafePayloadSize-ResponseOverhead, pluginsState.originalMaxPayloadSize)
|
pluginsState.originalMaxPayloadSize = Max(
|
||||||
|
pluginsState.maxUnencryptedUDPSafePayloadSize-ResponseOverhead,
|
||||||
|
pluginsState.originalMaxPayloadSize,
|
||||||
|
)
|
||||||
dnssec = edns0.Do()
|
dnssec = edns0.Do()
|
||||||
}
|
}
|
||||||
var options *[]dns.EDNS0
|
var options *[]dns.EDNS0
|
||||||
pluginsState.dnssec = dnssec
|
pluginsState.dnssec = dnssec
|
||||||
pluginsState.maxPayloadSize = Min(MaxDNSUDPPacketSize-ResponseOverhead, Max(pluginsState.originalMaxPayloadSize, pluginsState.maxPayloadSize))
|
pluginsState.maxPayloadSize = Min(
|
||||||
|
MaxDNSUDPPacketSize-ResponseOverhead,
|
||||||
|
Max(pluginsState.originalMaxPayloadSize, pluginsState.maxPayloadSize),
|
||||||
|
)
|
||||||
if pluginsState.maxPayloadSize > 512 {
|
if pluginsState.maxPayloadSize > 512 {
|
||||||
extra2 := []dns.RR{}
|
extra2 := []dns.RR{}
|
||||||
for _, extra := range msg.Extra {
|
for _, extra := range msg.Extra {
|
||||||
|
|
|
@ -43,17 +43,21 @@ func (plugin *PluginNxLog) Eval(pluginsState *PluginsState, msg *dns.Msg) error
|
||||||
if msg.Rcode != dns.RcodeNameError {
|
if msg.Rcode != dns.RcodeNameError {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
var clientIPStr string
|
||||||
|
switch pluginsState.clientProto {
|
||||||
|
case "udp":
|
||||||
|
clientIPStr = (*pluginsState.clientAddr).(*net.UDPAddr).IP.String()
|
||||||
|
case "tcp", "local_doh":
|
||||||
|
clientIPStr = (*pluginsState.clientAddr).(*net.TCPAddr).IP.String()
|
||||||
|
default:
|
||||||
|
// Ignore internal flow.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
question := msg.Question[0]
|
question := msg.Question[0]
|
||||||
qType, ok := dns.TypeToString[question.Qtype]
|
qType, ok := dns.TypeToString[question.Qtype]
|
||||||
if !ok {
|
if !ok {
|
||||||
qType = string(qType)
|
qType = string(qType)
|
||||||
}
|
}
|
||||||
var clientIPStr string
|
|
||||||
if pluginsState.clientProto == "udp" {
|
|
||||||
clientIPStr = (*pluginsState.clientAddr).(*net.UDPAddr).IP.String()
|
|
||||||
} else {
|
|
||||||
clientIPStr = (*pluginsState.clientAddr).(*net.TCPAddr).IP.String()
|
|
||||||
}
|
|
||||||
qName := pluginsState.qName
|
qName := pluginsState.qName
|
||||||
|
|
||||||
var line string
|
var line string
|
||||||
|
|
|
@ -43,6 +43,16 @@ func (plugin *PluginQueryLog) Reload() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (plugin *PluginQueryLog) Eval(pluginsState *PluginsState, msg *dns.Msg) error {
|
func (plugin *PluginQueryLog) Eval(pluginsState *PluginsState, msg *dns.Msg) error {
|
||||||
|
var clientIPStr string
|
||||||
|
switch pluginsState.clientProto {
|
||||||
|
case "udp":
|
||||||
|
clientIPStr = (*pluginsState.clientAddr).(*net.UDPAddr).IP.String()
|
||||||
|
case "tcp", "local_doh":
|
||||||
|
clientIPStr = (*pluginsState.clientAddr).(*net.TCPAddr).IP.String()
|
||||||
|
default:
|
||||||
|
// Ignore internal flow.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
question := msg.Question[0]
|
question := msg.Question[0]
|
||||||
qType, ok := dns.TypeToString[question.Qtype]
|
qType, ok := dns.TypeToString[question.Qtype]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -55,12 +65,6 @@ func (plugin *PluginQueryLog) Eval(pluginsState *PluginsState, msg *dns.Msg) err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var clientIPStr string
|
|
||||||
if pluginsState.clientProto == "udp" {
|
|
||||||
clientIPStr = (*pluginsState.clientAddr).(*net.UDPAddr).IP.String()
|
|
||||||
} else {
|
|
||||||
clientIPStr = (*pluginsState.clientAddr).(*net.TCPAddr).IP.String()
|
|
||||||
}
|
|
||||||
qName := pluginsState.qName
|
qName := pluginsState.qName
|
||||||
|
|
||||||
if pluginsState.cacheHit {
|
if pluginsState.cacheHit {
|
||||||
|
@ -86,8 +90,16 @@ func (plugin *PluginQueryLog) Eval(pluginsState *PluginsState, msg *dns.Msg) err
|
||||||
year, month, day := now.Date()
|
year, month, day := now.Date()
|
||||||
hour, minute, second := now.Clock()
|
hour, minute, second := now.Clock()
|
||||||
tsStr := fmt.Sprintf("[%d-%02d-%02d %02d:%02d:%02d]", year, int(month), day, hour, minute, second)
|
tsStr := fmt.Sprintf("[%d-%02d-%02d %02d:%02d:%02d]", year, int(month), day, hour, minute, second)
|
||||||
line = fmt.Sprintf("%s\t%s\t%s\t%s\t%s\t%dms\t%s\n", tsStr, clientIPStr, StringQuote(qName), qType, returnCode, requestDuration/time.Millisecond,
|
line = fmt.Sprintf(
|
||||||
StringQuote(pluginsState.serverName))
|
"%s\t%s\t%s\t%s\t%s\t%dms\t%s\n",
|
||||||
|
tsStr,
|
||||||
|
clientIPStr,
|
||||||
|
StringQuote(qName),
|
||||||
|
qType,
|
||||||
|
returnCode,
|
||||||
|
requestDuration/time.Millisecond,
|
||||||
|
StringQuote(pluginsState.serverName),
|
||||||
|
)
|
||||||
} else if plugin.format == "ltsv" {
|
} else if plugin.format == "ltsv" {
|
||||||
cached := 0
|
cached := 0
|
||||||
if pluginsState.cacheHit {
|
if pluginsState.cacheHit {
|
||||||
|
|
|
@ -18,8 +18,10 @@ func (plugin *PluginQueryMeta) Description() string {
|
||||||
|
|
||||||
func (plugin *PluginQueryMeta) Init(proxy *Proxy) error {
|
func (plugin *PluginQueryMeta) Init(proxy *Proxy) error {
|
||||||
queryMetaRR := new(dns.TXT)
|
queryMetaRR := new(dns.TXT)
|
||||||
queryMetaRR.Hdr = dns.RR_Header{Name: ".", Rrtype: dns.TypeTXT,
|
queryMetaRR.Hdr = dns.RR_Header{
|
||||||
Class: dns.ClassINET, Ttl: 86400}
|
Name: ".", Rrtype: dns.TypeTXT,
|
||||||
|
Class: dns.ClassINET, Ttl: 86400,
|
||||||
|
}
|
||||||
queryMetaRR.Txt = proxy.queryMeta
|
queryMetaRR.Txt = proxy.queryMeta
|
||||||
plugin.queryMetaRR = queryMetaRR
|
plugin.queryMetaRR = queryMetaRR
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -189,11 +189,11 @@ func parseBlockedQueryResponse(blockedResponse string, pluginsGlobals *PluginsGl
|
||||||
|
|
||||||
if strings.HasPrefix(blockedResponse, "a:") {
|
if strings.HasPrefix(blockedResponse, "a:") {
|
||||||
blockedIPStrings := strings.Split(blockedResponse, ",")
|
blockedIPStrings := strings.Split(blockedResponse, ",")
|
||||||
(*pluginsGlobals).respondWithIPv4 = net.ParseIP(strings.TrimPrefix(blockedIPStrings[0], "a:"))
|
pluginsGlobals.respondWithIPv4 = net.ParseIP(strings.TrimPrefix(blockedIPStrings[0], "a:"))
|
||||||
|
|
||||||
if (*pluginsGlobals).respondWithIPv4 == nil {
|
if pluginsGlobals.respondWithIPv4 == nil {
|
||||||
dlog.Notice("Error parsing IPv4 response given in blocked_query_response option, defaulting to `hinfo`")
|
dlog.Notice("Error parsing IPv4 response given in blocked_query_response option, defaulting to `hinfo`")
|
||||||
(*pluginsGlobals).refusedCodeInResponses = false
|
pluginsGlobals.refusedCodeInResponses = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,28 +203,30 @@ func parseBlockedQueryResponse(blockedResponse string, pluginsGlobals *PluginsGl
|
||||||
if strings.HasPrefix(ipv6Response, "[") {
|
if strings.HasPrefix(ipv6Response, "[") {
|
||||||
ipv6Response = strings.Trim(ipv6Response, "[]")
|
ipv6Response = strings.Trim(ipv6Response, "[]")
|
||||||
}
|
}
|
||||||
(*pluginsGlobals).respondWithIPv6 = net.ParseIP(ipv6Response)
|
pluginsGlobals.respondWithIPv6 = net.ParseIP(ipv6Response)
|
||||||
|
|
||||||
if (*pluginsGlobals).respondWithIPv6 == nil {
|
if pluginsGlobals.respondWithIPv6 == nil {
|
||||||
dlog.Notice("Error parsing IPv6 response given in blocked_query_response option, defaulting to IPv4")
|
dlog.Notice(
|
||||||
|
"Error parsing IPv6 response given in blocked_query_response option, defaulting to IPv4",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dlog.Noticef("Invalid IPv6 response given in blocked_query_response option [%s], the option should take the form 'a:<IPv4>,aaaa:<IPv6>'", blockedIPStrings[1])
|
dlog.Noticef("Invalid IPv6 response given in blocked_query_response option [%s], the option should take the form 'a:<IPv4>,aaaa:<IPv6>'", blockedIPStrings[1])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (*pluginsGlobals).respondWithIPv6 == nil {
|
if pluginsGlobals.respondWithIPv6 == nil {
|
||||||
(*pluginsGlobals).respondWithIPv6 = (*pluginsGlobals).respondWithIPv4
|
pluginsGlobals.respondWithIPv6 = pluginsGlobals.respondWithIPv4
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch blockedResponse {
|
switch blockedResponse {
|
||||||
case "refused":
|
case "refused":
|
||||||
(*pluginsGlobals).refusedCodeInResponses = true
|
pluginsGlobals.refusedCodeInResponses = true
|
||||||
case "hinfo":
|
case "hinfo":
|
||||||
(*pluginsGlobals).refusedCodeInResponses = false
|
pluginsGlobals.refusedCodeInResponses = false
|
||||||
default:
|
default:
|
||||||
dlog.Noticef("Invalid blocked_query_response option [%s], defaulting to `hinfo`", blockedResponse)
|
dlog.Noticef("Invalid blocked_query_response option [%s], defaulting to `hinfo`", blockedResponse)
|
||||||
(*pluginsGlobals).refusedCodeInResponses = false
|
pluginsGlobals.refusedCodeInResponses = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -238,7 +240,13 @@ type Plugin interface {
|
||||||
Eval(pluginsState *PluginsState, msg *dns.Msg) error
|
Eval(pluginsState *PluginsState, msg *dns.Msg) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPluginsState(proxy *Proxy, clientProto string, clientAddr *net.Addr, serverProto string, start time.Time) PluginsState {
|
func NewPluginsState(
|
||||||
|
proxy *Proxy,
|
||||||
|
clientProto string,
|
||||||
|
clientAddr *net.Addr,
|
||||||
|
serverProto string,
|
||||||
|
start time.Time,
|
||||||
|
) PluginsState {
|
||||||
return PluginsState{
|
return PluginsState{
|
||||||
action: PluginsActionContinue,
|
action: PluginsActionContinue,
|
||||||
returnCode: PluginsReturnCodePass,
|
returnCode: PluginsReturnCodePass,
|
||||||
|
@ -262,7 +270,11 @@ func NewPluginsState(proxy *Proxy, clientProto string, clientAddr *net.Addr, ser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pluginsState *PluginsState) ApplyQueryPlugins(pluginsGlobals *PluginsGlobals, packet []byte, needsEDNS0Padding bool) ([]byte, error) {
|
func (pluginsState *PluginsState) ApplyQueryPlugins(
|
||||||
|
pluginsGlobals *PluginsGlobals,
|
||||||
|
packet []byte,
|
||||||
|
needsEDNS0Padding bool,
|
||||||
|
) ([]byte, error) {
|
||||||
msg := dns.Msg{}
|
msg := dns.Msg{}
|
||||||
if err := msg.Unpack(packet); err != nil {
|
if err := msg.Unpack(packet); err != nil {
|
||||||
return packet, err
|
return packet, err
|
||||||
|
@ -288,7 +300,13 @@ func (pluginsState *PluginsState) ApplyQueryPlugins(pluginsGlobals *PluginsGloba
|
||||||
return packet, err
|
return packet, err
|
||||||
}
|
}
|
||||||
if pluginsState.action == PluginsActionReject {
|
if pluginsState.action == PluginsActionReject {
|
||||||
synth := RefusedResponseFromMessage(&msg, pluginsGlobals.refusedCodeInResponses, pluginsGlobals.respondWithIPv4, pluginsGlobals.respondWithIPv6, pluginsState.rejectTTL)
|
synth := RefusedResponseFromMessage(
|
||||||
|
&msg,
|
||||||
|
pluginsGlobals.refusedCodeInResponses,
|
||||||
|
pluginsGlobals.respondWithIPv4,
|
||||||
|
pluginsGlobals.respondWithIPv6,
|
||||||
|
pluginsState.rejectTTL,
|
||||||
|
)
|
||||||
pluginsState.synthResponse = synth
|
pluginsState.synthResponse = synth
|
||||||
}
|
}
|
||||||
if pluginsState.action != PluginsActionContinue {
|
if pluginsState.action != PluginsActionContinue {
|
||||||
|
@ -309,7 +327,11 @@ func (pluginsState *PluginsState) ApplyQueryPlugins(pluginsGlobals *PluginsGloba
|
||||||
return packet2, nil
|
return packet2, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pluginsState *PluginsState) ApplyResponsePlugins(pluginsGlobals *PluginsGlobals, packet []byte, ttl *uint32) ([]byte, error) {
|
func (pluginsState *PluginsState) ApplyResponsePlugins(
|
||||||
|
pluginsGlobals *PluginsGlobals,
|
||||||
|
packet []byte,
|
||||||
|
ttl *uint32,
|
||||||
|
) ([]byte, error) {
|
||||||
msg := dns.Msg{Compress: true}
|
msg := dns.Msg{Compress: true}
|
||||||
if err := msg.Unpack(packet); err != nil {
|
if err := msg.Unpack(packet); err != nil {
|
||||||
if len(packet) >= MinDNSPacketSize && HasTCFlag(packet) {
|
if len(packet) >= MinDNSPacketSize && HasTCFlag(packet) {
|
||||||
|
@ -336,7 +358,13 @@ func (pluginsState *PluginsState) ApplyResponsePlugins(pluginsGlobals *PluginsGl
|
||||||
return packet, err
|
return packet, err
|
||||||
}
|
}
|
||||||
if pluginsState.action == PluginsActionReject {
|
if pluginsState.action == PluginsActionReject {
|
||||||
synth := RefusedResponseFromMessage(&msg, pluginsGlobals.refusedCodeInResponses, pluginsGlobals.respondWithIPv4, pluginsGlobals.respondWithIPv6, pluginsState.rejectTTL)
|
synth := RefusedResponseFromMessage(
|
||||||
|
&msg,
|
||||||
|
pluginsGlobals.refusedCodeInResponses,
|
||||||
|
pluginsGlobals.respondWithIPv4,
|
||||||
|
pluginsGlobals.respondWithIPv6,
|
||||||
|
pluginsState.rejectTTL,
|
||||||
|
)
|
||||||
pluginsState.synthResponse = synth
|
pluginsState.synthResponse = synth
|
||||||
}
|
}
|
||||||
if pluginsState.action != PluginsActionContinue {
|
if pluginsState.action != PluginsActionContinue {
|
||||||
|
|
|
@ -15,8 +15,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (proxy *Proxy) dropPrivilege(userStr string, fds []*os.File) {
|
func (proxy *Proxy) dropPrivilege(userStr string, fds []*os.File) {
|
||||||
currentUser, err := user.Current()
|
if os.Geteuid() != 0 {
|
||||||
if err != nil && currentUser.Uid != "0" {
|
|
||||||
dlog.Fatal("Root privileges are required in order to switch to a different user. Maybe try again with 'sudo'")
|
dlog.Fatal("Root privileges are required in order to switch to a different user. Maybe try again with 'sudo'")
|
||||||
}
|
}
|
||||||
userInfo, err := user.Lookup(userStr)
|
userInfo, err := user.Lookup(userStr)
|
||||||
|
@ -25,9 +24,19 @@ func (proxy *Proxy) dropPrivilege(userStr string, fds []*os.File) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
uid, err2 := strconv.Atoi(userStr)
|
uid, err2 := strconv.Atoi(userStr)
|
||||||
if err2 != nil || uid <= 0 {
|
if err2 != nil || uid <= 0 {
|
||||||
dlog.Fatalf("Unable to retrieve any information about user [%s]: [%s] - Remove the user_name directive from the configuration file in order to avoid identity switch", userStr, err)
|
dlog.Fatalf(
|
||||||
|
"Unable to retrieve any information about user [%s]: [%s] - Remove the user_name directive from the configuration file in order to avoid identity switch",
|
||||||
|
userStr,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
dlog.Warnf("Unable to retrieve any information about user [%s]: [%s] - Switching to user id [%v] with the same group id, as [%v] looks like a user id. But you should remove or fix the user_name directive in the configuration file if possible", userStr, err, uid, uid)
|
dlog.Warnf(
|
||||||
|
"Unable to retrieve any information about user [%s]: [%s] - Switching to user id [%v] with the same group id, as [%v] looks like a user id. But you should remove or fix the user_name directive in the configuration file if possible",
|
||||||
|
userStr,
|
||||||
|
err,
|
||||||
|
uid,
|
||||||
|
uid,
|
||||||
|
)
|
||||||
userInfo = &user.User{Uid: userStr, Gid: userStr}
|
userInfo = &user.User{Uid: userStr, Gid: userStr}
|
||||||
}
|
}
|
||||||
uid, err := strconv.Atoi(userInfo.Uid)
|
uid, err := strconv.Atoi(userInfo.Uid)
|
||||||
|
|
|
@ -17,8 +17,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (proxy *Proxy) dropPrivilege(userStr string, fds []*os.File) {
|
func (proxy *Proxy) dropPrivilege(userStr string, fds []*os.File) {
|
||||||
currentUser, err := user.Current()
|
if os.Geteuid() != 0 {
|
||||||
if err != nil && currentUser.Uid != "0" {
|
|
||||||
dlog.Fatal("Root privileges are required in order to switch to a different user. Maybe try again with 'sudo'")
|
dlog.Fatal("Root privileges are required in order to switch to a different user. Maybe try again with 'sudo'")
|
||||||
}
|
}
|
||||||
userInfo, err := user.Lookup(userStr)
|
userInfo, err := user.Lookup(userStr)
|
||||||
|
@ -27,9 +26,19 @@ func (proxy *Proxy) dropPrivilege(userStr string, fds []*os.File) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
uid, err2 := strconv.Atoi(userStr)
|
uid, err2 := strconv.Atoi(userStr)
|
||||||
if err2 != nil || uid <= 0 {
|
if err2 != nil || uid <= 0 {
|
||||||
dlog.Fatalf("Unable to retrieve any information about user [%s]: [%s] - Remove the user_name directive from the configuration file in order to avoid identity switch", userStr, err)
|
dlog.Fatalf(
|
||||||
|
"Unable to retrieve any information about user [%s]: [%s] - Remove the user_name directive from the configuration file in order to avoid identity switch",
|
||||||
|
userStr,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
dlog.Warnf("Unable to retrieve any information about user [%s]: [%s] - Switching to user id [%v] with the same group id, as [%v] looks like a user id. But you should remove or fix the user_name directive in the configuration file if possible", userStr, err, uid, uid)
|
dlog.Warnf(
|
||||||
|
"Unable to retrieve any information about user [%s]: [%s] - Switching to user id [%v] with the same group id, as [%v] looks like a user id. But you should remove or fix the user_name directive in the configuration file if possible",
|
||||||
|
userStr,
|
||||||
|
err,
|
||||||
|
uid,
|
||||||
|
uid,
|
||||||
|
)
|
||||||
userInfo = &user.User{Uid: userStr, Gid: userStr}
|
userInfo = &user.User{Uid: userStr, Gid: userStr}
|
||||||
}
|
}
|
||||||
uid, err := strconv.Atoi(userInfo.Uid)
|
uid, err := strconv.Atoi(userInfo.Uid)
|
||||||
|
|
|
@ -68,9 +68,13 @@ type Proxy struct {
|
||||||
nxLogFile string
|
nxLogFile string
|
||||||
proxySecretKey [32]byte
|
proxySecretKey [32]byte
|
||||||
proxyPublicKey [32]byte
|
proxyPublicKey [32]byte
|
||||||
|
ServerNames []string
|
||||||
|
DisabledServerNames []string
|
||||||
|
requiredProps stamps.ServerInformalProperties
|
||||||
certRefreshDelayAfterFailure time.Duration
|
certRefreshDelayAfterFailure time.Duration
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
certRefreshDelay time.Duration
|
certRefreshDelay time.Duration
|
||||||
|
certRefreshConcurrency int
|
||||||
cacheSize int
|
cacheSize int
|
||||||
logMaxBackups int
|
logMaxBackups int
|
||||||
logMaxAge int
|
logMaxAge int
|
||||||
|
@ -83,6 +87,7 @@ type Proxy struct {
|
||||||
cacheMinTTL uint32
|
cacheMinTTL uint32
|
||||||
cacheNegMaxTTL uint32
|
cacheNegMaxTTL uint32
|
||||||
cloakTTL uint32
|
cloakTTL uint32
|
||||||
|
cloakedPTR bool
|
||||||
cache bool
|
cache bool
|
||||||
pluginBlockIPv6 bool
|
pluginBlockIPv6 bool
|
||||||
ephemeralKeys bool
|
ephemeralKeys bool
|
||||||
|
@ -93,9 +98,6 @@ type Proxy struct {
|
||||||
anonDirectCertFallback bool
|
anonDirectCertFallback bool
|
||||||
pluginBlockUndelegated bool
|
pluginBlockUndelegated bool
|
||||||
child bool
|
child bool
|
||||||
requiredProps stamps.ServerInformalProperties
|
|
||||||
ServerNames []string
|
|
||||||
DisabledServerNames []string
|
|
||||||
SourceIPv4 bool
|
SourceIPv4 bool
|
||||||
SourceIPv6 bool
|
SourceIPv6 bool
|
||||||
SourceDNSCrypt bool
|
SourceDNSCrypt bool
|
||||||
|
@ -116,11 +118,18 @@ func (proxy *Proxy) registerLocalDoHListener(listener *net.TCPListener) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (proxy *Proxy) addDNSListener(listenAddrStr string) {
|
func (proxy *Proxy) addDNSListener(listenAddrStr string) {
|
||||||
listenUDPAddr, err := net.ResolveUDPAddr("udp", listenAddrStr)
|
udp := "udp"
|
||||||
|
tcp := "tcp"
|
||||||
|
isIPv4 := isDigit(listenAddrStr[0])
|
||||||
|
if isIPv4 {
|
||||||
|
udp = "udp4"
|
||||||
|
tcp = "tcp4"
|
||||||
|
}
|
||||||
|
listenUDPAddr, err := net.ResolveUDPAddr(udp, listenAddrStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dlog.Fatal(err)
|
dlog.Fatal(err)
|
||||||
}
|
}
|
||||||
listenTCPAddr, err := net.ResolveTCPAddr("tcp", listenAddrStr)
|
listenTCPAddr, err := net.ResolveTCPAddr(tcp, listenAddrStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dlog.Fatal(err)
|
dlog.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -139,11 +148,11 @@ func (proxy *Proxy) addDNSListener(listenAddrStr string) {
|
||||||
// if 'userName' is set and we are the parent process
|
// if 'userName' is set and we are the parent process
|
||||||
if !proxy.child {
|
if !proxy.child {
|
||||||
// parent
|
// parent
|
||||||
listenerUDP, err := net.ListenUDP("udp", listenUDPAddr)
|
listenerUDP, err := net.ListenUDP(udp, listenUDPAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dlog.Fatal(err)
|
dlog.Fatal(err)
|
||||||
}
|
}
|
||||||
listenerTCP, err := net.ListenTCP("tcp", listenTCPAddr)
|
listenerTCP, err := net.ListenTCP(tcp, listenTCPAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dlog.Fatal(err)
|
dlog.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -184,7 +193,12 @@ func (proxy *Proxy) addDNSListener(listenAddrStr string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (proxy *Proxy) addLocalDoHListener(listenAddrStr string) {
|
func (proxy *Proxy) addLocalDoHListener(listenAddrStr string) {
|
||||||
listenTCPAddr, err := net.ResolveTCPAddr("tcp", listenAddrStr)
|
network := "tcp"
|
||||||
|
isIPv4 := isDigit(listenAddrStr[0])
|
||||||
|
if isIPv4 {
|
||||||
|
network = "tcp4"
|
||||||
|
}
|
||||||
|
listenTCPAddr, err := net.ResolveTCPAddr(network, listenAddrStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dlog.Fatal(err)
|
dlog.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -200,7 +214,7 @@ func (proxy *Proxy) addLocalDoHListener(listenAddrStr string) {
|
||||||
// if 'userName' is set and we are the parent process
|
// if 'userName' is set and we are the parent process
|
||||||
if !proxy.child {
|
if !proxy.child {
|
||||||
// parent
|
// parent
|
||||||
listenerTCP, err := net.ListenTCP("tcp", listenTCPAddr)
|
listenerTCP, err := net.ListenTCP(network, listenTCPAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dlog.Fatal(err)
|
dlog.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -232,6 +246,17 @@ func (proxy *Proxy) StartProxy() {
|
||||||
}
|
}
|
||||||
curve25519.ScalarBaseMult(&proxy.proxyPublicKey, &proxy.proxySecretKey)
|
curve25519.ScalarBaseMult(&proxy.proxyPublicKey, &proxy.proxySecretKey)
|
||||||
proxy.startAcceptingClients()
|
proxy.startAcceptingClients()
|
||||||
|
if !proxy.child {
|
||||||
|
// Notify the service manager that dnscrypt-proxy is ready. dnscrypt-proxy manages itself in case
|
||||||
|
// servers are not immediately live/reachable. The service manager may assume it is initialized and
|
||||||
|
// functioning properly. Note that the service manager 'Ready' signal is delayed if netprobe
|
||||||
|
// cannot reach the internet during start-up.
|
||||||
|
if err := ServiceManagerReadyNotify(); err != nil {
|
||||||
|
dlog.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxy.xTransport.internalResolverReady = false
|
||||||
|
proxy.xTransport.internalResolvers = proxy.listenAddresses
|
||||||
liveServers, err := proxy.serversInfo.refresh(proxy)
|
liveServers, err := proxy.serversInfo.refresh(proxy)
|
||||||
if liveServers > 0 {
|
if liveServers > 0 {
|
||||||
proxy.certIgnoreTimestamp = false
|
proxy.certIgnoreTimestamp = false
|
||||||
|
@ -241,11 +266,6 @@ func (proxy *Proxy) StartProxy() {
|
||||||
}
|
}
|
||||||
if liveServers > 0 {
|
if liveServers > 0 {
|
||||||
dlog.Noticef("dnscrypt-proxy is ready - live servers: %d", liveServers)
|
dlog.Noticef("dnscrypt-proxy is ready - live servers: %d", liveServers)
|
||||||
if !proxy.child {
|
|
||||||
if err := ServiceManagerReadyNotify(); err != nil {
|
|
||||||
dlog.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
dlog.Error(err)
|
dlog.Error(err)
|
||||||
dlog.Notice("dnscrypt-proxy is waiting for at least one server to be reachable")
|
dlog.Notice("dnscrypt-proxy is waiting for at least one server to be reachable")
|
||||||
|
@ -283,10 +303,16 @@ func (proxy *Proxy) updateRegisteredServers() error {
|
||||||
dlog.Criticalf("Unable to use source [%s]: [%s]", source.name, err)
|
dlog.Criticalf("Unable to use source [%s]: [%s]", source.name, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
dlog.Warnf("Error in source [%s]: [%s] -- Continuing with reduced server count [%d]", source.name, err, len(registeredServers))
|
dlog.Warnf(
|
||||||
|
"Error in source [%s]: [%s] -- Continuing with reduced server count [%d]",
|
||||||
|
source.name,
|
||||||
|
err,
|
||||||
|
len(registeredServers),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
for _, registeredServer := range registeredServers {
|
for _, registeredServer := range registeredServers {
|
||||||
if registeredServer.stamp.Proto != stamps.StampProtoTypeDNSCryptRelay && registeredServer.stamp.Proto != stamps.StampProtoTypeODoHRelay {
|
if registeredServer.stamp.Proto != stamps.StampProtoTypeDNSCryptRelay &&
|
||||||
|
registeredServer.stamp.Proto != stamps.StampProtoTypeODoHRelay {
|
||||||
if len(proxy.ServerNames) > 0 {
|
if len(proxy.ServerNames) > 0 {
|
||||||
if !includesName(proxy.ServerNames, registeredServer.name) {
|
if !includesName(proxy.ServerNames, registeredServer.name) {
|
||||||
continue
|
continue
|
||||||
|
@ -310,13 +336,19 @@ func (proxy *Proxy) updateRegisteredServers() error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if registeredServer.stamp.Proto == stamps.StampProtoTypeDNSCryptRelay || registeredServer.stamp.Proto == stamps.StampProtoTypeODoHRelay {
|
if registeredServer.stamp.Proto == stamps.StampProtoTypeDNSCryptRelay ||
|
||||||
|
registeredServer.stamp.Proto == stamps.StampProtoTypeODoHRelay {
|
||||||
var found bool
|
var found bool
|
||||||
for i, currentRegisteredRelay := range proxy.registeredRelays {
|
for i, currentRegisteredRelay := range proxy.registeredRelays {
|
||||||
if currentRegisteredRelay.name == registeredServer.name {
|
if currentRegisteredRelay.name == registeredServer.name {
|
||||||
found = true
|
found = true
|
||||||
if currentRegisteredRelay.stamp.String() != registeredServer.stamp.String() {
|
if currentRegisteredRelay.stamp.String() != registeredServer.stamp.String() {
|
||||||
dlog.Infof("Updating stamp for [%s] was: %s now: %s", registeredServer.name, currentRegisteredRelay.stamp.String(), registeredServer.stamp.String())
|
dlog.Infof(
|
||||||
|
"Updating stamp for [%s] was: %s now: %s",
|
||||||
|
registeredServer.name,
|
||||||
|
currentRegisteredRelay.stamp.String(),
|
||||||
|
registeredServer.stamp.String(),
|
||||||
|
)
|
||||||
proxy.registeredRelays[i].stamp = registeredServer.stamp
|
proxy.registeredRelays[i].stamp = registeredServer.stamp
|
||||||
dlog.Debugf("Total count of registered relays %v", len(proxy.registeredRelays))
|
dlog.Debugf("Total count of registered relays %v", len(proxy.registeredRelays))
|
||||||
}
|
}
|
||||||
|
@ -370,7 +402,15 @@ func (proxy *Proxy) udpListener(clientPc *net.UDPConn) {
|
||||||
packet := buffer[:length]
|
packet := buffer[:length]
|
||||||
if !proxy.clientsCountInc() {
|
if !proxy.clientsCountInc() {
|
||||||
dlog.Warnf("Too many incoming connections (max=%d)", proxy.maxClients)
|
dlog.Warnf("Too many incoming connections (max=%d)", proxy.maxClients)
|
||||||
proxy.processIncomingQuery("udp", proxy.mainProto, packet, &clientAddr, clientPc, time.Now(), true) // respond synchronously, but only to cached/synthesized queries
|
proxy.processIncomingQuery(
|
||||||
|
"udp",
|
||||||
|
proxy.mainProto,
|
||||||
|
packet,
|
||||||
|
&clientAddr,
|
||||||
|
clientPc,
|
||||||
|
time.Now(),
|
||||||
|
true,
|
||||||
|
) // respond synchronously, but only to cached/synthesized queries
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -414,7 +454,13 @@ func (proxy *Proxy) udpListenerFromAddr(listenAddr *net.UDPAddr) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
clientPc, err := listenConfig.ListenPacket(context.Background(), "udp", listenAddr.String())
|
listenAddrStr := listenAddr.String()
|
||||||
|
network := "udp"
|
||||||
|
isIPv4 := isDigit(listenAddrStr[0])
|
||||||
|
if isIPv4 {
|
||||||
|
network = "udp4"
|
||||||
|
}
|
||||||
|
clientPc, err := listenConfig.ListenPacket(context.Background(), network, listenAddrStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -428,7 +474,13 @@ func (proxy *Proxy) tcpListenerFromAddr(listenAddr *net.TCPAddr) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
acceptPc, err := listenConfig.Listen(context.Background(), "tcp", listenAddr.String())
|
listenAddrStr := listenAddr.String()
|
||||||
|
network := "tcp"
|
||||||
|
isIPv4 := isDigit(listenAddrStr[0])
|
||||||
|
if isIPv4 {
|
||||||
|
network = "tcp4"
|
||||||
|
}
|
||||||
|
acceptPc, err := listenConfig.Listen(context.Background(), network, listenAddrStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -442,7 +494,13 @@ func (proxy *Proxy) localDoHListenerFromAddr(listenAddr *net.TCPAddr) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
acceptPc, err := listenConfig.Listen(context.Background(), "tcp", listenAddr.String())
|
listenAddrStr := listenAddr.String()
|
||||||
|
network := "tcp"
|
||||||
|
isIPv4 := isDigit(listenAddrStr[0])
|
||||||
|
if isIPv4 {
|
||||||
|
network = "tcp4"
|
||||||
|
}
|
||||||
|
acceptPc, err := listenConfig.Listen(context.Background(), network, listenAddrStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -476,7 +534,12 @@ func (proxy *Proxy) prepareForRelay(ip net.IP, port int, encryptedQuery *[]byte)
|
||||||
*encryptedQuery = relayedQuery
|
*encryptedQuery = relayedQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
func (proxy *Proxy) exchangeWithUDPServer(serverInfo *ServerInfo, sharedKey *[32]byte, encryptedQuery []byte, clientNonce []byte) ([]byte, error) {
|
func (proxy *Proxy) exchangeWithUDPServer(
|
||||||
|
serverInfo *ServerInfo,
|
||||||
|
sharedKey *[32]byte,
|
||||||
|
encryptedQuery []byte,
|
||||||
|
clientNonce []byte,
|
||||||
|
) ([]byte, error) {
|
||||||
upstreamAddr := serverInfo.UDPAddr
|
upstreamAddr := serverInfo.UDPAddr
|
||||||
if serverInfo.Relay != nil && serverInfo.Relay.Dnscrypt != nil {
|
if serverInfo.Relay != nil && serverInfo.Relay.Dnscrypt != nil {
|
||||||
upstreamAddr = serverInfo.Relay.Dnscrypt.RelayUDPAddr
|
upstreamAddr = serverInfo.Relay.Dnscrypt.RelayUDPAddr
|
||||||
|
@ -485,7 +548,7 @@ func (proxy *Proxy) exchangeWithUDPServer(serverInfo *ServerInfo, sharedKey *[32
|
||||||
var pc net.Conn
|
var pc net.Conn
|
||||||
proxyDialer := proxy.xTransport.proxyDialer
|
proxyDialer := proxy.xTransport.proxyDialer
|
||||||
if proxyDialer == nil {
|
if proxyDialer == nil {
|
||||||
pc, err = net.DialUDP("udp", nil, upstreamAddr)
|
pc, err = net.DialTimeout("udp", upstreamAddr.String(), serverInfo.Timeout)
|
||||||
} else {
|
} else {
|
||||||
pc, err = (*proxyDialer).Dial("udp", upstreamAddr.String())
|
pc, err = (*proxyDialer).Dial("udp", upstreamAddr.String())
|
||||||
}
|
}
|
||||||
|
@ -514,7 +577,12 @@ func (proxy *Proxy) exchangeWithUDPServer(serverInfo *ServerInfo, sharedKey *[32
|
||||||
return proxy.Decrypt(serverInfo, sharedKey, encryptedResponse, clientNonce)
|
return proxy.Decrypt(serverInfo, sharedKey, encryptedResponse, clientNonce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (proxy *Proxy) exchangeWithTCPServer(serverInfo *ServerInfo, sharedKey *[32]byte, encryptedQuery []byte, clientNonce []byte) ([]byte, error) {
|
func (proxy *Proxy) exchangeWithTCPServer(
|
||||||
|
serverInfo *ServerInfo,
|
||||||
|
sharedKey *[32]byte,
|
||||||
|
encryptedQuery []byte,
|
||||||
|
clientNonce []byte,
|
||||||
|
) ([]byte, error) {
|
||||||
upstreamAddr := serverInfo.TCPAddr
|
upstreamAddr := serverInfo.TCPAddr
|
||||||
if serverInfo.Relay != nil && serverInfo.Relay.Dnscrypt != nil {
|
if serverInfo.Relay != nil && serverInfo.Relay.Dnscrypt != nil {
|
||||||
upstreamAddr = serverInfo.Relay.Dnscrypt.RelayTCPAddr
|
upstreamAddr = serverInfo.Relay.Dnscrypt.RelayTCPAddr
|
||||||
|
@ -523,7 +591,7 @@ func (proxy *Proxy) exchangeWithTCPServer(serverInfo *ServerInfo, sharedKey *[32
|
||||||
var pc net.Conn
|
var pc net.Conn
|
||||||
proxyDialer := proxy.xTransport.proxyDialer
|
proxyDialer := proxy.xTransport.proxyDialer
|
||||||
if proxyDialer == nil {
|
if proxyDialer == nil {
|
||||||
pc, err = net.DialTCP("tcp", nil, upstreamAddr)
|
pc, err = net.DialTimeout("tcp", upstreamAddr.String(), serverInfo.Timeout)
|
||||||
} else {
|
} else {
|
||||||
pc, err = (*proxyDialer).Dial("tcp", upstreamAddr.String())
|
pc, err = (*proxyDialer).Dial("tcp", upstreamAddr.String())
|
||||||
}
|
}
|
||||||
|
@ -566,14 +634,23 @@ func (proxy *Proxy) clientsCountInc() bool {
|
||||||
|
|
||||||
func (proxy *Proxy) clientsCountDec() {
|
func (proxy *Proxy) clientsCountDec() {
|
||||||
for {
|
for {
|
||||||
if count := atomic.LoadUint32(&proxy.clientsCount); count == 0 || atomic.CompareAndSwapUint32(&proxy.clientsCount, count, count-1) {
|
if count := atomic.LoadUint32(&proxy.clientsCount); count == 0 ||
|
||||||
|
atomic.CompareAndSwapUint32(&proxy.clientsCount, count, count-1) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (proxy *Proxy) processIncomingQuery(clientProto string, serverProto string, query []byte, clientAddr *net.Addr, clientPc net.Conn, start time.Time, onlyCached bool) []byte {
|
func (proxy *Proxy) processIncomingQuery(
|
||||||
var response []byte = nil
|
clientProto string,
|
||||||
|
serverProto string,
|
||||||
|
query []byte,
|
||||||
|
clientAddr *net.Addr,
|
||||||
|
clientPc net.Conn,
|
||||||
|
start time.Time,
|
||||||
|
onlyCached bool,
|
||||||
|
) []byte {
|
||||||
|
var response []byte
|
||||||
if len(query) < MinDNSPacketSize {
|
if len(query) < MinDNSPacketSize {
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
@ -773,7 +850,7 @@ func (proxy *Proxy) processIncomingQuery(clientProto string, serverProto string,
|
||||||
if pluginsState.dnssec {
|
if pluginsState.dnssec {
|
||||||
dlog.Debug("A response had an invalid DNSSEC signature")
|
dlog.Debug("A response had an invalid DNSSEC signature")
|
||||||
} else {
|
} else {
|
||||||
dlog.Infof("Server [%v] returned temporary error code SERVFAIL -- Invalid DNSSEC signature received or server may be experiencing connectivity issues", serverInfo.Name)
|
dlog.Infof("A response with status code 2 was received - this is usually a temporary, remote issue with the configuration of the domain name")
|
||||||
serverInfo.noticeFailure(proxy)
|
serverInfo.noticeFailure(proxy)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -11,10 +11,12 @@ import (
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
const myResolverHost string = "resolver.dnscrypt.info."
|
const (
|
||||||
const nonexistentName string = "nonexistent-zone.dnscrypt-test."
|
myResolverHost string = "resolver.dnscrypt.info."
|
||||||
|
nonexistentName string = "nonexistent-zone.dnscrypt-test."
|
||||||
|
)
|
||||||
|
|
||||||
func resolveQuery(server string, qName string, qType uint16) (*dns.Msg, error) {
|
func resolveQuery(server string, qName string, qType uint16, sendClientSubnet bool) (*dns.Msg, error) {
|
||||||
client := new(dns.Client)
|
client := new(dns.Client)
|
||||||
client.ReadTimeout = 2 * time.Second
|
client.ReadTimeout = 2 * time.Second
|
||||||
msg := &dns.Msg{
|
msg := &dns.Msg{
|
||||||
|
@ -30,9 +32,27 @@ func resolveQuery(server string, qName string, qType uint16) (*dns.Msg, error) {
|
||||||
Rrtype: dns.TypeOPT,
|
Rrtype: dns.TypeOPT,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if sendClientSubnet {
|
||||||
|
subnet := net.IPNet{IP: net.IPv4(93, 184, 216, 0), Mask: net.CIDRMask(24, 32)}
|
||||||
|
prr := dns.EDNS0_SUBNET{}
|
||||||
|
prr.Code = dns.EDNS0SUBNET
|
||||||
|
bits, totalSize := subnet.Mask.Size()
|
||||||
|
if totalSize == 32 {
|
||||||
|
prr.Family = 1
|
||||||
|
} else if totalSize == 128 { // if we want to test with IPv6
|
||||||
|
prr.Family = 2
|
||||||
|
}
|
||||||
|
prr.SourceNetmask = uint8(bits)
|
||||||
|
prr.SourceScope = 0
|
||||||
|
prr.Address = subnet.IP
|
||||||
|
options.Option = append(options.Option, &prr)
|
||||||
|
}
|
||||||
|
|
||||||
msg.Extra = append(msg.Extra, options)
|
msg.Extra = append(msg.Extra, options)
|
||||||
options.SetDo()
|
options.SetDo()
|
||||||
options.SetUDPSize(uint16(MaxDNSPacketSize))
|
options.SetUDPSize(uint16(MaxDNSPacketSize))
|
||||||
|
|
||||||
msg.Question[0] = dns.Question{Name: qName, Qtype: qType, Qclass: dns.ClassINET}
|
msg.Question[0] = dns.Question{Name: qName, Qtype: qType, Qclass: dns.ClassINET}
|
||||||
msg.Id = dns.Id()
|
msg.Id = dns.Id()
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
|
@ -69,9 +89,10 @@ func Resolve(server string, name string, singleResolver bool) {
|
||||||
name = dns.Fqdn(name)
|
name = dns.Fqdn(name)
|
||||||
|
|
||||||
cname := name
|
cname := name
|
||||||
|
var clientSubnet string
|
||||||
|
|
||||||
for once := true; once; once = false {
|
for once := true; once; once = false {
|
||||||
response, err := resolveQuery(server, myResolverHost, dns.TypeA)
|
response, err := resolveQuery(server, myResolverHost, dns.TypeTXT, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Unable to resolve: [%s]\n", err)
|
fmt.Printf("Unable to resolve: [%s]\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
@ -79,17 +100,22 @@ func Resolve(server string, name string, singleResolver bool) {
|
||||||
fmt.Printf("Resolver : ")
|
fmt.Printf("Resolver : ")
|
||||||
res := make([]string, 0)
|
res := make([]string, 0)
|
||||||
for _, answer := range response.Answer {
|
for _, answer := range response.Answer {
|
||||||
if answer.Header().Class != dns.ClassINET {
|
if answer.Header().Class != dns.ClassINET || answer.Header().Rrtype != dns.TypeTXT {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var ip string
|
var ip string
|
||||||
if answer.Header().Rrtype == dns.TypeA {
|
for _, txt := range answer.(*dns.TXT).Txt {
|
||||||
ip = answer.(*dns.A).A.String()
|
if strings.HasPrefix(txt, "Resolver IP: ") {
|
||||||
} else if answer.Header().Rrtype == dns.TypeAAAA {
|
ip = strings.TrimPrefix(txt, "Resolver IP: ")
|
||||||
ip = answer.(*dns.AAAA).AAAA.String()
|
} else if strings.HasPrefix(txt, "EDNS0 client subnet: ") {
|
||||||
|
clientSubnet = strings.TrimPrefix(txt, "EDNS0 client subnet: ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ip == "" {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if rev, err := dns.ReverseAddr(ip); err == nil {
|
if rev, err := dns.ReverseAddr(ip); err == nil {
|
||||||
response, err = resolveQuery(server, rev, dns.TypePTR)
|
response, err = resolveQuery(server, rev, dns.TypePTR, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -113,8 +139,9 @@ func Resolve(server string, name string, singleResolver bool) {
|
||||||
if singleResolver {
|
if singleResolver {
|
||||||
for once := true; once; once = false {
|
for once := true; once; once = false {
|
||||||
fmt.Printf("Lying : ")
|
fmt.Printf("Lying : ")
|
||||||
response, err := resolveQuery(server, nonexistentName, dns.TypeA)
|
response, err := resolveQuery(server, nonexistentName, dns.TypeA, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Printf("[%v]", err)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if response.Rcode == dns.RcodeSuccess {
|
if response.Rcode == dns.RcodeSuccess {
|
||||||
|
@ -133,6 +160,13 @@ func Resolve(server string, name string, singleResolver bool) {
|
||||||
fmt.Println("no, the resolver doesn't support DNSSEC")
|
fmt.Println("no, the resolver doesn't support DNSSEC")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("ECS : ")
|
||||||
|
if clientSubnet != "" {
|
||||||
|
fmt.Println("client network address is sent to authoritative servers")
|
||||||
|
} else {
|
||||||
|
fmt.Println("ignored or selective")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +176,7 @@ cname:
|
||||||
for once := true; once; once = false {
|
for once := true; once; once = false {
|
||||||
fmt.Printf("Canonical name: ")
|
fmt.Printf("Canonical name: ")
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; i < 100; i++ {
|
||||||
response, err := resolveQuery(server, cname, dns.TypeCNAME)
|
response, err := resolveQuery(server, cname, dns.TypeCNAME, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break cname
|
break cname
|
||||||
}
|
}
|
||||||
|
@ -166,7 +200,7 @@ cname:
|
||||||
|
|
||||||
for once := true; once; once = false {
|
for once := true; once; once = false {
|
||||||
fmt.Printf("IPv4 addresses: ")
|
fmt.Printf("IPv4 addresses: ")
|
||||||
response, err := resolveQuery(server, cname, dns.TypeA)
|
response, err := resolveQuery(server, cname, dns.TypeA, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -186,7 +220,7 @@ cname:
|
||||||
|
|
||||||
for once := true; once; once = false {
|
for once := true; once; once = false {
|
||||||
fmt.Printf("IPv6 addresses: ")
|
fmt.Printf("IPv6 addresses: ")
|
||||||
response, err := resolveQuery(server, cname, dns.TypeAAAA)
|
response, err := resolveQuery(server, cname, dns.TypeAAAA, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -208,7 +242,7 @@ cname:
|
||||||
|
|
||||||
for once := true; once; once = false {
|
for once := true; once; once = false {
|
||||||
fmt.Printf("Name servers : ")
|
fmt.Printf("Name servers : ")
|
||||||
response, err := resolveQuery(server, cname, dns.TypeNS)
|
response, err := resolveQuery(server, cname, dns.TypeNS, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -238,7 +272,7 @@ cname:
|
||||||
|
|
||||||
for once := true; once; once = false {
|
for once := true; once; once = false {
|
||||||
fmt.Printf("Mail servers : ")
|
fmt.Printf("Mail servers : ")
|
||||||
response, err := resolveQuery(server, cname, dns.TypeMX)
|
response, err := resolveQuery(server, cname, dns.TypeMX, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -262,7 +296,7 @@ cname:
|
||||||
|
|
||||||
for once := true; once; once = false {
|
for once := true; once; once = false {
|
||||||
fmt.Printf("HTTPS alias : ")
|
fmt.Printf("HTTPS alias : ")
|
||||||
response, err := resolveQuery(server, cname, dns.TypeHTTPS)
|
response, err := resolveQuery(server, cname, dns.TypeHTTPS, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -308,7 +342,7 @@ cname:
|
||||||
|
|
||||||
for once := true; once; once = false {
|
for once := true; once; once = false {
|
||||||
fmt.Printf("Host info : ")
|
fmt.Printf("Host info : ")
|
||||||
response, err := resolveQuery(server, cname, dns.TypeHINFO)
|
response, err := resolveQuery(server, cname, dns.TypeHINFO, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -328,7 +362,7 @@ cname:
|
||||||
|
|
||||||
for once := true; once; once = false {
|
for once := true; once; once = false {
|
||||||
fmt.Printf("TXT records : ")
|
fmt.Printf("TXT records : ")
|
||||||
response, err := resolveQuery(server, cname, dns.TypeTXT)
|
response, err := resolveQuery(server, cname, dns.TypeTXT, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,7 @@ type ServerInfo struct {
|
||||||
|
|
||||||
type LBStrategy interface {
|
type LBStrategy interface {
|
||||||
getCandidate(serversCount int) int
|
getCandidate(serversCount int) int
|
||||||
|
getActiveCount(serversCount int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
type LBStrategyP2 struct{}
|
type LBStrategyP2 struct{}
|
||||||
|
@ -75,30 +76,50 @@ func (LBStrategyP2) getCandidate(serversCount int) int {
|
||||||
return rand.Intn(Min(serversCount, 2))
|
return rand.Intn(Min(serversCount, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (LBStrategyP2) getActiveCount(serversCount int) int {
|
||||||
|
return Min(serversCount, 2)
|
||||||
|
}
|
||||||
|
|
||||||
type LBStrategyPN struct{ n int }
|
type LBStrategyPN struct{ n int }
|
||||||
|
|
||||||
func (s LBStrategyPN) getCandidate(serversCount int) int {
|
func (s LBStrategyPN) getCandidate(serversCount int) int {
|
||||||
return rand.Intn(Min(serversCount, s.n))
|
return rand.Intn(Min(serversCount, s.n))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s LBStrategyPN) getActiveCount(serversCount int) int {
|
||||||
|
return Min(serversCount, s.n)
|
||||||
|
}
|
||||||
|
|
||||||
type LBStrategyPH struct{}
|
type LBStrategyPH struct{}
|
||||||
|
|
||||||
func (LBStrategyPH) getCandidate(serversCount int) int {
|
func (LBStrategyPH) getCandidate(serversCount int) int {
|
||||||
return rand.Intn(Max(Min(serversCount, 2), serversCount/2))
|
return rand.Intn(Max(Min(serversCount, 2), serversCount/2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (LBStrategyPH) getActiveCount(serversCount int) int {
|
||||||
|
return Max(Min(serversCount, 2), serversCount/2)
|
||||||
|
}
|
||||||
|
|
||||||
type LBStrategyFirst struct{}
|
type LBStrategyFirst struct{}
|
||||||
|
|
||||||
func (LBStrategyFirst) getCandidate(int) int {
|
func (LBStrategyFirst) getCandidate(int) int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (LBStrategyFirst) getActiveCount(int) int {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
type LBStrategyRandom struct{}
|
type LBStrategyRandom struct{}
|
||||||
|
|
||||||
func (LBStrategyRandom) getCandidate(serversCount int) int {
|
func (LBStrategyRandom) getCandidate(serversCount int) int {
|
||||||
return rand.Intn(serversCount)
|
return rand.Intn(serversCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (LBStrategyRandom) getActiveCount(serversCount int) int {
|
||||||
|
return serversCount
|
||||||
|
}
|
||||||
|
|
||||||
var DefaultLBStrategy = LBStrategyP2{}
|
var DefaultLBStrategy = LBStrategyP2{}
|
||||||
|
|
||||||
type DNSCryptRelay struct {
|
type DNSCryptRelay struct {
|
||||||
|
@ -126,7 +147,12 @@ type ServersInfo struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServersInfo() ServersInfo {
|
func NewServersInfo() ServersInfo {
|
||||||
return ServersInfo{lbStrategy: DefaultLBStrategy, lbEstimator: true, registeredServers: make([]RegisteredServer, 0), registeredRelays: make([]RegisteredServer, 0)}
|
return ServersInfo{
|
||||||
|
lbStrategy: DefaultLBStrategy,
|
||||||
|
lbEstimator: true,
|
||||||
|
registeredServers: make([]RegisteredServer, 0),
|
||||||
|
registeredRelays: make([]RegisteredServer, 0),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (serversInfo *ServersInfo) registerServer(name string, stamp stamps.ServerStamp) {
|
func (serversInfo *ServersInfo) registerServer(name string, stamp stamps.ServerStamp) {
|
||||||
|
@ -197,15 +223,35 @@ func (serversInfo *ServersInfo) refreshServer(proxy *Proxy, name string, stamp s
|
||||||
func (serversInfo *ServersInfo) refresh(proxy *Proxy) (int, error) {
|
func (serversInfo *ServersInfo) refresh(proxy *Proxy) (int, error) {
|
||||||
dlog.Debug("Refreshing certificates")
|
dlog.Debug("Refreshing certificates")
|
||||||
serversInfo.RLock()
|
serversInfo.RLock()
|
||||||
registeredServers := serversInfo.registeredServers
|
// Appending registeredServers slice from sources may allocate new memory.
|
||||||
|
serversCount := len(serversInfo.registeredServers)
|
||||||
|
registeredServers := make([]RegisteredServer, serversCount)
|
||||||
|
copy(registeredServers, serversInfo.registeredServers)
|
||||||
serversInfo.RUnlock()
|
serversInfo.RUnlock()
|
||||||
|
countChannel := make(chan struct{}, proxy.certRefreshConcurrency)
|
||||||
|
errorChannel := make(chan error, serversCount)
|
||||||
|
for i := range registeredServers {
|
||||||
|
countChannel <- struct{}{}
|
||||||
|
go func(registeredServer *RegisteredServer) {
|
||||||
|
err := serversInfo.refreshServer(proxy, registeredServer.name, registeredServer.stamp)
|
||||||
|
if err == nil {
|
||||||
|
proxy.xTransport.internalResolverReady = true
|
||||||
|
}
|
||||||
|
errorChannel <- err
|
||||||
|
<-countChannel
|
||||||
|
}(®isteredServers[i])
|
||||||
|
}
|
||||||
liveServers := 0
|
liveServers := 0
|
||||||
var err error
|
var err error
|
||||||
for _, registeredServer := range registeredServers {
|
for i := 0; i < serversCount; i++ {
|
||||||
if err = serversInfo.refreshServer(proxy, registeredServer.name, registeredServer.stamp); err == nil {
|
err = <-errorChannel
|
||||||
|
if err == nil {
|
||||||
liveServers++
|
liveServers++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if liveServers > 0 {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
serversInfo.Lock()
|
serversInfo.Lock()
|
||||||
sort.SliceStable(serversInfo.inner, func(i, j int) bool {
|
sort.SliceStable(serversInfo.inner, func(i, j int) bool {
|
||||||
return serversInfo.inner[i].initialRtt < serversInfo.inner[j].initialRtt
|
return serversInfo.inner[i].initialRtt < serversInfo.inner[j].initialRtt
|
||||||
|
@ -225,31 +271,44 @@ func (serversInfo *ServersInfo) refresh(proxy *Proxy) (int, error) {
|
||||||
return liveServers, err
|
return liveServers, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (serversInfo *ServersInfo) estimatorUpdate() {
|
func (serversInfo *ServersInfo) estimatorUpdate(currentActive int) {
|
||||||
// serversInfo.RWMutex is assumed to be Locked
|
// serversInfo.RWMutex is assumed to be Locked
|
||||||
candidate := rand.Intn(len(serversInfo.inner))
|
serversCount := len(serversInfo.inner)
|
||||||
if candidate == 0 {
|
activeCount := serversInfo.lbStrategy.getActiveCount(serversCount)
|
||||||
|
if activeCount == serversCount {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
candidateRtt, currentBestRtt := serversInfo.inner[candidate].rtt.Value(), serversInfo.inner[0].rtt.Value()
|
candidate := rand.Intn(serversCount-activeCount) + activeCount
|
||||||
if currentBestRtt < 0 {
|
candidateRtt, currentActiveRtt := serversInfo.inner[candidate].rtt.Value(), serversInfo.inner[currentActive].rtt.Value()
|
||||||
currentBestRtt = candidateRtt
|
if currentActiveRtt < 0 {
|
||||||
serversInfo.inner[0].rtt.Set(currentBestRtt)
|
currentActiveRtt = candidateRtt
|
||||||
|
serversInfo.inner[currentActive].rtt.Set(currentActiveRtt)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
partialSort := false
|
partialSort := false
|
||||||
if candidateRtt < currentBestRtt {
|
if candidateRtt < currentActiveRtt {
|
||||||
serversInfo.inner[candidate], serversInfo.inner[0] = serversInfo.inner[0], serversInfo.inner[candidate]
|
serversInfo.inner[candidate], serversInfo.inner[currentActive] = serversInfo.inner[currentActive], serversInfo.inner[candidate]
|
||||||
|
dlog.Debugf(
|
||||||
|
"New preferred candidate: %s (RTT: %d vs previous: %d)",
|
||||||
|
serversInfo.inner[currentActive].Name,
|
||||||
|
int(candidateRtt),
|
||||||
|
int(currentActiveRtt),
|
||||||
|
)
|
||||||
partialSort = true
|
partialSort = true
|
||||||
dlog.Debugf("New preferred candidate: %v (rtt: %d vs previous: %d)", serversInfo.inner[0].Name, int(candidateRtt), int(currentBestRtt))
|
} else if candidateRtt > 0 && candidateRtt >= (serversInfo.inner[0].rtt.Value()+serversInfo.inner[activeCount-1].rtt.Value())/2.0*4.0 {
|
||||||
} else if candidateRtt > 0 && candidateRtt >= currentBestRtt*4.0 {
|
|
||||||
if time.Since(serversInfo.inner[candidate].lastActionTS) > time.Duration(1*time.Minute) {
|
if time.Since(serversInfo.inner[candidate].lastActionTS) > time.Duration(1*time.Minute) {
|
||||||
serversInfo.inner[candidate].rtt.Add(MinF(MaxF(candidateRtt/2.0, currentBestRtt*2.0), candidateRtt))
|
serversInfo.inner[candidate].rtt.Add(candidateRtt / 2.0)
|
||||||
dlog.Debugf("Giving a new chance to candidate [%s], lowering its RTT from %d to %d (best: %d)", serversInfo.inner[candidate].Name, int(candidateRtt), int(serversInfo.inner[candidate].rtt.Value()), int(currentBestRtt))
|
dlog.Debugf(
|
||||||
|
"Giving a new chance to candidate [%s], lowering its RTT from %d to %d (best: %d)",
|
||||||
|
serversInfo.inner[candidate].Name,
|
||||||
|
int(candidateRtt),
|
||||||
|
int(serversInfo.inner[candidate].rtt.Value()),
|
||||||
|
int(serversInfo.inner[0].rtt.Value()),
|
||||||
|
)
|
||||||
partialSort = true
|
partialSort = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if partialSort {
|
if partialSort {
|
||||||
serversCount := len(serversInfo.inner)
|
|
||||||
for i := 1; i < serversCount; i++ {
|
for i := 1; i < serversCount; i++ {
|
||||||
if serversInfo.inner[i-1].rtt.Value() > serversInfo.inner[i].rtt.Value() {
|
if serversInfo.inner[i-1].rtt.Value() > serversInfo.inner[i].rtt.Value() {
|
||||||
serversInfo.inner[i-1], serversInfo.inner[i] = serversInfo.inner[i], serversInfo.inner[i-1]
|
serversInfo.inner[i-1], serversInfo.inner[i] = serversInfo.inner[i], serversInfo.inner[i-1]
|
||||||
|
@ -265,12 +324,12 @@ func (serversInfo *ServersInfo) getOne() *ServerInfo {
|
||||||
serversInfo.Unlock()
|
serversInfo.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if serversInfo.lbEstimator {
|
|
||||||
serversInfo.estimatorUpdate()
|
|
||||||
}
|
|
||||||
candidate := serversInfo.lbStrategy.getCandidate(serversCount)
|
candidate := serversInfo.lbStrategy.getCandidate(serversCount)
|
||||||
|
if serversInfo.lbEstimator {
|
||||||
|
serversInfo.estimatorUpdate(candidate)
|
||||||
|
}
|
||||||
serverInfo := serversInfo.inner[candidate]
|
serverInfo := serversInfo.inner[candidate]
|
||||||
dlog.Debugf("Using candidate [%s] RTT: %d", (*serverInfo).Name, int((*serverInfo).rtt.Value()))
|
dlog.Debugf("Using candidate [%s] RTT: %d", serverInfo.Name, int(serverInfo.rtt.Value()))
|
||||||
serversInfo.Unlock()
|
serversInfo.Unlock()
|
||||||
|
|
||||||
return serverInfo
|
return serverInfo
|
||||||
|
@ -297,12 +356,13 @@ func findFarthestRoute(proxy *Proxy, name string, relayStamps []stamps.ServerSta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if serverIdx < 0 {
|
if serverIdx < 0 {
|
||||||
|
proxy.serversInfo.RUnlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
server := proxy.serversInfo.registeredServers[serverIdx]
|
server := proxy.serversInfo.registeredServers[serverIdx]
|
||||||
proxy.serversInfo.RUnlock()
|
proxy.serversInfo.RUnlock()
|
||||||
|
|
||||||
// Fall back to random relays until the logic is implementeed for non-DNSCrypt relays
|
// Fall back to random relays until the logic is implemented for non-DNSCrypt relays
|
||||||
if server.stamp.Proto == stamps.StampProtoTypeODoHTarget {
|
if server.stamp.Proto == stamps.StampProtoTypeODoHTarget {
|
||||||
candidates := make([]int, 0)
|
candidates := make([]int, 0)
|
||||||
for relayIdx, relayStamp := range relayStamps {
|
for relayIdx, relayStamp := range relayStamps {
|
||||||
|
@ -392,16 +452,19 @@ func route(proxy *Proxy, name string, serverProto stamps.StampProtoType) (*Relay
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
relayStamps := make([]stamps.ServerStamp, 0)
|
relayStamps := make([]stamps.ServerStamp, 0)
|
||||||
|
relayStampToName := make(map[string]string)
|
||||||
for _, relayName := range relayNames {
|
for _, relayName := range relayNames {
|
||||||
if relayStamp, err := stamps.NewServerStampFromString(relayName); err == nil {
|
if relayStamp, err := stamps.NewServerStampFromString(relayName); err == nil {
|
||||||
if relayStamp.Proto == relayProto {
|
if relayStamp.Proto == relayProto {
|
||||||
relayStamps = append(relayStamps, relayStamp)
|
relayStamps = append(relayStamps, relayStamp)
|
||||||
|
relayStampToName[relayStamp.String()] = relayName
|
||||||
}
|
}
|
||||||
} else if relayName == "*" {
|
} else if relayName == "*" {
|
||||||
proxy.serversInfo.RLock()
|
proxy.serversInfo.RLock()
|
||||||
for _, registeredServer := range proxy.serversInfo.registeredRelays {
|
for _, registeredServer := range proxy.serversInfo.registeredRelays {
|
||||||
if registeredServer.stamp.Proto == relayProto {
|
if registeredServer.stamp.Proto == relayProto {
|
||||||
relayStamps = append(relayStamps, registeredServer.stamp)
|
relayStamps = append(relayStamps, registeredServer.stamp)
|
||||||
|
relayStampToName[registeredServer.stamp.String()] = registeredServer.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
proxy.serversInfo.RUnlock()
|
proxy.serversInfo.RUnlock()
|
||||||
|
@ -412,12 +475,7 @@ func route(proxy *Proxy, name string, serverProto stamps.StampProtoType) (*Relay
|
||||||
for _, registeredServer := range proxy.serversInfo.registeredRelays {
|
for _, registeredServer := range proxy.serversInfo.registeredRelays {
|
||||||
if registeredServer.name == relayName && registeredServer.stamp.Proto == relayProto {
|
if registeredServer.name == relayName && registeredServer.stamp.Proto == relayProto {
|
||||||
relayStamps = append(relayStamps, registeredServer.stamp)
|
relayStamps = append(relayStamps, registeredServer.stamp)
|
||||||
break
|
relayStampToName[registeredServer.stamp.String()] = relayName
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, registeredServer := range proxy.serversInfo.registeredServers {
|
|
||||||
if registeredServer.name == relayName && registeredServer.stamp.Proto == relayProto {
|
|
||||||
relayStamps = append(relayStamps, registeredServer.stamp)
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -425,8 +483,8 @@ func route(proxy *Proxy, name string, serverProto stamps.StampProtoType) (*Relay
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(relayStamps) == 0 {
|
if len(relayStamps) == 0 {
|
||||||
dlog.Warnf("Empty relay set for [%v]", name)
|
err := fmt.Errorf("Non-existent relay set for server [%v]", name)
|
||||||
return nil, nil
|
return nil, err
|
||||||
}
|
}
|
||||||
var relayCandidateStamp *stamps.ServerStamp
|
var relayCandidateStamp *stamps.ServerStamp
|
||||||
if !wildcard || len(relayStamps) == 1 {
|
if !wildcard || len(relayStamps) == 1 {
|
||||||
|
@ -437,15 +495,7 @@ func route(proxy *Proxy, name string, serverProto stamps.StampProtoType) (*Relay
|
||||||
if relayCandidateStamp == nil {
|
if relayCandidateStamp == nil {
|
||||||
return nil, fmt.Errorf("No valid relay for server [%v]", name)
|
return nil, fmt.Errorf("No valid relay for server [%v]", name)
|
||||||
}
|
}
|
||||||
relayName := relayCandidateStamp.ServerAddrStr
|
relayName := relayStampToName[relayCandidateStamp.String()]
|
||||||
proxy.serversInfo.RLock()
|
|
||||||
for _, registeredServer := range proxy.serversInfo.registeredRelays {
|
|
||||||
if registeredServer.stamp.ServerAddrStr == relayCandidateStamp.ServerAddrStr {
|
|
||||||
relayName = registeredServer.name
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
proxy.serversInfo.RUnlock()
|
|
||||||
switch relayCandidateStamp.Proto {
|
switch relayCandidateStamp.Proto {
|
||||||
case stamps.StampProtoTypeDNSCrypt, stamps.StampProtoTypeDNSCryptRelay:
|
case stamps.StampProtoTypeDNSCrypt, stamps.StampProtoTypeDNSCryptRelay:
|
||||||
relayUDPAddr, err := net.ResolveUDPAddr("udp", relayCandidateStamp.ServerAddrStr)
|
relayUDPAddr, err := net.ResolveUDPAddr("udp", relayCandidateStamp.ServerAddrStr)
|
||||||
|
@ -457,9 +507,14 @@ func route(proxy *Proxy, name string, serverProto stamps.StampProtoType) (*Relay
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
dlog.Noticef("Anonymizing queries for [%v] via [%v]", name, relayName)
|
dlog.Noticef("Anonymizing queries for [%v] via [%v]", name, relayName)
|
||||||
return &Relay{Proto: stamps.StampProtoTypeDNSCryptRelay, Dnscrypt: &DNSCryptRelay{RelayUDPAddr: relayUDPAddr, RelayTCPAddr: relayTCPAddr}}, nil
|
return &Relay{
|
||||||
|
Proto: stamps.StampProtoTypeDNSCryptRelay,
|
||||||
|
Dnscrypt: &DNSCryptRelay{RelayUDPAddr: relayUDPAddr, RelayTCPAddr: relayTCPAddr},
|
||||||
|
}, nil
|
||||||
case stamps.StampProtoTypeODoHRelay:
|
case stamps.StampProtoTypeODoHRelay:
|
||||||
relayBaseURL, err := url.Parse("https://" + url.PathEscape(relayCandidateStamp.ProviderName) + relayCandidateStamp.Path)
|
relayBaseURL, err := url.Parse(
|
||||||
|
"https://" + url.PathEscape(relayCandidateStamp.ProviderName) + relayCandidateStamp.Path,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -496,7 +551,7 @@ func route(proxy *Proxy, name string, serverProto stamps.StampProtoType) (*Relay
|
||||||
|
|
||||||
func fetchDNSCryptServerInfo(proxy *Proxy, name string, stamp stamps.ServerStamp, isNew bool) (ServerInfo, error) {
|
func fetchDNSCryptServerInfo(proxy *Proxy, name string, stamp stamps.ServerStamp, isNew bool) (ServerInfo, error) {
|
||||||
if len(stamp.ServerPk) != ed25519.PublicKeySize {
|
if len(stamp.ServerPk) != ed25519.PublicKeySize {
|
||||||
serverPk, err := hex.DecodeString(strings.Replace(string(stamp.ServerPk), ":", "", -1))
|
serverPk, err := hex.DecodeString(strings.ReplaceAll(string(stamp.ServerPk), ":", ""))
|
||||||
if err != nil || len(serverPk) != ed25519.PublicKeySize {
|
if err != nil || len(serverPk) != ed25519.PublicKeySize {
|
||||||
dlog.Fatalf("Unsupported public key for [%s]: [%s]", name, stamp.ServerPk)
|
dlog.Fatalf("Unsupported public key for [%s]: [%s]", name, stamp.ServerPk)
|
||||||
}
|
}
|
||||||
|
@ -519,7 +574,17 @@ func fetchDNSCryptServerInfo(proxy *Proxy, name string, stamp stamps.ServerStamp
|
||||||
if relay != nil {
|
if relay != nil {
|
||||||
dnscryptRelay = relay.Dnscrypt
|
dnscryptRelay = relay.Dnscrypt
|
||||||
}
|
}
|
||||||
certInfo, rtt, fragmentsBlocked, err := FetchCurrentDNSCryptCert(proxy, &name, proxy.mainProto, stamp.ServerPk, stamp.ServerAddrStr, stamp.ProviderName, isNew, dnscryptRelay, knownBugs)
|
certInfo, rtt, fragmentsBlocked, err := FetchCurrentDNSCryptCert(
|
||||||
|
proxy,
|
||||||
|
&name,
|
||||||
|
proxy.mainProto,
|
||||||
|
stamp.ServerPk,
|
||||||
|
stamp.ServerAddrStr,
|
||||||
|
stamp.ProviderName,
|
||||||
|
isNew,
|
||||||
|
dnscryptRelay,
|
||||||
|
knownBugs,
|
||||||
|
)
|
||||||
if !knownBugs.fragmentsBlocked && fragmentsBlocked {
|
if !knownBugs.fragmentsBlocked && fragmentsBlocked {
|
||||||
dlog.Debugf("[%v] drops fragmented queries", name)
|
dlog.Debugf("[%v] drops fragmented queries", name)
|
||||||
knownBugs.fragmentsBlocked = true
|
knownBugs.fragmentsBlocked = true
|
||||||
|
@ -567,7 +632,7 @@ func dohTestPacket(msgID uint16) []byte {
|
||||||
msg.SetEdns0(uint16(MaxDNSPacketSize), false)
|
msg.SetEdns0(uint16(MaxDNSPacketSize), false)
|
||||||
ext := new(dns.EDNS0_PADDING)
|
ext := new(dns.EDNS0_PADDING)
|
||||||
ext.Padding = make([]byte, 16)
|
ext.Padding = make([]byte, 16)
|
||||||
crypto_rand.Read(ext.Padding)
|
_, _ = crypto_rand.Read(ext.Padding)
|
||||||
edns0 := msg.IsEdns0()
|
edns0 := msg.IsEdns0()
|
||||||
edns0.Option = append(edns0.Option, ext)
|
edns0.Option = append(edns0.Option, ext)
|
||||||
body, err := msg.Pack()
|
body, err := msg.Pack()
|
||||||
|
@ -590,7 +655,7 @@ func dohNXTestPacket(msgID uint16) []byte {
|
||||||
msg.SetEdns0(uint16(MaxDNSPacketSize), false)
|
msg.SetEdns0(uint16(MaxDNSPacketSize), false)
|
||||||
ext := new(dns.EDNS0_PADDING)
|
ext := new(dns.EDNS0_PADDING)
|
||||||
ext.Padding = make([]byte, 16)
|
ext.Padding = make([]byte, 16)
|
||||||
crypto_rand.Read(ext.Padding)
|
_, _ = crypto_rand.Read(ext.Padding)
|
||||||
edns0 := msg.IsEdns0()
|
edns0 := msg.IsEdns0()
|
||||||
edns0.Option = append(edns0.Option, ext)
|
edns0.Option = append(edns0.Option, ext)
|
||||||
body, err := msg.Pack()
|
body, err := msg.Pack()
|
||||||
|
@ -648,7 +713,7 @@ func fetchDoHServerInfo(proxy *Proxy, name string, stamp stamps.ServerStamp, isN
|
||||||
protocol = "http/1.x"
|
protocol = "http/1.x"
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(protocol, "http/1.") {
|
if strings.HasPrefix(protocol, "http/1.") {
|
||||||
dlog.Warnf("[%s] does not support HTTP/2", name)
|
dlog.Warnf("[%s] does not support HTTP/2 nor HTTP/3", name)
|
||||||
}
|
}
|
||||||
dlog.Infof("[%s] TLS version: %x - Protocol: %v - Cipher suite: %v", name, tls.Version, protocol, tls.CipherSuite)
|
dlog.Infof("[%s] TLS version: %x - Protocol: %v - Cipher suite: %v", name, tls.Version, protocol, tls.CipherSuite)
|
||||||
showCerts := proxy.showCerts
|
showCerts := proxy.showCerts
|
||||||
|
@ -728,7 +793,10 @@ func _fetchODoHTargetInfo(proxy *Proxy, name string, stamp stamps.ServerStamp, i
|
||||||
}
|
}
|
||||||
|
|
||||||
if relay == nil {
|
if relay == nil {
|
||||||
dlog.Criticalf("No relay defined for [%v] - Configuring a relay is required for ODoH servers (see the `[anonymized_dns]` section)", name)
|
dlog.Criticalf(
|
||||||
|
"No relay defined for [%v] - Configuring a relay is required for ODoH servers (see the `[anonymized_dns]` section)",
|
||||||
|
name,
|
||||||
|
)
|
||||||
return ServerInfo{}, errors.New("No ODoH relay")
|
return ServerInfo{}, errors.New("No ODoH relay")
|
||||||
} else {
|
} else {
|
||||||
if relay.ODoH == nil {
|
if relay.ODoH == nil {
|
||||||
|
@ -776,7 +844,12 @@ func _fetchODoHTargetInfo(proxy *Proxy, name string, stamp stamps.ServerStamp, i
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
responseBody, responseCode, tls, rtt, err := proxy.xTransport.ObliviousDoHQuery(useGet, url, odohQuery.odohMessage, proxy.timeout)
|
responseBody, responseCode, tls, rtt, err := proxy.xTransport.ObliviousDoHQuery(
|
||||||
|
useGet,
|
||||||
|
url,
|
||||||
|
odohQuery.odohMessage,
|
||||||
|
proxy.timeout,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -798,42 +871,57 @@ func _fetchODoHTargetInfo(proxy *Proxy, name string, stamp stamps.ServerStamp, i
|
||||||
if msg.Rcode != dns.RcodeNameError {
|
if msg.Rcode != dns.RcodeNameError {
|
||||||
dlog.Criticalf("[%s] may be a lying resolver", name)
|
dlog.Criticalf("[%s] may be a lying resolver", name)
|
||||||
}
|
}
|
||||||
|
protocol := "http"
|
||||||
protocol := tls.NegotiatedProtocol
|
tlsVersion := uint16(0)
|
||||||
if len(protocol) == 0 {
|
tlsCipherSuite := uint16(0)
|
||||||
protocol = "http/1.x"
|
if tls != nil {
|
||||||
|
protocol = tls.NegotiatedProtocol
|
||||||
|
if len(protocol) == 0 {
|
||||||
|
protocol = "http/1.x"
|
||||||
|
} else {
|
||||||
|
tlsVersion = tls.Version
|
||||||
|
tlsCipherSuite = tls.CipherSuite
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(protocol, "http/1.") {
|
if strings.HasPrefix(protocol, "http/1.") {
|
||||||
dlog.Warnf("[%s] does not support HTTP/2", name)
|
dlog.Warnf("[%s] does not support HTTP/2", name)
|
||||||
}
|
}
|
||||||
dlog.Infof("[%s] TLS version: %x - Protocol: %v - Cipher suite: %v", name, tls.Version, protocol, tls.CipherSuite)
|
dlog.Infof(
|
||||||
|
"[%s] TLS version: %x - Protocol: %v - Cipher suite: %v",
|
||||||
|
name,
|
||||||
|
tlsVersion,
|
||||||
|
protocol,
|
||||||
|
tlsCipherSuite,
|
||||||
|
)
|
||||||
showCerts := proxy.showCerts
|
showCerts := proxy.showCerts
|
||||||
found := false
|
found := false
|
||||||
var wantedHash [32]byte
|
var wantedHash [32]byte
|
||||||
for _, cert := range tls.PeerCertificates {
|
if tls != nil {
|
||||||
h := sha256.Sum256(cert.RawTBSCertificate)
|
for _, cert := range tls.PeerCertificates {
|
||||||
if showCerts {
|
h := sha256.Sum256(cert.RawTBSCertificate)
|
||||||
dlog.Noticef("Advertised relay cert: [%s] [%x]", cert.Subject, h)
|
if showCerts {
|
||||||
} else {
|
dlog.Noticef("Advertised relay cert: [%s] [%x]", cert.Subject, h)
|
||||||
dlog.Debugf("Advertised relay cert: [%s] [%x]", cert.Subject, h)
|
} else {
|
||||||
}
|
dlog.Debugf("Advertised relay cert: [%s] [%x]", cert.Subject, h)
|
||||||
for _, hash := range stamp.Hashes {
|
}
|
||||||
if len(hash) == len(wantedHash) {
|
for _, hash := range stamp.Hashes {
|
||||||
copy(wantedHash[:], hash)
|
if len(hash) == len(wantedHash) {
|
||||||
if h == wantedHash {
|
copy(wantedHash[:], hash)
|
||||||
found = true
|
if h == wantedHash {
|
||||||
break
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if found {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if found {
|
if !found && len(stamp.Hashes) > 0 {
|
||||||
break
|
dlog.Criticalf("[%s] Certificate hash [%x] not found", name, wantedHash)
|
||||||
|
return ServerInfo{}, fmt.Errorf("Certificate hash not found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found && len(stamp.Hashes) > 0 {
|
|
||||||
dlog.Criticalf("[%s] Certificate hash [%x] not found", name, wantedHash)
|
|
||||||
return ServerInfo{}, fmt.Errorf("Certificate hash not found")
|
|
||||||
}
|
|
||||||
if len(serverResponse) < MinDNSPacketSize || len(serverResponse) > MaxDNSPacketSize ||
|
if len(serverResponse) < MinDNSPacketSize || len(serverResponse) > MaxDNSPacketSize ||
|
||||||
serverResponse[0] != 0xca || serverResponse[1] != 0xfe || serverResponse[4] != 0x00 || serverResponse[5] != 0x01 {
|
serverResponse[0] != 0xca || serverResponse[1] != 0xfe || serverResponse[4] != 0x00 || serverResponse[5] != 0x01 {
|
||||||
dlog.Info("Webserver returned an unexpected response")
|
dlog.Info("Webserver returned an unexpected response")
|
||||||
|
|
|
@ -8,13 +8,15 @@ import (
|
||||||
clocksmith "github.com/jedisct1/go-clocksmith"
|
clocksmith "github.com/jedisct1/go-clocksmith"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const SdNotifyStatus = "STATUS="
|
||||||
|
|
||||||
func ServiceManagerStartNotify() error {
|
func ServiceManagerStartNotify() error {
|
||||||
daemon.SdNotify(false, "STATUS=Starting")
|
daemon.SdNotify(false, SdNotifyStatus+"Starting...")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ServiceManagerReadyNotify() error {
|
func ServiceManagerReadyNotify() error {
|
||||||
daemon.SdNotify(false, "READY=1")
|
daemon.SdNotify(false, daemon.SdNotifyReady+"\n"+SdNotifyStatus+"Ready")
|
||||||
return systemDWatchdog()
|
return systemDWatchdog()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,10 +28,9 @@ func systemDWatchdog() error {
|
||||||
refreshInterval := watchdogFailureDelay / 3
|
refreshInterval := watchdogFailureDelay / 3
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
daemon.SdNotify(false, "WATCHDOG=1")
|
daemon.SdNotify(false, daemon.SdNotifyWatchdog)
|
||||||
clocksmith.Sleep(refreshInterval)
|
clocksmith.Sleep(refreshInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
}()
|
}()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,12 @@ func (proxy *Proxy) udpListenerConfig() (*net.ListenConfig, error) {
|
||||||
_ = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_FREEBIND, 1)
|
_ = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_FREEBIND, 1)
|
||||||
_ = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_DF, 0)
|
_ = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_DF, 0)
|
||||||
_ = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_TOS, 0x70)
|
_ = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_TOS, 0x70)
|
||||||
_ = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_MTU_DISCOVER, syscall.IP_PMTUDISC_DONT)
|
_ = syscall.SetsockoptInt(
|
||||||
|
int(fd),
|
||||||
|
syscall.IPPROTO_IP,
|
||||||
|
syscall.IP_MTU_DISCOVER,
|
||||||
|
syscall.IP_PMTUDISC_DONT,
|
||||||
|
)
|
||||||
_ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_RCVBUFFORCE, 4096)
|
_ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_RCVBUFFORCE, 4096)
|
||||||
_ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_SNDBUFFORCE, 4096)
|
_ = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_SNDBUFFORCE, 4096)
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,6 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
@ -33,7 +32,7 @@ type Source struct {
|
||||||
name string
|
name string
|
||||||
urls []*url.URL
|
urls []*url.URL
|
||||||
format SourceFormat
|
format SourceFormat
|
||||||
in []byte
|
bin []byte
|
||||||
minisignKey *minisign.PublicKey
|
minisignKey *minisign.PublicKey
|
||||||
cacheFile string
|
cacheFile string
|
||||||
cacheTTL, prefetchDelay time.Duration
|
cacheTTL, prefetchDelay time.Duration
|
||||||
|
@ -41,83 +40,84 @@ type Source struct {
|
||||||
prefix string
|
prefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (source *Source) checkSignature(bin, sig []byte) (err error) {
|
// timeNow() is replaced by tests to provide a static value
|
||||||
var signature minisign.Signature
|
var timeNow = time.Now
|
||||||
if signature, err = minisign.DecodeSignature(string(sig)); err == nil {
|
|
||||||
|
func (source *Source) checkSignature(bin, sig []byte) error {
|
||||||
|
signature, err := minisign.DecodeSignature(string(sig))
|
||||||
|
if err == nil {
|
||||||
_, err = source.minisignKey.Verify(bin, signature)
|
_, err = source.minisignKey.Verify(bin, signature)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// timeNow can be replaced by tests to provide a static value
|
func (source *Source) fetchFromCache(now time.Time) (time.Duration, error) {
|
||||||
var timeNow = time.Now
|
var err error
|
||||||
|
|
||||||
func (source *Source) fetchFromCache(now time.Time) (delay time.Duration, err error) {
|
|
||||||
var bin, sig []byte
|
var bin, sig []byte
|
||||||
if bin, err = ioutil.ReadFile(source.cacheFile); err != nil {
|
if bin, err = os.ReadFile(source.cacheFile); err != nil {
|
||||||
return
|
return 0, err
|
||||||
}
|
}
|
||||||
if sig, err = ioutil.ReadFile(source.cacheFile + ".minisig"); err != nil {
|
if sig, err = os.ReadFile(source.cacheFile + ".minisig"); err != nil {
|
||||||
return
|
return 0, err
|
||||||
}
|
}
|
||||||
if err = source.checkSignature(bin, sig); err != nil {
|
if err = source.checkSignature(bin, sig); err != nil {
|
||||||
return
|
return 0, err
|
||||||
}
|
}
|
||||||
source.in = bin
|
source.bin = bin
|
||||||
var fi os.FileInfo
|
var fi os.FileInfo
|
||||||
if fi, err = os.Stat(source.cacheFile); err != nil {
|
if fi, err = os.Stat(source.cacheFile); err != nil {
|
||||||
return
|
return 0, err
|
||||||
}
|
}
|
||||||
|
var ttl time.Duration = 0
|
||||||
if elapsed := now.Sub(fi.ModTime()); elapsed < source.cacheTTL {
|
if elapsed := now.Sub(fi.ModTime()); elapsed < source.cacheTTL {
|
||||||
delay = source.prefetchDelay - elapsed
|
ttl = source.prefetchDelay - elapsed
|
||||||
dlog.Debugf("Source [%s] cache file [%s] is still fresh, next update: %v", source.name, source.cacheFile, delay)
|
dlog.Debugf("Source [%s] cache file [%s] is still fresh, next update: %v", source.name, source.cacheFile, ttl)
|
||||||
} else {
|
} else {
|
||||||
dlog.Debugf("Source [%s] cache file [%s] needs to be refreshed", source.name, source.cacheFile)
|
dlog.Debugf("Source [%s] cache file [%s] needs to be refreshed", source.name, source.cacheFile)
|
||||||
}
|
}
|
||||||
return
|
return ttl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeSource(f string, bin, sig []byte) (err error) {
|
func writeSource(f string, bin, sig []byte) error {
|
||||||
|
var err error
|
||||||
var fSrc, fSig *safefile.File
|
var fSrc, fSig *safefile.File
|
||||||
if fSrc, err = safefile.Create(f, 0644); err != nil {
|
if fSrc, err = safefile.Create(f, 0o644); err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
defer fSrc.Close()
|
defer fSrc.Close()
|
||||||
if fSig, err = safefile.Create(f+".minisig", 0644); err != nil {
|
if fSig, err = safefile.Create(f+".minisig", 0o644); err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
defer fSig.Close()
|
defer fSig.Close()
|
||||||
if _, err = fSrc.Write(bin); err != nil {
|
if _, err = fSrc.Write(bin); err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
if _, err = fSig.Write(sig); err != nil {
|
if _, err = fSig.Write(sig); err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
if err = fSrc.Commit(); err != nil {
|
if err = fSrc.Commit(); err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
return fSig.Commit()
|
return fSig.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (source *Source) writeToCache(bin, sig []byte, now time.Time) {
|
func (source *Source) updateCache(bin, sig []byte, now time.Time) {
|
||||||
f := source.cacheFile
|
file := source.cacheFile
|
||||||
var writeErr error // an error writing cache isn't fatal
|
absPath := file
|
||||||
defer func() {
|
if resolved, err := filepath.Abs(file); err != nil {
|
||||||
source.in = bin
|
absPath = resolved
|
||||||
if writeErr == nil {
|
}
|
||||||
return
|
|
||||||
}
|
if !bytes.Equal(source.bin, bin) {
|
||||||
if absPath, absErr := filepath.Abs(f); absErr == nil {
|
if err := writeSource(file, bin, sig); err != nil {
|
||||||
f = absPath
|
dlog.Warnf("Couldn't write cache file [%s]: %s", absPath, err) // an error writing to the cache isn't fatal
|
||||||
}
|
|
||||||
dlog.Warnf("%s: %s", f, writeErr)
|
|
||||||
}()
|
|
||||||
if !bytes.Equal(source.in, bin) {
|
|
||||||
if writeErr = writeSource(f, bin, sig); writeErr != nil {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
writeErr = os.Chtimes(f, now, now)
|
if err := os.Chtimes(file, now, now); err != nil {
|
||||||
|
dlog.Warnf("Couldn't update cache file [%s]: %s", absPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
source.bin = bin
|
||||||
}
|
}
|
||||||
|
|
||||||
func (source *Source) parseURLs(urls []string) {
|
func (source *Source) parseURLs(urls []string) {
|
||||||
|
@ -130,28 +130,32 @@ func (source *Source) parseURLs(urls []string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchFromURL(xTransport *XTransport, u *url.URL) (bin []byte, err error) {
|
func fetchFromURL(xTransport *XTransport, u *url.URL) ([]byte, error) {
|
||||||
bin, _, _, _, err = xTransport.Get(u, "", DefaultTimeout)
|
bin, _, _, _, err := xTransport.GetWithCompression(u, "", DefaultTimeout)
|
||||||
return bin, err
|
return bin, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (source *Source) fetchWithCache(xTransport *XTransport, now time.Time) (delay time.Duration, err error) {
|
func (source *Source) fetchWithCache(xTransport *XTransport, now time.Time) (time.Duration, error) {
|
||||||
if delay, err = source.fetchFromCache(now); err != nil {
|
var err error
|
||||||
|
var ttl time.Duration
|
||||||
|
if ttl, err = source.fetchFromCache(now); err != nil {
|
||||||
if len(source.urls) == 0 {
|
if len(source.urls) == 0 {
|
||||||
dlog.Errorf("Source [%s] cache file [%s] not present and no valid URL", source.name, source.cacheFile)
|
dlog.Errorf("Source [%s] cache file [%s] not present and no valid URL", source.name, source.cacheFile)
|
||||||
return
|
return 0, err
|
||||||
}
|
}
|
||||||
dlog.Debugf("Source [%s] cache file [%s] not present", source.name, source.cacheFile)
|
dlog.Debugf("Source [%s] cache file [%s] not present", source.name, source.cacheFile)
|
||||||
}
|
}
|
||||||
if len(source.urls) > 0 {
|
|
||||||
defer func() {
|
if len(source.urls) == 0 {
|
||||||
source.refresh = now.Add(delay)
|
return 0, err
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
if len(source.urls) == 0 || delay > 0 {
|
if ttl > 0 {
|
||||||
return
|
source.refresh = now.Add(ttl)
|
||||||
|
return 0, err
|
||||||
}
|
}
|
||||||
delay = MinimumPrefetchInterval
|
|
||||||
|
ttl = MinimumPrefetchInterval
|
||||||
|
source.refresh = now.Add(ttl)
|
||||||
var bin, sig []byte
|
var bin, sig []byte
|
||||||
for _, srcURL := range source.urls {
|
for _, srcURL := range source.urls {
|
||||||
dlog.Infof("Source [%s] loading from URL [%s]", source.name, srcURL)
|
dlog.Infof("Source [%s] loading from URL [%s]", source.name, srcURL)
|
||||||
|
@ -166,25 +170,43 @@ func (source *Source) fetchWithCache(xTransport *XTransport, now time.Time) (del
|
||||||
dlog.Debugf("Source [%s] failed to download signature from URL [%s]", source.name, sigURL)
|
dlog.Debugf("Source [%s] failed to download signature from URL [%s]", source.name, sigURL)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err = source.checkSignature(bin, sig); err == nil {
|
if err = source.checkSignature(bin, sig); err != nil {
|
||||||
break // valid signature
|
dlog.Debugf("Source [%s] failed signature check using URL [%s]", source.name, srcURL)
|
||||||
} // above err check inverted to make use of implicit continue
|
continue
|
||||||
dlog.Debugf("Source [%s] failed signature check using URL [%s]", source.name, srcURL)
|
}
|
||||||
|
break // valid signature
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return 0, err
|
||||||
}
|
}
|
||||||
source.writeToCache(bin, sig, now)
|
source.updateCache(bin, sig, now)
|
||||||
delay = source.prefetchDelay
|
ttl = source.prefetchDelay
|
||||||
return
|
source.refresh = now.Add(ttl)
|
||||||
|
return ttl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSource loads a new source using the given cacheFile and urls, ensuring it has a valid signature
|
// NewSource loads a new source using the given cacheFile and urls, ensuring it has a valid signature
|
||||||
func NewSource(name string, xTransport *XTransport, urls []string, minisignKeyStr string, cacheFile string, formatStr string, refreshDelay time.Duration, prefix string) (source *Source, err error) {
|
func NewSource(
|
||||||
|
name string,
|
||||||
|
xTransport *XTransport,
|
||||||
|
urls []string,
|
||||||
|
minisignKeyStr string,
|
||||||
|
cacheFile string,
|
||||||
|
formatStr string,
|
||||||
|
refreshDelay time.Duration,
|
||||||
|
prefix string,
|
||||||
|
) (*Source, error) {
|
||||||
if refreshDelay < DefaultPrefetchDelay {
|
if refreshDelay < DefaultPrefetchDelay {
|
||||||
refreshDelay = DefaultPrefetchDelay
|
refreshDelay = DefaultPrefetchDelay
|
||||||
}
|
}
|
||||||
source = &Source{name: name, urls: []*url.URL{}, cacheFile: cacheFile, cacheTTL: refreshDelay, prefetchDelay: DefaultPrefetchDelay, prefix: prefix}
|
source := &Source{
|
||||||
|
name: name,
|
||||||
|
urls: []*url.URL{},
|
||||||
|
cacheFile: cacheFile,
|
||||||
|
cacheTTL: refreshDelay,
|
||||||
|
prefetchDelay: DefaultPrefetchDelay,
|
||||||
|
prefix: prefix,
|
||||||
|
}
|
||||||
if formatStr == "v2" {
|
if formatStr == "v2" {
|
||||||
source.format = SourceFormatV2
|
source.format = SourceFormatV2
|
||||||
} else {
|
} else {
|
||||||
|
@ -196,10 +218,11 @@ func NewSource(name string, xTransport *XTransport, urls []string, minisignKeySt
|
||||||
return source, err
|
return source, err
|
||||||
}
|
}
|
||||||
source.parseURLs(urls)
|
source.parseURLs(urls)
|
||||||
if _, err = source.fetchWithCache(xTransport, timeNow()); err == nil {
|
_, err := source.fetchWithCache(xTransport, timeNow())
|
||||||
|
if err == nil {
|
||||||
dlog.Noticef("Source [%s] loaded", name)
|
dlog.Noticef("Source [%s] loaded", name)
|
||||||
}
|
}
|
||||||
return
|
return source, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrefetchSources downloads latest versions of given sources, ensuring they have a valid signature before caching
|
// PrefetchSources downloads latest versions of given sources, ensuring they have a valid signature before caching
|
||||||
|
@ -214,7 +237,7 @@ func PrefetchSources(xTransport *XTransport, sources []*Source) time.Duration {
|
||||||
if delay, err := source.fetchWithCache(xTransport, now); err != nil {
|
if delay, err := source.fetchWithCache(xTransport, now); err != nil {
|
||||||
dlog.Infof("Prefetching [%s] failed: %v, will retry in %v", source.name, err, interval)
|
dlog.Infof("Prefetching [%s] failed: %v, will retry in %v", source.name, err, interval)
|
||||||
} else {
|
} else {
|
||||||
dlog.Debugf("Prefetching [%s] succeeded, next update: %v", source.name, delay)
|
dlog.Debugf("Prefetching [%s] succeeded, next update in %v min", source.name, delay)
|
||||||
if delay >= MinimumPrefetchInterval && (interval == MinimumPrefetchInterval || interval > delay) {
|
if delay >= MinimumPrefetchInterval && (interval == MinimumPrefetchInterval || interval > delay) {
|
||||||
interval = delay
|
interval = delay
|
||||||
}
|
}
|
||||||
|
@ -239,7 +262,7 @@ func (source *Source) parseV2() ([]RegisteredServer, error) {
|
||||||
stampErrs = append(stampErrs, stampErr)
|
stampErrs = append(stampErrs, stampErr)
|
||||||
dlog.Warn(stampErr)
|
dlog.Warn(stampErr)
|
||||||
}
|
}
|
||||||
in := string(source.in)
|
in := string(source.bin)
|
||||||
parts := strings.Split(in, "## ")
|
parts := strings.Split(in, "## ")
|
||||||
if len(parts) < 2 {
|
if len(parts) < 2 {
|
||||||
return registeredServers, fmt.Errorf("Invalid format for source at [%v]", source.urls)
|
return registeredServers, fmt.Errorf("Invalid format for source at [%v]", source.urls)
|
||||||
|
|
|
@ -3,7 +3,6 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -15,9 +14,9 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hectane/go-acl"
|
"github.com/hectane/go-acl"
|
||||||
"github.com/powerman/check"
|
"github.com/jedisct1/dlog"
|
||||||
|
|
||||||
"github.com/jedisct1/go-minisign"
|
"github.com/jedisct1/go-minisign"
|
||||||
|
"github.com/powerman/check"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SourceFixture struct {
|
type SourceFixture struct {
|
||||||
|
@ -69,7 +68,7 @@ type SourceTestExpect struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func readFixture(t *testing.T, name string) []byte {
|
func readFixture(t *testing.T, name string) []byte {
|
||||||
bin, err := ioutil.ReadFile(filepath.Join("testdata", name))
|
bin, err := os.ReadFile(filepath.Join("testdata", name))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unable to read test fixture %s: %v", name, err)
|
t.Fatalf("Unable to read test fixture %s: %v", name, err)
|
||||||
}
|
}
|
||||||
|
@ -84,9 +83,9 @@ func writeSourceCache(t *testing.T, e *SourceTestExpect) {
|
||||||
path := e.cachePath + f.suffix
|
path := e.cachePath + f.suffix
|
||||||
perms := f.perms
|
perms := f.perms
|
||||||
if perms == 0 {
|
if perms == 0 {
|
||||||
perms = 0644
|
perms = 0o644
|
||||||
}
|
}
|
||||||
if err := ioutil.WriteFile(path, f.content, perms); err != nil {
|
if err := os.WriteFile(path, f.content, perms); err != nil {
|
||||||
t.Fatalf("Unable to write cache file %s: %v", path, err)
|
t.Fatalf("Unable to write cache file %s: %v", path, err)
|
||||||
}
|
}
|
||||||
if err := acl.Chmod(path, perms); err != nil {
|
if err := acl.Chmod(path, perms); err != nil {
|
||||||
|
@ -108,8 +107,8 @@ func writeSourceCache(t *testing.T, e *SourceTestExpect) {
|
||||||
func checkSourceCache(c *check.C, e *SourceTestExpect) {
|
func checkSourceCache(c *check.C, e *SourceTestExpect) {
|
||||||
for _, f := range e.cache {
|
for _, f := range e.cache {
|
||||||
path := e.cachePath + f.suffix
|
path := e.cachePath + f.suffix
|
||||||
_ = acl.Chmod(path, 0644) // don't worry if this fails, reading it will catch the same problem
|
_ = acl.Chmod(path, 0o644) // don't worry if this fails, reading it will catch the same problem
|
||||||
got, err := ioutil.ReadFile(path)
|
got, err := os.ReadFile(path)
|
||||||
c.DeepEqual(got, f.content, "Unexpected content for cache file '%s', err %v", path, err)
|
c.DeepEqual(got, f.content, "Unexpected content for cache file '%s', err %v", path, err)
|
||||||
if f.suffix != "" {
|
if f.suffix != "" {
|
||||||
continue
|
continue
|
||||||
|
@ -134,7 +133,7 @@ func loadSnakeoil(t *testing.T, d *SourceTestData) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadTestSourceNames(t *testing.T, d *SourceTestData) {
|
func loadTestSourceNames(t *testing.T, d *SourceTestData) {
|
||||||
files, err := ioutil.ReadDir(filepath.Join("testdata", "sources"))
|
files, err := os.ReadDir(filepath.Join("testdata", "sources"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unable to load list of test sources: %v", err)
|
t.Fatalf("Unable to load list of test sources: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -145,7 +144,7 @@ func loadTestSourceNames(t *testing.T, d *SourceTestData) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateFixtureState(t *testing.T, d *SourceTestData, suffix, file string, state SourceTestState) {
|
func generateFixtureState(_ *testing.T, d *SourceTestData, suffix, file string, state SourceTestState) {
|
||||||
if _, ok := d.fixtures[state]; !ok {
|
if _, ok := d.fixtures[state]; !ok {
|
||||||
d.fixtures[state] = map[string]SourceFixture{}
|
d.fixtures[state] = map[string]SourceFixture{}
|
||||||
}
|
}
|
||||||
|
@ -165,7 +164,7 @@ func generateFixtureState(t *testing.T, d *SourceTestData, suffix, file string,
|
||||||
case TestStateReadErr, TestStateReadSigErr:
|
case TestStateReadErr, TestStateReadSigErr:
|
||||||
f.content, f.length = []byte{}, "1"
|
f.content, f.length = []byte{}, "1"
|
||||||
case TestStateOpenErr, TestStateOpenSigErr:
|
case TestStateOpenErr, TestStateOpenSigErr:
|
||||||
f.content, f.perms = d.fixtures[TestStateCorrect][file].content[:1], 0200
|
f.content, f.perms = d.fixtures[TestStateCorrect][file].content[:1], 0o200
|
||||||
}
|
}
|
||||||
d.fixtures[state][file] = f
|
d.fixtures[state][file] = f
|
||||||
}
|
}
|
||||||
|
@ -196,7 +195,7 @@ func loadFixtures(t *testing.T, d *SourceTestData) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeTempDir(t *testing.T, d *SourceTestData) {
|
func makeTempDir(t *testing.T, d *SourceTestData) {
|
||||||
name, err := ioutil.TempDir("", "sources_test.go."+t.Name())
|
name, err := os.MkdirTemp("", "sources_test.go."+t.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Unable to create temporary directory: %v", err)
|
t.Fatalf("Unable to create temporary directory: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -285,9 +284,9 @@ func prepSourceTestCache(t *testing.T, d *SourceTestData, e *SourceTestExpect, s
|
||||||
e.cache = []SourceFixture{d.fixtures[state][source], d.fixtures[state][source+".minisig"]}
|
e.cache = []SourceFixture{d.fixtures[state][source], d.fixtures[state][source+".minisig"]}
|
||||||
switch state {
|
switch state {
|
||||||
case TestStateCorrect:
|
case TestStateCorrect:
|
||||||
e.Source.in, e.success = e.cache[0].content, true
|
e.Source.bin, e.success = e.cache[0].content, true
|
||||||
case TestStateExpired:
|
case TestStateExpired:
|
||||||
e.Source.in = e.cache[0].content
|
e.Source.bin = e.cache[0].content
|
||||||
case TestStatePartial, TestStatePartialSig:
|
case TestStatePartial, TestStatePartialSig:
|
||||||
e.err = "signature"
|
e.err = "signature"
|
||||||
case TestStateMissing, TestStateMissingSig, TestStateOpenErr, TestStateOpenSigErr:
|
case TestStateMissing, TestStateMissingSig, TestStateOpenErr, TestStateOpenSigErr:
|
||||||
|
@ -296,7 +295,13 @@ func prepSourceTestCache(t *testing.T, d *SourceTestData, e *SourceTestExpect, s
|
||||||
writeSourceCache(t, e)
|
writeSourceCache(t, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepSourceTestDownload(t *testing.T, d *SourceTestData, e *SourceTestExpect, source string, downloadTest []SourceTestState) {
|
func prepSourceTestDownload(
|
||||||
|
_ *testing.T,
|
||||||
|
d *SourceTestData,
|
||||||
|
e *SourceTestExpect,
|
||||||
|
source string,
|
||||||
|
downloadTest []SourceTestState,
|
||||||
|
) {
|
||||||
if len(downloadTest) == 0 {
|
if len(downloadTest) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -313,7 +318,11 @@ func prepSourceTestDownload(t *testing.T, d *SourceTestData, e *SourceTestExpect
|
||||||
case TestStateOpenErr, TestStateOpenSigErr:
|
case TestStateOpenErr, TestStateOpenSigErr:
|
||||||
if u, err := url.Parse(serverURL + path); err == nil {
|
if u, err := url.Parse(serverURL + path); err == nil {
|
||||||
host, port := ExtractHostAndPort(u.Host, -1)
|
host, port := ExtractHostAndPort(u.Host, -1)
|
||||||
u.Host = fmt.Sprintf("%s:%d", host, port|0x10000) // high numeric port is parsed but then fails to connect
|
u.Host = fmt.Sprintf(
|
||||||
|
"%s:%d",
|
||||||
|
host,
|
||||||
|
port|0x10000,
|
||||||
|
) // high numeric port is parsed but then fails to connect
|
||||||
serverURL = u.String()
|
serverURL = u.String()
|
||||||
}
|
}
|
||||||
e.err = "invalid port"
|
e.err = "invalid port"
|
||||||
|
@ -330,7 +339,7 @@ func prepSourceTestDownload(t *testing.T, d *SourceTestData, e *SourceTestExpect
|
||||||
switch state {
|
switch state {
|
||||||
case TestStateCorrect:
|
case TestStateCorrect:
|
||||||
e.cache = []SourceFixture{d.fixtures[state][source], d.fixtures[state][source+".minisig"]}
|
e.cache = []SourceFixture{d.fixtures[state][source], d.fixtures[state][source+".minisig"]}
|
||||||
e.Source.in, e.success = e.cache[0].content, true
|
e.Source.bin, e.success = e.cache[0].content, true
|
||||||
fallthrough
|
fallthrough
|
||||||
case TestStateMissingSig, TestStatePartial, TestStatePartialSig, TestStateReadSigErr:
|
case TestStateMissingSig, TestStatePartial, TestStatePartialSig, TestStateReadSigErr:
|
||||||
d.reqExpect[path+".minisig"]++
|
d.reqExpect[path+".minisig"]++
|
||||||
|
@ -353,14 +362,17 @@ func prepSourceTestDownload(t *testing.T, d *SourceTestData, e *SourceTestExpect
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupSourceTestCase(t *testing.T, d *SourceTestData, i int,
|
func setupSourceTestCase(t *testing.T, d *SourceTestData, i int,
|
||||||
cacheTest *SourceTestState, downloadTest []SourceTestState) (id string, e *SourceTestExpect) {
|
cacheTest *SourceTestState, downloadTest []SourceTestState,
|
||||||
|
) (id string, e *SourceTestExpect) {
|
||||||
id = strconv.Itoa(d.n) + "-" + strconv.Itoa(i)
|
id = strconv.Itoa(d.n) + "-" + strconv.Itoa(i)
|
||||||
e = &SourceTestExpect{
|
e = &SourceTestExpect{
|
||||||
cachePath: filepath.Join(d.tempDir, id),
|
cachePath: filepath.Join(d.tempDir, id),
|
||||||
mtime: d.timeNow,
|
mtime: d.timeNow,
|
||||||
}
|
}
|
||||||
e.Source = &Source{name: id, urls: []*url.URL{}, format: SourceFormatV2, minisignKey: d.key,
|
e.Source = &Source{
|
||||||
cacheFile: e.cachePath, cacheTTL: DefaultPrefetchDelay * 3, prefetchDelay: DefaultPrefetchDelay}
|
name: id, urls: []*url.URL{}, format: SourceFormatV2, minisignKey: d.key,
|
||||||
|
cacheFile: e.cachePath, cacheTTL: DefaultPrefetchDelay * 3, prefetchDelay: DefaultPrefetchDelay,
|
||||||
|
}
|
||||||
if cacheTest != nil {
|
if cacheTest != nil {
|
||||||
prepSourceTestCache(t, d, e, d.sources[i], *cacheTest)
|
prepSourceTestCache(t, d, e, d.sources[i], *cacheTest)
|
||||||
i = (i + 1) % len(d.sources) // make the cached and downloaded fixtures different
|
i = (i + 1) % len(d.sources) // make the cached and downloaded fixtures different
|
||||||
|
@ -370,6 +382,10 @@ func setupSourceTestCase(t *testing.T, d *SourceTestData, i int,
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewSource(t *testing.T) {
|
func TestNewSource(t *testing.T) {
|
||||||
|
if testing.Verbose() {
|
||||||
|
dlog.SetLogLevel(dlog.SeverityDebug)
|
||||||
|
dlog.UseSyslog(false)
|
||||||
|
}
|
||||||
teardown, d := setupSourceTest(t)
|
teardown, d := setupSourceTest(t)
|
||||||
defer teardown()
|
defer teardown()
|
||||||
checkResult := func(t *testing.T, e *SourceTestExpect, got *Source, err error) {
|
checkResult := func(t *testing.T, e *SourceTestExpect, got *Source, err error) {
|
||||||
|
@ -394,7 +410,16 @@ func TestNewSource(t *testing.T) {
|
||||||
{"v2", "", DefaultPrefetchDelay * 3, &SourceTestExpect{err: "Invalid encoded public key", Source: &Source{name: "invalid public key", urls: []*url.URL{}, cacheTTL: DefaultPrefetchDelay * 3, prefetchDelay: DefaultPrefetchDelay}}},
|
{"v2", "", DefaultPrefetchDelay * 3, &SourceTestExpect{err: "Invalid encoded public key", Source: &Source{name: "invalid public key", urls: []*url.URL{}, cacheTTL: DefaultPrefetchDelay * 3, prefetchDelay: DefaultPrefetchDelay}}},
|
||||||
} {
|
} {
|
||||||
t.Run(tt.e.Source.name, func(t *testing.T) {
|
t.Run(tt.e.Source.name, func(t *testing.T) {
|
||||||
got, err := NewSource(tt.e.Source.name, d.xTransport, tt.e.urls, tt.key, tt.e.cachePath, tt.v, tt.refreshDelay, tt.e.prefix)
|
got, err := NewSource(
|
||||||
|
tt.e.Source.name,
|
||||||
|
d.xTransport,
|
||||||
|
tt.e.urls,
|
||||||
|
tt.key,
|
||||||
|
tt.e.cachePath,
|
||||||
|
tt.v,
|
||||||
|
tt.refreshDelay,
|
||||||
|
tt.e.prefix,
|
||||||
|
)
|
||||||
checkResult(t, tt.e, got, err)
|
checkResult(t, tt.e, got, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -404,7 +429,16 @@ func TestNewSource(t *testing.T) {
|
||||||
for i := range d.sources {
|
for i := range d.sources {
|
||||||
id, e := setupSourceTestCase(t, d, i, &cacheTest, downloadTest)
|
id, e := setupSourceTestCase(t, d, i, &cacheTest, downloadTest)
|
||||||
t.Run("cache "+cacheTestName+", download "+downloadTestName+"/"+id, func(t *testing.T) {
|
t.Run("cache "+cacheTestName+", download "+downloadTestName+"/"+id, func(t *testing.T) {
|
||||||
got, err := NewSource(id, d.xTransport, e.urls, d.keyStr, e.cachePath, "v2", DefaultPrefetchDelay*3, "")
|
got, err := NewSource(
|
||||||
|
id,
|
||||||
|
d.xTransport,
|
||||||
|
e.urls,
|
||||||
|
d.keyStr,
|
||||||
|
e.cachePath,
|
||||||
|
"v2",
|
||||||
|
DefaultPrefetchDelay*3,
|
||||||
|
"",
|
||||||
|
)
|
||||||
checkResult(t, e, got, err)
|
checkResult(t, e, got, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -413,6 +447,10 @@ func TestNewSource(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrefetchSources(t *testing.T) {
|
func TestPrefetchSources(t *testing.T) {
|
||||||
|
if testing.Verbose() {
|
||||||
|
dlog.SetLogLevel(dlog.SeverityDebug)
|
||||||
|
dlog.UseSyslog(false)
|
||||||
|
}
|
||||||
teardown, d := setupSourceTest(t)
|
teardown, d := setupSourceTest(t)
|
||||||
defer teardown()
|
defer teardown()
|
||||||
checkResult := func(t *testing.T, expects []*SourceTestExpect, got time.Duration) {
|
checkResult := func(t *testing.T, expects []*SourceTestExpect, got time.Duration) {
|
||||||
|
@ -439,7 +477,7 @@ func TestPrefetchSources(t *testing.T) {
|
||||||
e.mtime = d.timeUpd
|
e.mtime = d.timeUpd
|
||||||
s := &Source{}
|
s := &Source{}
|
||||||
*s = *e.Source
|
*s = *e.Source
|
||||||
s.in = nil
|
s.bin = nil
|
||||||
sources = append(sources, s)
|
sources = append(sources, s)
|
||||||
expects = append(expects, e)
|
expects = append(expects, e)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,9 @@ func (proxy *Proxy) addSystemDListeners() error {
|
||||||
|
|
||||||
if len(files) > 0 {
|
if len(files) > 0 {
|
||||||
if len(proxy.userName) > 0 || proxy.child {
|
if len(proxy.userName) > 0 || proxy.child {
|
||||||
dlog.Fatal("Systemd activated sockets are incompatible with privilege dropping. Remove activated sockets and fill `listen_addresses` in the dnscrypt-proxy configuration file instead.")
|
dlog.Fatal(
|
||||||
|
"Systemd activated sockets are incompatible with privilege dropping. Remove activated sockets and fill `listen_addresses` in the dnscrypt-proxy configuration file instead.",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
dlog.Warn("Systemd sockets are untested and unsupported - use at your own risk")
|
dlog.Warn("Systemd sockets are untested and unsupported - use at your own risk")
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,15 @@ func parseTimeRanges(timeRangesStr []TimeRangeStr) ([]TimeRange, error) {
|
||||||
|
|
||||||
func parseWeeklyRanges(weeklyRangesStr WeeklyRangesStr) (WeeklyRanges, error) {
|
func parseWeeklyRanges(weeklyRangesStr WeeklyRangesStr) (WeeklyRanges, error) {
|
||||||
weeklyRanges := WeeklyRanges{}
|
weeklyRanges := WeeklyRanges{}
|
||||||
weeklyRangesStrX := [7][]TimeRangeStr{weeklyRangesStr.Sun, weeklyRangesStr.Mon, weeklyRangesStr.Tue, weeklyRangesStr.Wed, weeklyRangesStr.Thu, weeklyRangesStr.Fri, weeklyRangesStr.Sat}
|
weeklyRangesStrX := [7][]TimeRangeStr{
|
||||||
|
weeklyRangesStr.Sun,
|
||||||
|
weeklyRangesStr.Mon,
|
||||||
|
weeklyRangesStr.Tue,
|
||||||
|
weeklyRangesStr.Wed,
|
||||||
|
weeklyRangesStr.Thu,
|
||||||
|
weeklyRangesStr.Fri,
|
||||||
|
weeklyRangesStr.Sat,
|
||||||
|
}
|
||||||
for day, weeklyRangeStrX := range weeklyRangesStrX {
|
for day, weeklyRangeStrX := range weeklyRangesStrX {
|
||||||
timeRanges, err := parseTimeRanges(weeklyRangeStrX)
|
timeRanges, err := parseTimeRanges(weeklyRangeStrX)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
@ -10,11 +11,11 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -23,6 +24,8 @@ import (
|
||||||
"github.com/jedisct1/dlog"
|
"github.com/jedisct1/dlog"
|
||||||
stamps "github.com/jedisct1/go-dnsstamps"
|
stamps "github.com/jedisct1/go-dnsstamps"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
"github.com/quic-go/quic-go"
|
||||||
|
"github.com/quic-go/quic-go/http3"
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
netproxy "golang.org/x/net/proxy"
|
netproxy "golang.org/x/net/proxy"
|
||||||
)
|
)
|
||||||
|
@ -46,21 +49,32 @@ type CachedIPs struct {
|
||||||
cache map[string]*CachedIPItem
|
cache map[string]*CachedIPItem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AltSupport struct {
|
||||||
|
sync.RWMutex
|
||||||
|
cache map[string]uint16
|
||||||
|
}
|
||||||
|
|
||||||
type XTransport struct {
|
type XTransport struct {
|
||||||
transport *http.Transport
|
transport *http.Transport
|
||||||
|
h3Transport *http3.RoundTripper
|
||||||
keepAlive time.Duration
|
keepAlive time.Duration
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
cachedIPs CachedIPs
|
cachedIPs CachedIPs
|
||||||
|
altSupport AltSupport
|
||||||
|
internalResolvers []string
|
||||||
bootstrapResolvers []string
|
bootstrapResolvers []string
|
||||||
mainProto string
|
mainProto string
|
||||||
ignoreSystemDNS bool
|
ignoreSystemDNS bool
|
||||||
|
internalResolverReady bool
|
||||||
useIPv4 bool
|
useIPv4 bool
|
||||||
useIPv6 bool
|
useIPv6 bool
|
||||||
|
http3 bool
|
||||||
tlsDisableSessionTickets bool
|
tlsDisableSessionTickets bool
|
||||||
tlsCipherSuite []uint16
|
tlsCipherSuite []uint16
|
||||||
proxyDialer *netproxy.Dialer
|
proxyDialer *netproxy.Dialer
|
||||||
httpProxyFunction func(*http.Request) (*url.URL, error)
|
httpProxyFunction func(*http.Request) (*url.URL, error)
|
||||||
tlsClientCreds DOHClientCreds
|
tlsClientCreds DOHClientCreds
|
||||||
|
keyLogWriter io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewXTransport() *XTransport {
|
func NewXTransport() *XTransport {
|
||||||
|
@ -69,6 +83,7 @@ func NewXTransport() *XTransport {
|
||||||
}
|
}
|
||||||
xTransport := XTransport{
|
xTransport := XTransport{
|
||||||
cachedIPs: CachedIPs{cache: make(map[string]*CachedIPItem)},
|
cachedIPs: CachedIPs{cache: make(map[string]*CachedIPItem)},
|
||||||
|
altSupport: AltSupport{cache: make(map[string]uint16)},
|
||||||
keepAlive: DefaultKeepAlive,
|
keepAlive: DefaultKeepAlive,
|
||||||
timeout: DefaultTimeout,
|
timeout: DefaultTimeout,
|
||||||
bootstrapResolvers: []string{DefaultBootstrapResolver},
|
bootstrapResolvers: []string{DefaultBootstrapResolver},
|
||||||
|
@ -78,6 +93,7 @@ func NewXTransport() *XTransport {
|
||||||
useIPv6: false,
|
useIPv6: false,
|
||||||
tlsDisableSessionTickets: false,
|
tlsDisableSessionTickets: false,
|
||||||
tlsCipherSuite: nil,
|
tlsCipherSuite: nil,
|
||||||
|
keyLogWriter: nil,
|
||||||
}
|
}
|
||||||
return &xTransport
|
return &xTransport
|
||||||
}
|
}
|
||||||
|
@ -121,7 +137,7 @@ func (xTransport *XTransport) loadCachedIP(host string) (ip net.IP, expired bool
|
||||||
func (xTransport *XTransport) rebuildTransport() {
|
func (xTransport *XTransport) rebuildTransport() {
|
||||||
dlog.Debug("Rebuilding transport")
|
dlog.Debug("Rebuilding transport")
|
||||||
if xTransport.transport != nil {
|
if xTransport.transport != nil {
|
||||||
(*xTransport.transport).CloseIdleConnections()
|
xTransport.transport.CloseIdleConnections()
|
||||||
}
|
}
|
||||||
timeout := xTransport.timeout
|
timeout := xTransport.timeout
|
||||||
transport := &http.Transport{
|
transport := &http.Transport{
|
||||||
|
@ -145,7 +161,7 @@ func (xTransport *XTransport) rebuildTransport() {
|
||||||
ipOnly = "[" + cachedIP.String() + "]"
|
ipOnly = "[" + cachedIP.String() + "]"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dlog.Debugf("[%s] IP address was not cached", host)
|
dlog.Debugf("[%s] IP address was not cached in DialContext", host)
|
||||||
}
|
}
|
||||||
addrStr = ipOnly + ":" + strconv.Itoa(port)
|
addrStr = ipOnly + ":" + strconv.Itoa(port)
|
||||||
if xTransport.proxyDialer == nil {
|
if xTransport.proxyDialer == nil {
|
||||||
|
@ -164,11 +180,15 @@ func (xTransport *XTransport) rebuildTransport() {
|
||||||
tlsClientConfig := tls.Config{}
|
tlsClientConfig := tls.Config{}
|
||||||
certPool, certPoolErr := x509.SystemCertPool()
|
certPool, certPoolErr := x509.SystemCertPool()
|
||||||
|
|
||||||
|
if xTransport.keyLogWriter != nil {
|
||||||
|
tlsClientConfig.KeyLogWriter = xTransport.keyLogWriter
|
||||||
|
}
|
||||||
|
|
||||||
if clientCreds.rootCA != "" {
|
if clientCreds.rootCA != "" {
|
||||||
if certPool == nil {
|
if certPool == nil {
|
||||||
dlog.Fatalf("Additional CAs not supported on this platform: %v", certPoolErr)
|
dlog.Fatalf("Additional CAs not supported on this platform: %v", certPoolErr)
|
||||||
}
|
}
|
||||||
additionalCaCert, err := ioutil.ReadFile(clientCreds.rootCA)
|
additionalCaCert, err := os.ReadFile(clientCreds.rootCA)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dlog.Fatal(err)
|
dlog.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -177,7 +197,7 @@ func (xTransport *XTransport) rebuildTransport() {
|
||||||
|
|
||||||
if certPool != nil {
|
if certPool != nil {
|
||||||
// Some operating systems don't include Let's Encrypt ISRG Root X1 certificate yet
|
// Some operating systems don't include Let's Encrypt ISRG Root X1 certificate yet
|
||||||
var letsEncryptX1Cert = []byte(`-----BEGIN CERTIFICATE-----
|
letsEncryptX1Cert := []byte(`-----BEGIN CERTIFICATE-----
|
||||||
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZLubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZLubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
||||||
-----END CERTIFICATE-----`)
|
-----END CERTIFICATE-----`)
|
||||||
certPool.AppendCertsFromPEM(letsEncryptX1Cert)
|
certPool.AppendCertsFromPEM(letsEncryptX1Cert)
|
||||||
|
@ -187,7 +207,12 @@ func (xTransport *XTransport) rebuildTransport() {
|
||||||
if clientCreds.clientCert != "" {
|
if clientCreds.clientCert != "" {
|
||||||
cert, err := tls.LoadX509KeyPair(clientCreds.clientCert, clientCreds.clientKey)
|
cert, err := tls.LoadX509KeyPair(clientCreds.clientCert, clientCreds.clientKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dlog.Fatalf("Unable to use certificate [%v] (key: [%v]): %v", clientCreds.clientCert, clientCreds.clientKey, err)
|
dlog.Fatalf(
|
||||||
|
"Unable to use certificate [%v] (key: [%v]): %v",
|
||||||
|
clientCreds.clientCert,
|
||||||
|
clientCreds.clientKey,
|
||||||
|
err,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
tlsClientConfig.Certificates = []tls.Certificate{cert}
|
tlsClientConfig.Certificates = []tls.Certificate{cert}
|
||||||
}
|
}
|
||||||
|
@ -200,6 +225,30 @@ func (xTransport *XTransport) rebuildTransport() {
|
||||||
if xTransport.tlsCipherSuite != nil {
|
if xTransport.tlsCipherSuite != nil {
|
||||||
tlsClientConfig.PreferServerCipherSuites = false
|
tlsClientConfig.PreferServerCipherSuites = false
|
||||||
tlsClientConfig.CipherSuites = xTransport.tlsCipherSuite
|
tlsClientConfig.CipherSuites = xTransport.tlsCipherSuite
|
||||||
|
|
||||||
|
// Go doesn't allow changing the cipher suite with TLS 1.3
|
||||||
|
// So, check if the requested set of ciphers matches the TLS 1.3 suite.
|
||||||
|
// If it doesn't, downgrade to TLS 1.2
|
||||||
|
compatibleSuitesCount := 0
|
||||||
|
for _, suite := range tls.CipherSuites() {
|
||||||
|
if suite.Insecure {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, supportedVersion := range suite.SupportedVersions {
|
||||||
|
if supportedVersion != tls.VersionTLS13 {
|
||||||
|
for _, expectedSuiteID := range xTransport.tlsCipherSuite {
|
||||||
|
if expectedSuiteID == suite.ID {
|
||||||
|
compatibleSuitesCount += 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if compatibleSuitesCount != len(tls.CipherSuites()) {
|
||||||
|
dlog.Notice("Explicit cipher suite configured - downgrading to TLS 1.2")
|
||||||
|
tlsClientConfig.MaxVersion = tls.VersionTLS12
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
transport.TLSClientConfig = &tlsClientConfig
|
transport.TLSClientConfig = &tlsClientConfig
|
||||||
|
@ -208,6 +257,45 @@ func (xTransport *XTransport) rebuildTransport() {
|
||||||
http2Transport.AllowHTTP = false
|
http2Transport.AllowHTTP = false
|
||||||
}
|
}
|
||||||
xTransport.transport = transport
|
xTransport.transport = transport
|
||||||
|
if xTransport.http3 {
|
||||||
|
dial := func(ctx context.Context, addrStr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||||
|
dlog.Debugf("Dialing for H3: [%v]", addrStr)
|
||||||
|
host, port := ExtractHostAndPort(addrStr, stamps.DefaultPort)
|
||||||
|
ipOnly := host
|
||||||
|
cachedIP, _ := xTransport.loadCachedIP(host)
|
||||||
|
network := "udp4"
|
||||||
|
if cachedIP != nil {
|
||||||
|
if ipv4 := cachedIP.To4(); ipv4 != nil {
|
||||||
|
ipOnly = ipv4.String()
|
||||||
|
} else {
|
||||||
|
ipOnly = "[" + cachedIP.String() + "]"
|
||||||
|
network = "udp6"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dlog.Debugf("[%s] IP address was not cached in H3 context", host)
|
||||||
|
if xTransport.useIPv6 {
|
||||||
|
if xTransport.useIPv4 {
|
||||||
|
network = "udp"
|
||||||
|
} else {
|
||||||
|
network = "udp6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addrStr = ipOnly + ":" + strconv.Itoa(port)
|
||||||
|
udpAddr, err := net.ResolveUDPAddr(network, addrStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
udpConn, err := net.ListenUDP(network, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsCfg.ServerName = host
|
||||||
|
return quic.DialEarly(ctx, udpConn, udpAddr, tlsCfg, cfg)
|
||||||
|
}
|
||||||
|
h3Transport := &http3.RoundTripper{DisableCompression: true, TLSClientConfig: &tlsClientConfig, Dial: dial}
|
||||||
|
xTransport.h3Transport = h3Transport
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xTransport *XTransport) resolveUsingSystem(host string) (ip net.IP, ttl time.Duration, err error) {
|
func (xTransport *XTransport) resolveUsingSystem(host string) (ip net.IP, ttl time.Duration, err error) {
|
||||||
|
@ -238,7 +326,10 @@ func (xTransport *XTransport) resolveUsingSystem(host string) (ip net.IP, ttl ti
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xTransport *XTransport) resolveUsingResolver(proto, host string, resolver string) (ip net.IP, ttl time.Duration, err error) {
|
func (xTransport *XTransport) resolveUsingResolver(
|
||||||
|
proto, host string,
|
||||||
|
resolver string,
|
||||||
|
) (ip net.IP, ttl time.Duration, err error) {
|
||||||
dnsClient := dns.Client{Net: proto}
|
dnsClient := dns.Client{Net: proto}
|
||||||
if xTransport.useIPv4 {
|
if xTransport.useIPv4 {
|
||||||
msg := dns.Msg{}
|
msg := dns.Msg{}
|
||||||
|
@ -283,17 +374,21 @@ func (xTransport *XTransport) resolveUsingResolver(proto, host string, resolver
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xTransport *XTransport) resolveUsingResolvers(proto, host string, resolvers []string) (ip net.IP, ttl time.Duration, err error) {
|
func (xTransport *XTransport) resolveUsingResolvers(
|
||||||
|
proto, host string,
|
||||||
|
resolvers []string,
|
||||||
|
) (ip net.IP, ttl time.Duration, err error) {
|
||||||
|
err = errors.New("Empty resolvers")
|
||||||
for i, resolver := range resolvers {
|
for i, resolver := range resolvers {
|
||||||
ip, ttl, err = xTransport.resolveUsingResolver(proto, host, resolver)
|
ip, ttl, err = xTransport.resolveUsingResolver(proto, host, resolver)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
dlog.Infof("Resolution succeeded with bootstrap resolver %s[%s]", proto, resolver)
|
dlog.Infof("Resolution succeeded with resolver %s[%s]", proto, resolver)
|
||||||
resolvers[0], resolvers[i] = resolvers[i], resolvers[0]
|
resolvers[0], resolvers[i] = resolvers[i], resolvers[0]
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
dlog.Infof("Unable to resolve [%s] using bootstrap resolver %s[%s]: %v", host, proto, resolver, err)
|
dlog.Infof("Unable to resolve [%s] using resolver [%s] (%s): %v", host, resolver, proto, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -313,19 +408,37 @@ func (xTransport *XTransport) resolveAndUpdateCache(host string) error {
|
||||||
var foundIP net.IP
|
var foundIP net.IP
|
||||||
var ttl time.Duration
|
var ttl time.Duration
|
||||||
var err error
|
var err error
|
||||||
if !xTransport.ignoreSystemDNS {
|
protos := []string{"udp", "tcp"}
|
||||||
foundIP, ttl, err = xTransport.resolveUsingSystem(host)
|
if xTransport.mainProto == "tcp" {
|
||||||
|
protos = []string{"tcp", "udp"}
|
||||||
}
|
}
|
||||||
if xTransport.ignoreSystemDNS || err != nil {
|
if xTransport.ignoreSystemDNS {
|
||||||
protos := []string{"udp", "tcp"}
|
if xTransport.internalResolverReady {
|
||||||
if xTransport.mainProto == "tcp" {
|
for _, proto := range protos {
|
||||||
protos = []string{"tcp", "udp"}
|
foundIP, ttl, err = xTransport.resolveUsingResolvers(proto, host, xTransport.internalResolvers)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = errors.New("Service is not usable yet")
|
||||||
|
dlog.Notice(err)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
foundIP, ttl, err = xTransport.resolveUsingSystem(host)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("System DNS is not usable yet")
|
||||||
|
dlog.Notice(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
for _, proto := range protos {
|
for _, proto := range protos {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dlog.Noticef("System DNS configuration not usable yet, exceptionally resolving [%s] using bootstrap resolvers over %s", host, proto)
|
dlog.Noticef(
|
||||||
} else {
|
"Resolving server host [%s] using bootstrap resolvers over %s",
|
||||||
dlog.Debugf("Resolving [%s] using bootstrap resolvers over %s", host, proto)
|
host,
|
||||||
|
proto,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
foundIP, ttl, err = xTransport.resolveUsingResolvers(proto, host, xTransport.bootstrapResolvers)
|
foundIP, ttl, err = xTransport.resolveUsingResolvers(proto, host, xTransport.bootstrapResolvers)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
@ -349,16 +462,50 @@ func (xTransport *XTransport) resolveAndUpdateCache(host string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if foundIP == nil {
|
||||||
|
if !xTransport.useIPv4 && xTransport.useIPv6 {
|
||||||
|
dlog.Warnf("no IPv6 address found for [%s]", host)
|
||||||
|
} else if xTransport.useIPv4 && !xTransport.useIPv6 {
|
||||||
|
dlog.Warnf("no IPv4 address found for [%s]", host)
|
||||||
|
} else {
|
||||||
|
dlog.Errorf("no IP address found for [%s]", host)
|
||||||
|
}
|
||||||
|
}
|
||||||
xTransport.saveCachedIP(host, foundIP, ttl)
|
xTransport.saveCachedIP(host, foundIP, ttl)
|
||||||
dlog.Debugf("[%s] IP address [%s] added to the cache, valid for %v", host, foundIP, ttl)
|
dlog.Debugf("[%s] IP address [%s] added to the cache, valid for %v", host, foundIP, ttl)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xTransport *XTransport) Fetch(method string, url *url.URL, accept string, contentType string, body *[]byte, timeout time.Duration) ([]byte, int, *tls.ConnectionState, time.Duration, error) {
|
func (xTransport *XTransport) Fetch(
|
||||||
|
method string,
|
||||||
|
url *url.URL,
|
||||||
|
accept string,
|
||||||
|
contentType string,
|
||||||
|
body *[]byte,
|
||||||
|
timeout time.Duration,
|
||||||
|
compress bool,
|
||||||
|
) ([]byte, int, *tls.ConnectionState, time.Duration, error) {
|
||||||
if timeout <= 0 {
|
if timeout <= 0 {
|
||||||
timeout = xTransport.timeout
|
timeout = xTransport.timeout
|
||||||
}
|
}
|
||||||
client := http.Client{Transport: xTransport.transport, Timeout: timeout}
|
client := http.Client{
|
||||||
|
Transport: xTransport.transport,
|
||||||
|
Timeout: timeout,
|
||||||
|
}
|
||||||
|
host, port := ExtractHostAndPort(url.Host, 443)
|
||||||
|
hasAltSupport := false
|
||||||
|
if xTransport.h3Transport != nil {
|
||||||
|
xTransport.altSupport.RLock()
|
||||||
|
var altPort uint16
|
||||||
|
altPort, hasAltSupport = xTransport.altSupport.cache[url.Host]
|
||||||
|
xTransport.altSupport.RUnlock()
|
||||||
|
if hasAltSupport {
|
||||||
|
if int(altPort) == port {
|
||||||
|
client.Transport = xTransport.h3Transport
|
||||||
|
dlog.Debugf("Using HTTP/3 transport for [%s]", url.Host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
header := map[string][]string{"User-Agent": {"dnscrypt-proxy"}}
|
header := map[string][]string{"User-Agent": {"dnscrypt-proxy"}}
|
||||||
if len(accept) > 0 {
|
if len(accept) > 0 {
|
||||||
header["Accept"] = []string{accept}
|
header["Accept"] = []string{accept}
|
||||||
|
@ -375,14 +522,19 @@ func (xTransport *XTransport) Fetch(method string, url *url.URL, accept string,
|
||||||
url2.RawQuery = qs.Encode()
|
url2.RawQuery = qs.Encode()
|
||||||
url = &url2
|
url = &url2
|
||||||
}
|
}
|
||||||
host, _ := ExtractHostAndPort(url.Host, 0)
|
|
||||||
if xTransport.proxyDialer == nil && strings.HasSuffix(host, ".onion") {
|
if xTransport.proxyDialer == nil && strings.HasSuffix(host, ".onion") {
|
||||||
return nil, 0, nil, 0, errors.New("Onion service is not reachable without Tor")
|
return nil, 0, nil, 0, errors.New("Onion service is not reachable without Tor")
|
||||||
}
|
}
|
||||||
if err := xTransport.resolveAndUpdateCache(host); err != nil {
|
if err := xTransport.resolveAndUpdateCache(host); err != nil {
|
||||||
dlog.Errorf("Unable to resolve [%v] - Make sure that the system resolver works, or that `bootstrap_resolvers` has been set to resolvers that can be reached", host)
|
dlog.Errorf(
|
||||||
|
"Unable to resolve [%v] - Make sure that the system resolver works, or that `bootstrap_resolvers` has been set to resolvers that can be reached",
|
||||||
|
host,
|
||||||
|
)
|
||||||
return nil, 0, nil, 0, err
|
return nil, 0, nil, 0, err
|
||||||
}
|
}
|
||||||
|
if compress && body == nil {
|
||||||
|
header["Accept-Encoding"] = []string{"gzip"}
|
||||||
|
}
|
||||||
req := &http.Request{
|
req := &http.Request{
|
||||||
Method: method,
|
Method: method,
|
||||||
URL: url,
|
URL: url,
|
||||||
|
@ -391,7 +543,7 @@ func (xTransport *XTransport) Fetch(method string, url *url.URL, accept string,
|
||||||
}
|
}
|
||||||
if body != nil {
|
if body != nil {
|
||||||
req.ContentLength = int64(len(*body))
|
req.ContentLength = int64(len(*body))
|
||||||
req.Body = ioutil.NopCloser(bytes.NewReader(*body))
|
req.Body = io.NopCloser(bytes.NewReader(*body))
|
||||||
}
|
}
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
|
@ -403,7 +555,8 @@ func (xTransport *XTransport) Fetch(method string, url *url.URL, accept string,
|
||||||
err = errors.New(resp.Status)
|
err = errors.New(resp.Status)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
(*xTransport.transport).CloseIdleConnections()
|
dlog.Debugf("HTTP client error: [%v] - closing idle connections", err)
|
||||||
|
xTransport.transport.CloseIdleConnections()
|
||||||
}
|
}
|
||||||
statusCode := 503
|
statusCode := 503
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
|
@ -412,14 +565,53 @@ func (xTransport *XTransport) Fetch(method string, url *url.URL, accept string,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
dlog.Debugf("[%s]: [%s]", req.URL, err)
|
dlog.Debugf("[%s]: [%s]", req.URL, err)
|
||||||
if xTransport.tlsCipherSuite != nil && strings.Contains(err.Error(), "handshake failure") {
|
if xTransport.tlsCipherSuite != nil && strings.Contains(err.Error(), "handshake failure") {
|
||||||
dlog.Warnf("TLS handshake failure - Try changing or deleting the tls_cipher_suite value in the configuration file")
|
dlog.Warnf(
|
||||||
|
"TLS handshake failure - Try changing or deleting the tls_cipher_suite value in the configuration file",
|
||||||
|
)
|
||||||
xTransport.tlsCipherSuite = nil
|
xTransport.tlsCipherSuite = nil
|
||||||
xTransport.rebuildTransport()
|
xTransport.rebuildTransport()
|
||||||
}
|
}
|
||||||
return nil, statusCode, nil, rtt, err
|
return nil, statusCode, nil, rtt, err
|
||||||
}
|
}
|
||||||
|
if xTransport.h3Transport != nil && !hasAltSupport {
|
||||||
|
if alt, found := resp.Header["Alt-Svc"]; found {
|
||||||
|
dlog.Debugf("Alt-Svc [%s]: [%s]", url.Host, alt)
|
||||||
|
altPort := uint16(port & 0xffff)
|
||||||
|
for i, xalt := range alt {
|
||||||
|
for j, v := range strings.Split(xalt, ";") {
|
||||||
|
if i >= 8 || j >= 16 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if strings.HasPrefix(v, "h3=\":") {
|
||||||
|
v = strings.TrimPrefix(v, "h3=\":")
|
||||||
|
v = strings.TrimSuffix(v, "\"")
|
||||||
|
if xAltPort, err := strconv.ParseUint(v, 10, 16); err == nil && xAltPort <= 65535 {
|
||||||
|
altPort = uint16(xAltPort)
|
||||||
|
dlog.Debugf("Using HTTP/3 for [%s]", url.Host)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xTransport.altSupport.Lock()
|
||||||
|
xTransport.altSupport.cache[url.Host] = altPort
|
||||||
|
dlog.Debugf("Caching altPort for [%v]", url.Host)
|
||||||
|
xTransport.altSupport.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
tls := resp.TLS
|
tls := resp.TLS
|
||||||
bin, err := ioutil.ReadAll(io.LimitReader(resp.Body, MaxHTTPBodyLength))
|
|
||||||
|
var bodyReader io.ReadCloser = resp.Body
|
||||||
|
if compress && resp.Header.Get("Content-Encoding") == "gzip" {
|
||||||
|
bodyReader, err = gzip.NewReader(io.LimitReader(resp.Body, MaxHTTPBodyLength))
|
||||||
|
if err != nil {
|
||||||
|
return nil, statusCode, tls, rtt, err
|
||||||
|
}
|
||||||
|
defer bodyReader.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
bin, err := io.ReadAll(io.LimitReader(bodyReader, MaxHTTPBodyLength))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, statusCode, tls, rtt, err
|
return nil, statusCode, tls, rtt, err
|
||||||
}
|
}
|
||||||
|
@ -427,15 +619,39 @@ func (xTransport *XTransport) Fetch(method string, url *url.URL, accept string,
|
||||||
return bin, statusCode, tls, rtt, err
|
return bin, statusCode, tls, rtt, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xTransport *XTransport) Get(url *url.URL, accept string, timeout time.Duration) ([]byte, int, *tls.ConnectionState, time.Duration, error) {
|
func (xTransport *XTransport) GetWithCompression(
|
||||||
return xTransport.Fetch("GET", url, accept, "", nil, timeout)
|
url *url.URL,
|
||||||
|
accept string,
|
||||||
|
timeout time.Duration,
|
||||||
|
) ([]byte, int, *tls.ConnectionState, time.Duration, error) {
|
||||||
|
return xTransport.Fetch("GET", url, accept, "", nil, timeout, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xTransport *XTransport) Post(url *url.URL, accept string, contentType string, body *[]byte, timeout time.Duration) ([]byte, int, *tls.ConnectionState, time.Duration, error) {
|
func (xTransport *XTransport) Get(
|
||||||
return xTransport.Fetch("POST", url, accept, contentType, body, timeout)
|
url *url.URL,
|
||||||
|
accept string,
|
||||||
|
timeout time.Duration,
|
||||||
|
) ([]byte, int, *tls.ConnectionState, time.Duration, error) {
|
||||||
|
return xTransport.Fetch("GET", url, accept, "", nil, timeout, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xTransport *XTransport) dohLikeQuery(dataType string, useGet bool, url *url.URL, body []byte, timeout time.Duration) ([]byte, int, *tls.ConnectionState, time.Duration, error) {
|
func (xTransport *XTransport) Post(
|
||||||
|
url *url.URL,
|
||||||
|
accept string,
|
||||||
|
contentType string,
|
||||||
|
body *[]byte,
|
||||||
|
timeout time.Duration,
|
||||||
|
) ([]byte, int, *tls.ConnectionState, time.Duration, error) {
|
||||||
|
return xTransport.Fetch("POST", url, accept, contentType, body, timeout, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (xTransport *XTransport) dohLikeQuery(
|
||||||
|
dataType string,
|
||||||
|
useGet bool,
|
||||||
|
url *url.URL,
|
||||||
|
body []byte,
|
||||||
|
timeout time.Duration,
|
||||||
|
) ([]byte, int, *tls.ConnectionState, time.Duration, error) {
|
||||||
if useGet {
|
if useGet {
|
||||||
qs := url.Query()
|
qs := url.Query()
|
||||||
encBody := base64.RawURLEncoding.EncodeToString(body)
|
encBody := base64.RawURLEncoding.EncodeToString(body)
|
||||||
|
@ -447,10 +663,20 @@ func (xTransport *XTransport) dohLikeQuery(dataType string, useGet bool, url *ur
|
||||||
return xTransport.Post(url, dataType, dataType, &body, timeout)
|
return xTransport.Post(url, dataType, dataType, &body, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xTransport *XTransport) DoHQuery(useGet bool, url *url.URL, body []byte, timeout time.Duration) ([]byte, int, *tls.ConnectionState, time.Duration, error) {
|
func (xTransport *XTransport) DoHQuery(
|
||||||
|
useGet bool,
|
||||||
|
url *url.URL,
|
||||||
|
body []byte,
|
||||||
|
timeout time.Duration,
|
||||||
|
) ([]byte, int, *tls.ConnectionState, time.Duration, error) {
|
||||||
return xTransport.dohLikeQuery("application/dns-message", useGet, url, body, timeout)
|
return xTransport.dohLikeQuery("application/dns-message", useGet, url, body, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (xTransport *XTransport) ObliviousDoHQuery(useGet bool, url *url.URL, body []byte, timeout time.Duration) ([]byte, int, *tls.ConnectionState, time.Duration, error) {
|
func (xTransport *XTransport) ObliviousDoHQuery(
|
||||||
|
useGet bool,
|
||||||
|
url *url.URL,
|
||||||
|
body []byte,
|
||||||
|
timeout time.Duration,
|
||||||
|
) ([]byte, int, *tls.ConnectionState, time.Duration, error) {
|
||||||
return xTransport.dohLikeQuery("application/oblivious-dns-message", useGet, url, body, timeout)
|
return xTransport.dohLikeQuery("application/oblivious-dns-message", useGet, url, body, timeout)
|
||||||
}
|
}
|
||||||
|
|
186
go.mod
186
go.mod
|
@ -1,170 +1,52 @@
|
||||||
module github.com/dnscrypt/dnscrypt-proxy
|
module github.com/dnscrypt/dnscrypt-proxy
|
||||||
|
|
||||||
go 1.17
|
go 1.22
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v0.4.1
|
github.com/BurntSushi/toml v1.4.0
|
||||||
github.com/VividCortex/ewma v1.2.0
|
github.com/VividCortex/ewma v1.2.0
|
||||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||||
github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185
|
github.com/dchest/safefile v0.0.0-20151022103144-855e8d98f185
|
||||||
github.com/hashicorp/go-immutable-radix v1.3.1
|
github.com/hashicorp/go-immutable-radix v1.3.1
|
||||||
github.com/hashicorp/golang-lru v0.5.4
|
github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb
|
||||||
github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95
|
github.com/jedisct1/dlog v0.0.0-20230811132706-443b333ff1b3
|
||||||
github.com/jedisct1/dlog v0.0.0-20210927135244-3381aa132e7f
|
github.com/jedisct1/go-clocksmith v0.0.0-20230211133011-392c1afea73e
|
||||||
github.com/jedisct1/go-clocksmith v0.0.0-20210101121932-da382b963868
|
github.com/jedisct1/go-dnsstamps v0.0.0-20240423203910-07a0735c7774
|
||||||
github.com/jedisct1/go-dnsstamps v0.0.0-20210810213811-61cc83d2a354
|
github.com/jedisct1/go-hpke-compact v0.0.0-20230811132953-4ee502b61f80
|
||||||
github.com/jedisct1/go-hpke-compact v0.0.0-20210927135353-5b1ea328c479
|
github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267
|
||||||
github.com/jedisct1/go-minisign v0.0.0-20210927135422-df01d8d3e6f4
|
github.com/jedisct1/xsecretbox v0.0.0-20230811132812-b950633f9f1f
|
||||||
github.com/jedisct1/xsecretbox v0.0.0-20210927135450-ebe41aef7bef
|
|
||||||
github.com/k-sone/critbitgo v1.4.0
|
github.com/k-sone/critbitgo v1.4.0
|
||||||
github.com/kardianos/service v1.2.0
|
github.com/kardianos/service v1.2.2
|
||||||
github.com/miekg/dns v1.1.43
|
github.com/miekg/dns v1.1.59
|
||||||
github.com/powerman/check v1.6.0
|
github.com/opencoff/go-sieve v0.2.1
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
|
github.com/powerman/check v1.7.0
|
||||||
golang.org/x/net v0.0.0-20210924151903-3ad01bbaa167
|
github.com/quic-go/quic-go v0.44.0
|
||||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6
|
golang.org/x/crypto v0.23.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
golang.org/x/net v0.25.0
|
||||||
|
golang.org/x/sys v0.20.0
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
4d63.com/gochecknoglobals v0.0.0-20201008074935-acfc0b28355a // indirect
|
|
||||||
github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect
|
|
||||||
github.com/Masterminds/semver v1.5.0 // indirect
|
|
||||||
github.com/OpenPeeDeeP/depguard v1.0.1 // indirect
|
|
||||||
github.com/alexkohler/prealloc v1.0.0 // indirect
|
|
||||||
github.com/ashanbrown/forbidigo v1.2.0 // indirect
|
|
||||||
github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde // indirect
|
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
|
||||||
github.com/bkielbasa/cyclop v1.2.0 // indirect
|
|
||||||
github.com/bombsimon/wsl/v3 v3.3.0 // indirect
|
|
||||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
|
||||||
github.com/charithe/durationcheck v0.0.8 // indirect
|
|
||||||
github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af // indirect
|
|
||||||
github.com/daixiang0/gci v0.2.8 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/denis-tingajkin/go-header v0.4.2 // indirect
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||||
github.com/esimonov/ifshort v1.0.2 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/ettle/strcase v0.1.1 // indirect
|
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||||
github.com/fatih/color v1.12.0 // indirect
|
|
||||||
github.com/fatih/structtag v1.2.0 // indirect
|
|
||||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
|
||||||
github.com/fzipp/gocyclo v0.3.1 // indirect
|
|
||||||
github.com/go-critic/go-critic v0.5.6 // indirect
|
|
||||||
github.com/go-toolsmith/astcast v1.0.0 // indirect
|
|
||||||
github.com/go-toolsmith/astcopy v1.0.0 // indirect
|
|
||||||
github.com/go-toolsmith/astequal v1.0.0 // indirect
|
|
||||||
github.com/go-toolsmith/astfmt v1.0.0 // indirect
|
|
||||||
github.com/go-toolsmith/astp v1.0.0 // indirect
|
|
||||||
github.com/go-toolsmith/strparse v1.0.0 // indirect
|
|
||||||
github.com/go-toolsmith/typep v1.0.2 // indirect
|
|
||||||
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect
|
|
||||||
github.com/gobwas/glob v0.2.3 // indirect
|
|
||||||
github.com/gofrs/flock v0.8.0 // indirect
|
|
||||||
github.com/golang/protobuf v1.5.0 // indirect
|
|
||||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 // indirect
|
|
||||||
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect
|
|
||||||
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 // indirect
|
|
||||||
github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a // indirect
|
|
||||||
github.com/golangci/golangci-lint v1.41.1 // indirect
|
|
||||||
github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 // indirect
|
|
||||||
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca // indirect
|
|
||||||
github.com/golangci/misspell v0.3.5 // indirect
|
|
||||||
github.com/golangci/revgrep v0.0.0-20210208091834-cd28932614b5 // indirect
|
|
||||||
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect
|
|
||||||
github.com/google/go-cmp v0.5.5 // indirect
|
|
||||||
github.com/gordonklaus/ineffassign v0.0.0-20210225214923-2e10b2664254 // indirect
|
|
||||||
github.com/gostaticanalysis/analysisutil v0.4.1 // indirect
|
|
||||||
github.com/gostaticanalysis/comment v1.4.1 // indirect
|
|
||||||
github.com/gostaticanalysis/forcetypeassert v0.0.0-20200621232751-01d4955beaa5 // indirect
|
|
||||||
github.com/gostaticanalysis/nilerr v0.1.1 // indirect
|
|
||||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
|
||||||
github.com/hashicorp/go-syslog v1.0.0 // indirect
|
github.com/hashicorp/go-syslog v1.0.0 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/golang-lru v0.5.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||||
github.com/jgautheron/goconst v1.5.1 // indirect
|
|
||||||
github.com/jingyugao/rowserrcheck v1.1.0 // indirect
|
|
||||||
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect
|
|
||||||
github.com/julz/importas v0.0.0-20210419104244-841f0c0fe66d // indirect
|
|
||||||
github.com/kisielk/errcheck v1.6.0 // indirect
|
|
||||||
github.com/kisielk/gotool v1.0.0 // indirect
|
|
||||||
github.com/kulti/thelper v0.4.0 // indirect
|
|
||||||
github.com/kunwardeep/paralleltest v1.0.2 // indirect
|
|
||||||
github.com/kyoh86/exportloopref v0.1.8 // indirect
|
|
||||||
github.com/ldez/gomoddirectives v0.2.1 // indirect
|
|
||||||
github.com/ldez/tagliatelle v0.2.0 // indirect
|
|
||||||
github.com/magiconair/properties v1.8.1 // indirect
|
|
||||||
github.com/maratori/testpackage v1.0.1 // indirect
|
|
||||||
github.com/matoous/godox v0.0.0-20210227103229-6504466cf951 // indirect
|
|
||||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
|
||||||
github.com/mattn/goveralls v0.0.9 // indirect
|
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
|
||||||
github.com/mbilski/exhaustivestruct v1.2.0 // indirect
|
|
||||||
github.com/mgechev/dots v0.0.0-20190921121421-c36f7dcfbb81 // indirect
|
|
||||||
github.com/mgechev/revive v1.0.7 // indirect
|
|
||||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
|
||||||
github.com/mitchellh/mapstructure v1.1.2 // indirect
|
|
||||||
github.com/moricho/tparallel v0.2.1 // indirect
|
|
||||||
github.com/nakabonne/nestif v0.3.0 // indirect
|
|
||||||
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect
|
|
||||||
github.com/nishanths/exhaustive v0.1.0 // indirect
|
|
||||||
github.com/nishanths/predeclared v0.2.1 // indirect
|
|
||||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
|
||||||
github.com/pelletier/go-toml v1.2.0 // indirect
|
|
||||||
github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d // indirect
|
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/polyfloyd/go-errorlint v0.0.0-20210510181950-ab96adb96fea // indirect
|
|
||||||
github.com/powerman/deepequal v0.1.0 // indirect
|
github.com/powerman/deepequal v0.1.0 // indirect
|
||||||
github.com/prometheus/client_golang v1.7.1 // indirect
|
github.com/quic-go/qpack v0.4.0 // indirect
|
||||||
github.com/prometheus/client_model v0.2.0 // indirect
|
github.com/smartystreets/goconvey v1.7.2 // indirect
|
||||||
github.com/prometheus/common v0.10.0 // indirect
|
go.uber.org/mock v0.4.0 // indirect
|
||||||
github.com/prometheus/procfs v0.1.3 // indirect
|
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
|
||||||
github.com/quasilyte/go-ruleguard v0.3.4 // indirect
|
golang.org/x/mod v0.17.0 // indirect
|
||||||
github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95 // indirect
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
github.com/ryancurrah/gomodguard v1.2.2 // indirect
|
golang.org/x/text v0.15.0 // indirect
|
||||||
github.com/ryanrolds/sqlclosecheck v0.3.0 // indirect
|
golang.org/x/tools v0.21.0 // indirect
|
||||||
github.com/sanposhiho/wastedassign/v2 v2.0.6 // indirect
|
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
|
||||||
github.com/securego/gosec/v2 v2.8.0 // indirect
|
google.golang.org/grpc v1.53.0 // indirect
|
||||||
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
|
||||||
github.com/smartystreets/goconvey v1.6.4 // indirect
|
|
||||||
github.com/sonatard/noctx v0.0.1 // indirect
|
|
||||||
github.com/sourcegraph/go-diff v0.6.1 // indirect
|
|
||||||
github.com/spf13/afero v1.1.2 // indirect
|
|
||||||
github.com/spf13/cast v1.3.0 // indirect
|
|
||||||
github.com/spf13/cobra v1.1.3 // indirect
|
|
||||||
github.com/spf13/jwalterweatherman v1.0.0 // indirect
|
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
|
||||||
github.com/spf13/viper v1.7.1 // indirect
|
|
||||||
github.com/ssgreg/nlreturn/v2 v2.1.0 // indirect
|
|
||||||
github.com/stretchr/objx v0.1.1 // indirect
|
|
||||||
github.com/stretchr/testify v1.7.0 // indirect
|
|
||||||
github.com/subosito/gotenv v1.2.0 // indirect
|
|
||||||
github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b // indirect
|
|
||||||
github.com/tetafro/godot v1.4.7 // indirect
|
|
||||||
github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94 // indirect
|
|
||||||
github.com/tomarrell/wrapcheck/v2 v2.1.0 // indirect
|
|
||||||
github.com/tommy-muehle/go-mnd/v2 v2.4.0 // indirect
|
|
||||||
github.com/ultraware/funlen v0.0.3 // indirect
|
|
||||||
github.com/ultraware/whitespace v0.0.4 // indirect
|
|
||||||
github.com/uudashr/gocognit v1.0.1 // indirect
|
|
||||||
github.com/yeya24/promlinter v0.1.0 // indirect
|
|
||||||
golang.org/x/mod v0.4.2 // indirect
|
|
||||||
golang.org/x/text v0.3.6 // indirect
|
|
||||||
golang.org/x/tools v0.1.3 // indirect
|
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
|
||||||
google.golang.org/genproto v0.0.0-20200707001353-8e8330bf89df // indirect
|
|
||||||
google.golang.org/grpc v1.38.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.27.0 // indirect
|
|
||||||
gopkg.in/ini.v1 v1.51.0 // indirect
|
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
|
||||||
honnef.co/go/tools v0.2.0 // indirect
|
|
||||||
mvdan.cc/gofumpt v0.1.1 // indirect
|
|
||||||
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect
|
|
||||||
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect
|
|
||||||
mvdan.cc/unparam v0.0.0-20210104141923-aac4ce9116a7 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -53,8 +53,8 @@ https://pgl.yoyo.org/adservers/serverlist.php?hostformat=nohtml
|
||||||
# Basic tracking list by Disconnect
|
# Basic tracking list by Disconnect
|
||||||
# https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt
|
# https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt
|
||||||
|
|
||||||
# KAD host file (fraud/adware) without controversies
|
# KAD host file (fraud/adware)
|
||||||
# https://raw.githubusercontent.com/PolishFiltersTeam/KADhosts/master/KADhosts_without_controversies.txt
|
# https://raw.githubusercontent.com/PolishFiltersTeam/KADhosts/master/KADomains.txt
|
||||||
|
|
||||||
# BarbBlock list (spurious and invalid DMCA takedowns)
|
# BarbBlock list (spurious and invalid DMCA takedowns)
|
||||||
https://paulgb.github.io/BarbBlock/blacklists/domain-list.txt
|
https://paulgb.github.io/BarbBlock/blacklists/domain-list.txt
|
||||||
|
@ -92,15 +92,15 @@ https://hostfiles.frogeye.fr/firstparty-trackers.txt
|
||||||
# Steven Black hosts file
|
# Steven Black hosts file
|
||||||
# https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
|
# https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
|
||||||
|
|
||||||
|
# A list of adserving and tracking sites maintained by @lightswitch05
|
||||||
|
https://www.github.developerdan.com/hosts/lists/ads-and-tracking-extended.txt
|
||||||
|
|
||||||
# A list of adserving and tracking sites maintained by @anudeepND
|
# A list of adserving and tracking sites maintained by @anudeepND
|
||||||
# https://raw.githubusercontent.com/anudeepND/blacklist/master/adservers.txt
|
https://raw.githubusercontent.com/anudeepND/blacklist/master/adservers.txt
|
||||||
|
|
||||||
# Anudeep's Blacklist (CoinMiner) - Blocks cryptojacking sites
|
# Anudeep's Blacklist (CoinMiner) - Blocks cryptojacking sites
|
||||||
# https://raw.githubusercontent.com/anudeepND/blacklist/master/CoinMiner.txt
|
# https://raw.githubusercontent.com/anudeepND/blacklist/master/CoinMiner.txt
|
||||||
|
|
||||||
# Block Spotify ads
|
|
||||||
# https://gitlab.com/CHEF-KOCH/cks-filterlist/-/raw/master/Anti-Corp/Spotify/Spotify-HOSTS.txt
|
|
||||||
|
|
||||||
### Spark < Blu Go < Blu < Basic < Ultimate
|
### Spark < Blu Go < Blu < Basic < Ultimate
|
||||||
### (With pornware blocking) Porn < Unified
|
### (With pornware blocking) Porn < Unified
|
||||||
# Energized Ultimate
|
# Energized Ultimate
|
||||||
|
@ -113,10 +113,13 @@ https://hostfiles.frogeye.fr/firstparty-trackers.txt
|
||||||
# https://block.energized.pro/blu/formats/domains.txt
|
# https://block.energized.pro/blu/formats/domains.txt
|
||||||
|
|
||||||
# OISD.NL - Blocks ads, phishing, malware, tracking and more. WARNING: this is a huge list.
|
# OISD.NL - Blocks ads, phishing, malware, tracking and more. WARNING: this is a huge list.
|
||||||
# https://dbl.oisd.nl/
|
# https://dblw.oisd.nl/
|
||||||
|
|
||||||
# OISD.NL (smaller subset) - Blocks ads, phishing, malware, tracking and more. Tries to miminize false positives.
|
# OISD.NL (smaller subset) - Blocks ads, phishing, malware, tracking and more. Tries to miminize false positives.
|
||||||
https://hosts.oisd.nl/basic/
|
https://dblw.oisd.nl/basic/
|
||||||
|
|
||||||
|
# OISD.NL (extra) - Blocks ads, phishing, malware, tracking and more. Protection over functionality.
|
||||||
|
# https://dblw.oisd.nl/extra/
|
||||||
|
|
||||||
# Captain Miao ad list - Block ads and trackers, especially Chinese and Android trackers
|
# Captain Miao ad list - Block ads and trackers, especially Chinese and Android trackers
|
||||||
# https://raw.githubusercontent.com/jdlingyu/ad-wars/master/sha_ad_hosts
|
# https://raw.githubusercontent.com/jdlingyu/ad-wars/master/sha_ad_hosts
|
||||||
|
@ -131,6 +134,7 @@ https://hosts.oisd.nl/basic/
|
||||||
# https://raw.githubusercontent.com/olbat/ut1-blacklists/master/blacklists/adult/domains
|
# https://raw.githubusercontent.com/olbat/ut1-blacklists/master/blacklists/adult/domains
|
||||||
# https://block.energized.pro/porn/formats/domains.txt
|
# https://block.energized.pro/porn/formats/domains.txt
|
||||||
# https://raw.githubusercontent.com/mhxion/pornaway/master/hosts/porn_sites.txt
|
# https://raw.githubusercontent.com/mhxion/pornaway/master/hosts/porn_sites.txt
|
||||||
|
# https://dblw.oisd.nl/nsfw/
|
||||||
|
|
||||||
# Block gambling sites
|
# Block gambling sites
|
||||||
# https://raw.githubusercontent.com/Sinfonietta/hostfiles/master/gambling-hosts
|
# https://raw.githubusercontent.com/Sinfonietta/hostfiles/master/gambling-hosts
|
||||||
|
@ -138,11 +142,13 @@ https://hosts.oisd.nl/basic/
|
||||||
|
|
||||||
# Block dating websites
|
# Block dating websites
|
||||||
# https://raw.githubusercontent.com/olbat/ut1-blacklists/master/blacklists/dating/domains
|
# https://raw.githubusercontent.com/olbat/ut1-blacklists/master/blacklists/dating/domains
|
||||||
|
# https://www.github.developerdan.com/hosts/lists/dating-services-extended.txt
|
||||||
|
|
||||||
# Block social media sites
|
# Block social media sites
|
||||||
# https://raw.githubusercontent.com/Sinfonietta/hostfiles/master/social-hosts
|
# https://raw.githubusercontent.com/Sinfonietta/hostfiles/master/social-hosts
|
||||||
# https://block.energized.pro/extensions/social/formats/domains.txt
|
# https://block.energized.pro/extensions/social/formats/domains.txt
|
||||||
# https://raw.githubusercontent.com/olbat/ut1-blacklists/master/blacklists/social_networks/domains
|
# https://raw.githubusercontent.com/olbat/ut1-blacklists/master/blacklists/social_networks/domains
|
||||||
|
# https://www.github.developerdan.com/hosts/lists/facebook-extended.txt
|
||||||
|
|
||||||
# Goodbye Ads - Specially designed for mobile ad protection
|
# Goodbye Ads - Specially designed for mobile ad protection
|
||||||
# https://raw.githubusercontent.com/jerryn70/GoodbyeAds/master/Hosts/GoodbyeAds.txt
|
# https://raw.githubusercontent.com/jerryn70/GoodbyeAds/master/Hosts/GoodbyeAds.txt
|
||||||
|
@ -152,7 +158,3 @@ https://hosts.oisd.nl/basic/
|
||||||
|
|
||||||
# Block spying and tracking on Windows
|
# Block spying and tracking on Windows
|
||||||
# https://raw.githubusercontent.com/crazy-max/WindowsSpyBlocker/master/data/dnscrypt/spy.txt
|
# https://raw.githubusercontent.com/crazy-max/WindowsSpyBlocker/master/data/dnscrypt/spy.txt
|
||||||
|
|
||||||
# GameIndustry.eu - Block spyware, advertising, analytics, tracking in games and associated clients
|
|
||||||
# https://www.gameindustry.eu/files/hosts.txt
|
|
||||||
|
|
||||||
|
|
|
@ -65,6 +65,7 @@ def parse_list(content, trusted=False):
|
||||||
r"^@*\|\|([a-z0-9][a-z0-9.-]*[.][a-z]{2,})\^?(\$(popup|third-party))?$"
|
r"^@*\|\|([a-z0-9][a-z0-9.-]*[.][a-z]{2,})\^?(\$(popup|third-party))?$"
|
||||||
)
|
)
|
||||||
rx_l = re.compile(r"^([a-z0-9][a-z0-9.-]*[.][a-z]{2,})$")
|
rx_l = re.compile(r"^([a-z0-9][a-z0-9.-]*[.][a-z]{2,})$")
|
||||||
|
rx_lw = re.compile(r"^[*][.]([a-z0-9][a-z0-9.-]*[.][a-z]{2,})$")
|
||||||
rx_h = re.compile(
|
rx_h = re.compile(
|
||||||
r"^[0-9]{1,3}[.][0-9]{1,3}[.][0-9]{1,3}[.][0-9]{1,3}\s+([a-z0-9][a-z0-9.-]*[.][a-z]{2,})$"
|
r"^[0-9]{1,3}[.][0-9]{1,3}[.][0-9]{1,3}[.][0-9]{1,3}\s+([a-z0-9][a-z0-9.-]*[.][a-z]{2,})$"
|
||||||
)
|
)
|
||||||
|
@ -75,7 +76,7 @@ def parse_list(content, trusted=False):
|
||||||
names = set()
|
names = set()
|
||||||
time_restrictions = {}
|
time_restrictions = {}
|
||||||
globs = set()
|
globs = set()
|
||||||
rx_set = [rx_u, rx_l, rx_h, rx_mdl, rx_b, rx_dq]
|
rx_set = [rx_u, rx_l, rx_lw, rx_h, rx_mdl, rx_b, rx_dq]
|
||||||
for line in content.splitlines():
|
for line in content.splitlines():
|
||||||
line = str.lower(str.strip(line))
|
line = str.lower(str.strip(line))
|
||||||
if rx_comment.match(line):
|
if rx_comment.match(line):
|
||||||
|
@ -92,7 +93,8 @@ def parse_list(content, trusted=False):
|
||||||
|
|
||||||
def print_restricted_name(output_fd, name, time_restrictions):
|
def print_restricted_name(output_fd, name, time_restrictions):
|
||||||
if name in time_restrictions:
|
if name in time_restrictions:
|
||||||
print("{}\t{}".format(name, time_restrictions[name]), file=output_fd, end="\n")
|
print("{}\t{}".format(
|
||||||
|
name, time_restrictions[name]), file=output_fd, end="\n")
|
||||||
else:
|
else:
|
||||||
print(
|
print(
|
||||||
"# ignored: [{}] was in the time-restricted list, "
|
"# ignored: [{}] was in the time-restricted list, "
|
||||||
|
@ -120,7 +122,8 @@ def load_from_url(url):
|
||||||
except urllib.URLError as err:
|
except urllib.URLError as err:
|
||||||
raise Exception("[{}] could not be loaded: {}\n".format(url, err))
|
raise Exception("[{}] could not be loaded: {}\n".format(url, err))
|
||||||
if trusted is False and response.getcode() != 200:
|
if trusted is False and response.getcode() != 200:
|
||||||
raise Exception("[{}] returned HTTP code {}\n".format(url, response.getcode()))
|
raise Exception("[{}] returned HTTP code {}\n".format(
|
||||||
|
url, response.getcode()))
|
||||||
content = response.read()
|
content = response.read()
|
||||||
if URLLIB_NEW:
|
if URLLIB_NEW:
|
||||||
content = content.decode("utf-8", errors="replace")
|
content = content.decode("utf-8", errors="replace")
|
||||||
|
@ -262,10 +265,12 @@ def blocklists_from_config_file(
|
||||||
|
|
||||||
list_names.sort(key=name_cmp)
|
list_names.sort(key=name_cmp)
|
||||||
if ignored:
|
if ignored:
|
||||||
print("# Ignored duplicates: {}".format(ignored), file=output_fd, end="\n")
|
print("# Ignored duplicates: {}".format(
|
||||||
|
ignored), file=output_fd, end="\n")
|
||||||
if glob_ignored:
|
if glob_ignored:
|
||||||
print(
|
print(
|
||||||
"# Ignored due to overlapping local patterns: {}".format(glob_ignored),
|
"# Ignored due to overlapping local patterns: {}".format(
|
||||||
|
glob_ignored),
|
||||||
file=output_fd,
|
file=output_fd,
|
||||||
end="\n",
|
end="\n",
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2018 Leigh McCulloch
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
|
@ -1,154 +0,0 @@
|
||||||
package checknoglobals
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"go/ast"
|
|
||||||
"go/token"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/tools/go/analysis"
|
|
||||||
)
|
|
||||||
|
|
||||||
// allowedExpression is a struct representing packages and methods that will
|
|
||||||
// be an allowed combination to use as a global variable, f.ex. Name `regexp`
|
|
||||||
// and SelName `MustCompile`.
|
|
||||||
type allowedExpression struct {
|
|
||||||
Name string
|
|
||||||
SelName string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Doc = `check that no global variables exist
|
|
||||||
|
|
||||||
This analyzer checks for global variables and errors on any found.
|
|
||||||
|
|
||||||
A global variable is a variable declared in package scope and that can be read
|
|
||||||
and written to by any function within the package. Global variables can cause
|
|
||||||
side effects which are difficult to keep track of. A code in one function may
|
|
||||||
change the variables state while another unrelated chunk of code may be
|
|
||||||
effected by it.`
|
|
||||||
|
|
||||||
// Analyzer provides an Analyzer that checks that there are no global
|
|
||||||
// variables, except for errors and variables containing regular
|
|
||||||
// expressions.
|
|
||||||
func Analyzer() *analysis.Analyzer {
|
|
||||||
return &analysis.Analyzer{
|
|
||||||
Name: "gochecknoglobals",
|
|
||||||
Doc: Doc,
|
|
||||||
Run: checkNoGlobals,
|
|
||||||
Flags: flags(),
|
|
||||||
RunDespiteErrors: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func flags() flag.FlagSet {
|
|
||||||
flags := flag.NewFlagSet("", flag.ExitOnError)
|
|
||||||
flags.Bool("t", false, "Include tests")
|
|
||||||
|
|
||||||
return *flags
|
|
||||||
}
|
|
||||||
|
|
||||||
func isAllowed(v ast.Node) bool {
|
|
||||||
switch i := v.(type) {
|
|
||||||
case *ast.Ident:
|
|
||||||
return i.Name == "_" || i.Name == "version" || looksLikeError(i)
|
|
||||||
case *ast.CallExpr:
|
|
||||||
if expr, ok := i.Fun.(*ast.SelectorExpr); ok {
|
|
||||||
return isAllowedSelectorExpression(expr)
|
|
||||||
}
|
|
||||||
case *ast.CompositeLit:
|
|
||||||
if expr, ok := i.Type.(*ast.SelectorExpr); ok {
|
|
||||||
return isAllowedSelectorExpression(expr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isAllowedSelectorExpression(v *ast.SelectorExpr) bool {
|
|
||||||
x, ok := v.X.(*ast.Ident)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
allowList := []allowedExpression{
|
|
||||||
{Name: "regexp", SelName: "MustCompile"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, i := range allowList {
|
|
||||||
if x.Name == i.Name && v.Sel.Name == i.SelName {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// looksLikeError returns true if the AST identifier starts
|
|
||||||
// with 'err' or 'Err', or false otherwise.
|
|
||||||
//
|
|
||||||
// TODO: https://github.com/leighmcculloch/gochecknoglobals/issues/5
|
|
||||||
func looksLikeError(i *ast.Ident) bool {
|
|
||||||
prefix := "err"
|
|
||||||
if i.IsExported() {
|
|
||||||
prefix = "Err"
|
|
||||||
}
|
|
||||||
return strings.HasPrefix(i.Name, prefix)
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkNoGlobals(pass *analysis.Pass) (interface{}, error) {
|
|
||||||
includeTests := pass.Analyzer.Flags.Lookup("t").Value.(flag.Getter).Get().(bool)
|
|
||||||
|
|
||||||
for _, file := range pass.Files {
|
|
||||||
filename := pass.Fset.Position(file.Pos()).Filename
|
|
||||||
if !strings.HasSuffix(filename, ".go") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !includeTests && strings.HasSuffix(filename, "_test.go") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, decl := range file.Decls {
|
|
||||||
genDecl, ok := decl.(*ast.GenDecl)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if genDecl.Tok != token.VAR {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, spec := range genDecl.Specs {
|
|
||||||
valueSpec := spec.(*ast.ValueSpec)
|
|
||||||
onlyAllowedValues := false
|
|
||||||
|
|
||||||
for _, vn := range valueSpec.Values {
|
|
||||||
if isAllowed(vn) {
|
|
||||||
onlyAllowedValues = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
onlyAllowedValues = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if onlyAllowedValues {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, vn := range valueSpec.Names {
|
|
||||||
if isAllowed(vn) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
message := fmt.Sprintf("%s is a global variable", vn.Name)
|
|
||||||
pass.Report(analysis.Diagnostic{
|
|
||||||
Pos: vn.Pos(),
|
|
||||||
Category: "global",
|
|
||||||
Message: message,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
|
@ -1,2 +1,2 @@
|
||||||
toml.test
|
/toml.test
|
||||||
/toml-test
|
/toml-test
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Compatible with TOML version [v1.0.0](https://toml.io/en/v1.0.0).
|
|
|
@ -1,10 +1,5 @@
|
||||||
## TOML parser and encoder for Go with reflection
|
|
||||||
|
|
||||||
TOML stands for Tom's Obvious, Minimal Language. This Go package provides a
|
TOML stands for Tom's Obvious, Minimal Language. This Go package provides a
|
||||||
reflection interface similar to Go's standard library `json` and `xml`
|
reflection interface similar to Go's standard library `json` and `xml` packages.
|
||||||
packages. This package also supports the `encoding.TextUnmarshaler` and
|
|
||||||
`encoding.TextMarshaler` interfaces so that you can define custom data
|
|
||||||
representations. (There is an example of this below.)
|
|
||||||
|
|
||||||
Compatible with TOML version [v1.0.0](https://toml.io/en/v1.0.0).
|
Compatible with TOML version [v1.0.0](https://toml.io/en/v1.0.0).
|
||||||
|
|
||||||
|
@ -14,28 +9,18 @@ See the [releases page](https://github.com/BurntSushi/toml/releases) for a
|
||||||
changelog; this information is also in the git tag annotations (e.g. `git show
|
changelog; this information is also in the git tag annotations (e.g. `git show
|
||||||
v0.4.0`).
|
v0.4.0`).
|
||||||
|
|
||||||
This library requires Go 1.13 or newer; install it with:
|
This library requires Go 1.18 or newer; add it to your go.mod with:
|
||||||
|
|
||||||
$ go get github.com/BurntSushi/toml
|
% go get github.com/BurntSushi/toml@latest
|
||||||
|
|
||||||
It also comes with a TOML validator CLI tool:
|
It also comes with a TOML validator CLI tool:
|
||||||
|
|
||||||
$ go get github.com/BurntSushi/toml/cmd/tomlv
|
% go install github.com/BurntSushi/toml/cmd/tomlv@latest
|
||||||
$ tomlv some-toml-file.toml
|
% tomlv some-toml-file.toml
|
||||||
|
|
||||||
### Testing
|
|
||||||
|
|
||||||
This package passes all tests in
|
|
||||||
[toml-test](https://github.com/BurntSushi/toml-test) for both the decoder
|
|
||||||
and the encoder.
|
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
For the simplest example, consider some TOML file as just a list of keys and
|
||||||
This package works similarly to how the Go standard library handles XML and
|
values:
|
||||||
JSON. Namely, data is loaded into Go values via reflection.
|
|
||||||
|
|
||||||
For the simplest example, consider some TOML file as just a list of keys
|
|
||||||
and values:
|
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
Age = 25
|
Age = 25
|
||||||
|
@ -45,7 +30,7 @@ Perfection = [ 6, 28, 496, 8128 ]
|
||||||
DOB = 1987-07-05T05:45:00Z
|
DOB = 1987-07-05T05:45:00Z
|
||||||
```
|
```
|
||||||
|
|
||||||
Which could be defined in Go as:
|
Which can be decoded with:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -53,21 +38,15 @@ type Config struct {
|
||||||
Cats []string
|
Cats []string
|
||||||
Pi float64
|
Pi float64
|
||||||
Perfection []int
|
Perfection []int
|
||||||
DOB time.Time // requires `import time`
|
DOB time.Time
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
And then decoded with:
|
|
||||||
|
|
||||||
```go
|
|
||||||
var conf Config
|
var conf Config
|
||||||
if _, err := toml.Decode(tomlData, &conf); err != nil {
|
_, err := toml.Decode(tomlData, &conf)
|
||||||
// handle error
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also use struct tags if your struct field name doesn't map to a TOML
|
You can also use struct tags if your struct field name doesn't map to a TOML key
|
||||||
key value directly:
|
value directly:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
some_key_NAME = "wat"
|
some_key_NAME = "wat"
|
||||||
|
@ -75,146 +54,67 @@ some_key_NAME = "wat"
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type TOML struct {
|
type TOML struct {
|
||||||
ObscureKey string `toml:"some_key_NAME"`
|
ObscureKey string `toml:"some_key_NAME"`
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Beware that like other most other decoders **only exported fields** are
|
Beware that like other decoders **only exported fields** are considered when
|
||||||
considered when encoding and decoding; private fields are silently ignored.
|
encoding and decoding; private fields are silently ignored.
|
||||||
|
|
||||||
### Using the `encoding.TextUnmarshaler` interface
|
### Using the `Marshaler` and `encoding.TextUnmarshaler` interfaces
|
||||||
|
Here's an example that automatically parses values in a `mail.Address`:
|
||||||
Here's an example that automatically parses duration strings into
|
|
||||||
`time.Duration` values:
|
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[[song]]
|
contacts = [
|
||||||
name = "Thunder Road"
|
"Donald Duck <donald@duckburg.com>",
|
||||||
duration = "4m49s"
|
"Scrooge McDuck <scrooge@duckburg.com>",
|
||||||
|
]
|
||||||
[[song]]
|
|
||||||
name = "Stairway to Heaven"
|
|
||||||
duration = "8m03s"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Which can be decoded with:
|
Can be decoded with:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
type song struct {
|
// Create address type which satisfies the encoding.TextUnmarshaler interface.
|
||||||
Name string
|
type address struct {
|
||||||
Duration duration
|
*mail.Address
|
||||||
}
|
|
||||||
type songs struct {
|
|
||||||
Song []song
|
|
||||||
}
|
|
||||||
var favorites songs
|
|
||||||
if _, err := toml.Decode(blob, &favorites); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, s := range favorites.Song {
|
func (a *address) UnmarshalText(text []byte) error {
|
||||||
fmt.Printf("%s (%s)\n", s.Name, s.Duration)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And you'll also need a `duration` type that satisfies the
|
|
||||||
`encoding.TextUnmarshaler` interface:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type duration struct {
|
|
||||||
time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *duration) UnmarshalText(text []byte) error {
|
|
||||||
var err error
|
var err error
|
||||||
d.Duration, err = time.ParseDuration(string(text))
|
a.Address, err = mail.ParseAddress(string(text))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decode it.
|
||||||
|
func decode() {
|
||||||
|
blob := `
|
||||||
|
contacts = [
|
||||||
|
"Donald Duck <donald@duckburg.com>",
|
||||||
|
"Scrooge McDuck <scrooge@duckburg.com>",
|
||||||
|
]
|
||||||
|
`
|
||||||
|
|
||||||
|
var contacts struct {
|
||||||
|
Contacts []address
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := toml.Decode(blob, &contacts)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range contacts.Contacts {
|
||||||
|
fmt.Printf("%#v\n", c.Address)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// &mail.Address{Name:"Donald Duck", Address:"donald@duckburg.com"}
|
||||||
|
// &mail.Address{Name:"Scrooge McDuck", Address:"scrooge@duckburg.com"}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
To target TOML specifically you can implement `UnmarshalTOML` TOML interface in
|
To target TOML specifically you can implement `UnmarshalTOML` TOML interface in
|
||||||
a similar way.
|
a similar way.
|
||||||
|
|
||||||
### More complex usage
|
### More complex usage
|
||||||
|
See the [`_example/`](/_example) directory for a more complex example.
|
||||||
Here's an example of how to load the example from the official spec page:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# This is a TOML document. Boom.
|
|
||||||
|
|
||||||
title = "TOML Example"
|
|
||||||
|
|
||||||
[owner]
|
|
||||||
name = "Tom Preston-Werner"
|
|
||||||
organization = "GitHub"
|
|
||||||
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
|
|
||||||
dob = 1979-05-27T07:32:00Z # First class dates? Why not?
|
|
||||||
|
|
||||||
[database]
|
|
||||||
server = "192.168.1.1"
|
|
||||||
ports = [ 8001, 8001, 8002 ]
|
|
||||||
connection_max = 5000
|
|
||||||
enabled = true
|
|
||||||
|
|
||||||
[servers]
|
|
||||||
|
|
||||||
# You can indent as you please. Tabs or spaces. TOML don't care.
|
|
||||||
[servers.alpha]
|
|
||||||
ip = "10.0.0.1"
|
|
||||||
dc = "eqdc10"
|
|
||||||
|
|
||||||
[servers.beta]
|
|
||||||
ip = "10.0.0.2"
|
|
||||||
dc = "eqdc10"
|
|
||||||
|
|
||||||
[clients]
|
|
||||||
data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
|
|
||||||
|
|
||||||
# Line breaks are OK when inside arrays
|
|
||||||
hosts = [
|
|
||||||
"alpha",
|
|
||||||
"omega"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
And the corresponding Go types are:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type tomlConfig struct {
|
|
||||||
Title string
|
|
||||||
Owner ownerInfo
|
|
||||||
DB database `toml:"database"`
|
|
||||||
Servers map[string]server
|
|
||||||
Clients clients
|
|
||||||
}
|
|
||||||
|
|
||||||
type ownerInfo struct {
|
|
||||||
Name string
|
|
||||||
Org string `toml:"organization"`
|
|
||||||
Bio string
|
|
||||||
DOB time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type database struct {
|
|
||||||
Server string
|
|
||||||
Ports []int
|
|
||||||
ConnMax int `toml:"connection_max"`
|
|
||||||
Enabled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type server struct {
|
|
||||||
IP string
|
|
||||||
DC string
|
|
||||||
}
|
|
||||||
|
|
||||||
type clients struct {
|
|
||||||
Data [][]interface{}
|
|
||||||
Hosts []string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that a case insensitive match will be tried if an exact match can't be
|
|
||||||
found.
|
|
||||||
|
|
||||||
A working example of the above can be found in `_examples/example.{go,toml}`.
|
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
package toml
|
package toml
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding"
|
"encoding"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/fs"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -15,19 +18,49 @@ import (
|
||||||
// Unmarshaler is the interface implemented by objects that can unmarshal a
|
// Unmarshaler is the interface implemented by objects that can unmarshal a
|
||||||
// TOML description of themselves.
|
// TOML description of themselves.
|
||||||
type Unmarshaler interface {
|
type Unmarshaler interface {
|
||||||
UnmarshalTOML(interface{}) error
|
UnmarshalTOML(any) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal decodes the contents of `p` in TOML format into a pointer `v`.
|
// Unmarshal decodes the contents of data in TOML format into a pointer v.
|
||||||
func Unmarshal(p []byte, v interface{}) error {
|
//
|
||||||
_, err := Decode(string(p), v)
|
// See [Decoder] for a description of the decoding process.
|
||||||
|
func Unmarshal(data []byte, v any) error {
|
||||||
|
_, err := NewDecoder(bytes.NewReader(data)).Decode(v)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decode the TOML data in to the pointer v.
|
||||||
|
//
|
||||||
|
// See [Decoder] for a description of the decoding process.
|
||||||
|
func Decode(data string, v any) (MetaData, error) {
|
||||||
|
return NewDecoder(strings.NewReader(data)).Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeFile reads the contents of a file and decodes it with [Decode].
|
||||||
|
func DecodeFile(path string, v any) (MetaData, error) {
|
||||||
|
fp, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return MetaData{}, err
|
||||||
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
return NewDecoder(fp).Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeFS reads the contents of a file from [fs.FS] and decodes it with
|
||||||
|
// [Decode].
|
||||||
|
func DecodeFS(fsys fs.FS, path string, v any) (MetaData, error) {
|
||||||
|
fp, err := fsys.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return MetaData{}, err
|
||||||
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
return NewDecoder(fp).Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
// Primitive is a TOML value that hasn't been decoded into a Go value.
|
// Primitive is a TOML value that hasn't been decoded into a Go value.
|
||||||
//
|
//
|
||||||
// This type can be used for any value, which will cause decoding to be delayed.
|
// This type can be used for any value, which will cause decoding to be delayed.
|
||||||
// You can use the PrimitiveDecode() function to "manually" decode these values.
|
// You can use [PrimitiveDecode] to "manually" decode these values.
|
||||||
//
|
//
|
||||||
// NOTE: The underlying representation of a `Primitive` value is subject to
|
// NOTE: The underlying representation of a `Primitive` value is subject to
|
||||||
// change. Do not rely on it.
|
// change. Do not rely on it.
|
||||||
|
@ -36,36 +69,29 @@ func Unmarshal(p []byte, v interface{}) error {
|
||||||
// overhead of reflection. They can be useful when you don't know the exact type
|
// overhead of reflection. They can be useful when you don't know the exact type
|
||||||
// of TOML data until runtime.
|
// of TOML data until runtime.
|
||||||
type Primitive struct {
|
type Primitive struct {
|
||||||
undecoded interface{}
|
undecoded any
|
||||||
context Key
|
context Key
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrimitiveDecode is just like the other `Decode*` functions, except it
|
// The significand precision for float32 and float64 is 24 and 53 bits; this is
|
||||||
// decodes a TOML value that has already been parsed. Valid primitive values
|
// the range a natural number can be stored in a float without loss of data.
|
||||||
// can *only* be obtained from values filled by the decoder functions,
|
const (
|
||||||
// including this method. (i.e., `v` may contain more `Primitive`
|
maxSafeFloat32Int = 16777215 // 2^24-1
|
||||||
// values.)
|
maxSafeFloat64Int = int64(9007199254740991) // 2^53-1
|
||||||
//
|
)
|
||||||
// Meta data for primitive values is included in the meta data returned by
|
|
||||||
// the `Decode*` functions with one exception: keys returned by the Undecoded
|
|
||||||
// method will only reflect keys that were decoded. Namely, any keys hidden
|
|
||||||
// behind a Primitive will be considered undecoded. Executing this method will
|
|
||||||
// update the undecoded keys in the meta data. (See the example.)
|
|
||||||
func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {
|
|
||||||
md.context = primValue.context
|
|
||||||
defer func() { md.context = nil }()
|
|
||||||
return md.unify(primValue.undecoded, rvalue(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decoder decodes TOML data.
|
// Decoder decodes TOML data.
|
||||||
//
|
//
|
||||||
// TOML tables correspond to Go structs or maps (dealer's choice – they can be
|
// TOML tables correspond to Go structs or maps; they can be used
|
||||||
// used interchangeably).
|
// interchangeably, but structs offer better type safety.
|
||||||
//
|
//
|
||||||
// TOML table arrays correspond to either a slice of structs or a slice of maps.
|
// TOML table arrays correspond to either a slice of structs or a slice of maps.
|
||||||
//
|
//
|
||||||
// TOML datetimes correspond to Go time.Time values. Local datetimes are parsed
|
// TOML datetimes correspond to [time.Time]. Local datetimes are parsed in the
|
||||||
// in the local timezone.
|
// local timezone.
|
||||||
|
//
|
||||||
|
// [time.Duration] types are treated as nanoseconds if the TOML value is an
|
||||||
|
// integer, or they're parsed with time.ParseDuration() if they're strings.
|
||||||
//
|
//
|
||||||
// All other TOML types (float, string, int, bool and array) correspond to the
|
// All other TOML types (float, string, int, bool and array) correspond to the
|
||||||
// obvious Go types.
|
// obvious Go types.
|
||||||
|
@ -74,9 +100,9 @@ func (md *MetaData) PrimitiveDecode(primValue Primitive, v interface{}) error {
|
||||||
// interface, in which case any primitive TOML value (floats, strings, integers,
|
// interface, in which case any primitive TOML value (floats, strings, integers,
|
||||||
// booleans, datetimes) will be converted to a []byte and given to the value's
|
// booleans, datetimes) will be converted to a []byte and given to the value's
|
||||||
// UnmarshalText method. See the Unmarshaler example for a demonstration with
|
// UnmarshalText method. See the Unmarshaler example for a demonstration with
|
||||||
// time duration strings.
|
// email addresses.
|
||||||
//
|
//
|
||||||
// Key mapping
|
// # Key mapping
|
||||||
//
|
//
|
||||||
// TOML keys can map to either keys in a Go map or field names in a Go struct.
|
// TOML keys can map to either keys in a Go map or field names in a Go struct.
|
||||||
// The special `toml` struct tag can be used to map TOML keys to struct fields
|
// The special `toml` struct tag can be used to map TOML keys to struct fields
|
||||||
|
@ -100,19 +126,40 @@ func NewDecoder(r io.Reader) *Decoder {
|
||||||
return &Decoder{r: r}
|
return &Decoder{r: r}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
unmarshalToml = reflect.TypeOf((*Unmarshaler)(nil)).Elem()
|
||||||
|
unmarshalText = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()
|
||||||
|
primitiveType = reflect.TypeOf((*Primitive)(nil)).Elem()
|
||||||
|
)
|
||||||
|
|
||||||
// Decode TOML data in to the pointer `v`.
|
// Decode TOML data in to the pointer `v`.
|
||||||
func (dec *Decoder) Decode(v interface{}) (MetaData, error) {
|
func (dec *Decoder) Decode(v any) (MetaData, error) {
|
||||||
rv := reflect.ValueOf(v)
|
rv := reflect.ValueOf(v)
|
||||||
if rv.Kind() != reflect.Ptr {
|
if rv.Kind() != reflect.Ptr {
|
||||||
return MetaData{}, e("Decode of non-pointer %s", reflect.TypeOf(v))
|
s := "%q"
|
||||||
|
if reflect.TypeOf(v) == nil {
|
||||||
|
s = "%v"
|
||||||
|
}
|
||||||
|
|
||||||
|
return MetaData{}, fmt.Errorf("toml: cannot decode to non-pointer "+s, reflect.TypeOf(v))
|
||||||
}
|
}
|
||||||
if rv.IsNil() {
|
if rv.IsNil() {
|
||||||
return MetaData{}, e("Decode of nil %s", reflect.TypeOf(v))
|
return MetaData{}, fmt.Errorf("toml: cannot decode to nil value of %q", reflect.TypeOf(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: have parser should read from io.Reader? Or at the very least, make
|
// Check if this is a supported type: struct, map, any, or something that
|
||||||
// it read from []byte rather than string
|
// implements UnmarshalTOML or UnmarshalText.
|
||||||
data, err := ioutil.ReadAll(dec.r)
|
rv = indirect(rv)
|
||||||
|
rt := rv.Type()
|
||||||
|
if rv.Kind() != reflect.Struct && rv.Kind() != reflect.Map &&
|
||||||
|
!(rv.Kind() == reflect.Interface && rv.NumMethod() == 0) &&
|
||||||
|
!rt.Implements(unmarshalToml) && !rt.Implements(unmarshalText) {
|
||||||
|
return MetaData{}, fmt.Errorf("toml: cannot decode to type %s", rt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: parser should read from io.Reader? Or at the very least, make it
|
||||||
|
// read from []byte rather than string
|
||||||
|
data, err := io.ReadAll(dec.r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return MetaData{}, err
|
return MetaData{}, err
|
||||||
}
|
}
|
||||||
|
@ -121,29 +168,32 @@ func (dec *Decoder) Decode(v interface{}) (MetaData, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return MetaData{}, err
|
return MetaData{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
md := MetaData{
|
md := MetaData{
|
||||||
p.mapping, p.types, p.ordered,
|
mapping: p.mapping,
|
||||||
make(map[string]bool, len(p.ordered)), nil,
|
keyInfo: p.keyInfo,
|
||||||
|
keys: p.ordered,
|
||||||
|
decoded: make(map[string]struct{}, len(p.ordered)),
|
||||||
|
context: nil,
|
||||||
|
data: data,
|
||||||
}
|
}
|
||||||
return md, md.unify(p.mapping, indirect(rv))
|
return md, md.unify(p.mapping, rv)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode the TOML data in to the pointer v.
|
// PrimitiveDecode is just like the other Decode* functions, except it decodes a
|
||||||
|
// TOML value that has already been parsed. Valid primitive values can *only* be
|
||||||
|
// obtained from values filled by the decoder functions, including this method.
|
||||||
|
// (i.e., v may contain more [Primitive] values.)
|
||||||
//
|
//
|
||||||
// See the documentation on Decoder for a description of the decoding process.
|
// Meta data for primitive values is included in the meta data returned by the
|
||||||
func Decode(data string, v interface{}) (MetaData, error) {
|
// Decode* functions with one exception: keys returned by the Undecoded method
|
||||||
return NewDecoder(strings.NewReader(data)).Decode(v)
|
// will only reflect keys that were decoded. Namely, any keys hidden behind a
|
||||||
}
|
// Primitive will be considered undecoded. Executing this method will update the
|
||||||
|
// undecoded keys in the meta data. (See the example.)
|
||||||
// DecodeFile is just like Decode, except it will automatically read the
|
func (md *MetaData) PrimitiveDecode(primValue Primitive, v any) error {
|
||||||
// contents of the file at path and decode it for you.
|
md.context = primValue.context
|
||||||
func DecodeFile(path string, v interface{}) (MetaData, error) {
|
defer func() { md.context = nil }()
|
||||||
fp, err := os.Open(path)
|
return md.unify(primValue.undecoded, rvalue(v))
|
||||||
if err != nil {
|
|
||||||
return MetaData{}, err
|
|
||||||
}
|
|
||||||
defer fp.Close()
|
|
||||||
return NewDecoder(fp).Decode(v)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// unify performs a sort of type unification based on the structure of `rv`,
|
// unify performs a sort of type unification based on the structure of `rv`,
|
||||||
|
@ -151,10 +201,10 @@ func DecodeFile(path string, v interface{}) (MetaData, error) {
|
||||||
//
|
//
|
||||||
// Any type mismatch produces an error. Finding a type that we don't know
|
// Any type mismatch produces an error. Finding a type that we don't know
|
||||||
// how to handle produces an unsupported type error.
|
// how to handle produces an unsupported type error.
|
||||||
func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
|
func (md *MetaData) unify(data any, rv reflect.Value) error {
|
||||||
// Special case. Look for a `Primitive` value.
|
// Special case. Look for a `Primitive` value.
|
||||||
// TODO: #76 would make this superfluous after implemented.
|
// TODO: #76 would make this superfluous after implemented.
|
||||||
if rv.Type() == reflect.TypeOf((*Primitive)(nil)).Elem() {
|
if rv.Type() == primitiveType {
|
||||||
// Save the undecoded data and the key context into the primitive
|
// Save the undecoded data and the key context into the primitive
|
||||||
// value.
|
// value.
|
||||||
context := make(Key, len(md.context))
|
context := make(Key, len(md.context))
|
||||||
|
@ -166,17 +216,18 @@ func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case. Unmarshaler Interface support.
|
rvi := rv.Interface()
|
||||||
if rv.CanAddr() {
|
if v, ok := rvi.(Unmarshaler); ok {
|
||||||
if v, ok := rv.Addr().Interface().(Unmarshaler); ok {
|
err := v.UnmarshalTOML(data)
|
||||||
return v.UnmarshalTOML(data)
|
if err != nil {
|
||||||
|
return md.parseErr(err)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
if v, ok := rvi.(encoding.TextUnmarshaler); ok {
|
||||||
// Special case. Look for a value satisfying the TextUnmarshaler interface.
|
|
||||||
if v, ok := rv.Interface().(encoding.TextUnmarshaler); ok {
|
|
||||||
return md.unifyText(data, v)
|
return md.unifyText(data, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// The behavior here is incorrect whenever a Go type satisfies the
|
// The behavior here is incorrect whenever a Go type satisfies the
|
||||||
// encoding.TextUnmarshaler interface but also corresponds to a TOML hash or
|
// encoding.TextUnmarshaler interface but also corresponds to a TOML hash or
|
||||||
|
@ -187,19 +238,10 @@ func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
|
||||||
|
|
||||||
k := rv.Kind()
|
k := rv.Kind()
|
||||||
|
|
||||||
// laziness
|
|
||||||
if k >= reflect.Int && k <= reflect.Uint64 {
|
if k >= reflect.Int && k <= reflect.Uint64 {
|
||||||
return md.unifyInt(data, rv)
|
return md.unifyInt(data, rv)
|
||||||
}
|
}
|
||||||
switch k {
|
switch k {
|
||||||
case reflect.Ptr:
|
|
||||||
elem := reflect.New(rv.Type().Elem())
|
|
||||||
err := md.unify(data, reflect.Indirect(elem))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rv.Set(elem)
|
|
||||||
return nil
|
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
return md.unifyStruct(data, rv)
|
return md.unifyStruct(data, rv)
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
|
@ -213,27 +255,23 @@ func (md *MetaData) unify(data interface{}, rv reflect.Value) error {
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
return md.unifyBool(data, rv)
|
return md.unifyBool(data, rv)
|
||||||
case reflect.Interface:
|
case reflect.Interface:
|
||||||
// we only support empty interfaces.
|
if rv.NumMethod() > 0 { /// Only empty interfaces are supported.
|
||||||
if rv.NumMethod() > 0 {
|
return md.e("unsupported type %s", rv.Type())
|
||||||
return e("unsupported type %s", rv.Type())
|
|
||||||
}
|
}
|
||||||
return md.unifyAnything(data, rv)
|
return md.unifyAnything(data, rv)
|
||||||
case reflect.Float32:
|
case reflect.Float32, reflect.Float64:
|
||||||
fallthrough
|
|
||||||
case reflect.Float64:
|
|
||||||
return md.unifyFloat64(data, rv)
|
return md.unifyFloat64(data, rv)
|
||||||
}
|
}
|
||||||
return e("unsupported type %s", rv.Kind())
|
return md.e("unsupported type %s", rv.Kind())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
|
func (md *MetaData) unifyStruct(mapping any, rv reflect.Value) error {
|
||||||
tmap, ok := mapping.(map[string]interface{})
|
tmap, ok := mapping.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
if mapping == nil {
|
if mapping == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return e("type mismatch for %s: expected table but found %T",
|
return md.e("type mismatch for %s: expected table but found %s", rv.Type().String(), fmtType(mapping))
|
||||||
rv.Type().String(), mapping)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, datum := range tmap {
|
for key, datum := range tmap {
|
||||||
|
@ -254,78 +292,88 @@ func (md *MetaData) unifyStruct(mapping interface{}, rv reflect.Value) error {
|
||||||
for _, i := range f.index {
|
for _, i := range f.index {
|
||||||
subv = indirect(subv.Field(i))
|
subv = indirect(subv.Field(i))
|
||||||
}
|
}
|
||||||
|
|
||||||
if isUnifiable(subv) {
|
if isUnifiable(subv) {
|
||||||
md.decoded[md.context.add(key).String()] = true
|
md.decoded[md.context.add(key).String()] = struct{}{}
|
||||||
md.context = append(md.context, key)
|
md.context = append(md.context, key)
|
||||||
if err := md.unify(datum, subv); err != nil {
|
|
||||||
|
err := md.unify(datum, subv)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
md.context = md.context[0 : len(md.context)-1]
|
md.context = md.context[0 : len(md.context)-1]
|
||||||
} else if f.name != "" {
|
} else if f.name != "" {
|
||||||
// Bad user! No soup for you!
|
return md.e("cannot write unexported field %s.%s", rv.Type().String(), f.name)
|
||||||
return e("cannot write unexported field %s.%s",
|
|
||||||
rv.Type().String(), f.name)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MetaData) unifyMap(mapping interface{}, rv reflect.Value) error {
|
func (md *MetaData) unifyMap(mapping any, rv reflect.Value) error {
|
||||||
if k := rv.Type().Key().Kind(); k != reflect.String {
|
keyType := rv.Type().Key().Kind()
|
||||||
return fmt.Errorf(
|
if keyType != reflect.String && keyType != reflect.Interface {
|
||||||
"toml: cannot decode to a map with non-string key type (%s in %q)",
|
return fmt.Errorf("toml: cannot decode to a map with non-string key type (%s in %q)",
|
||||||
k, rv.Type())
|
keyType, rv.Type())
|
||||||
}
|
}
|
||||||
|
|
||||||
tmap, ok := mapping.(map[string]interface{})
|
tmap, ok := mapping.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
if tmap == nil {
|
if tmap == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return badtype("map", mapping)
|
return md.badtype("map", mapping)
|
||||||
}
|
}
|
||||||
if rv.IsNil() {
|
if rv.IsNil() {
|
||||||
rv.Set(reflect.MakeMap(rv.Type()))
|
rv.Set(reflect.MakeMap(rv.Type()))
|
||||||
}
|
}
|
||||||
for k, v := range tmap {
|
for k, v := range tmap {
|
||||||
md.decoded[md.context.add(k).String()] = true
|
md.decoded[md.context.add(k).String()] = struct{}{}
|
||||||
md.context = append(md.context, k)
|
md.context = append(md.context, k)
|
||||||
|
|
||||||
rvkey := indirect(reflect.New(rv.Type().Key()))
|
|
||||||
rvval := reflect.Indirect(reflect.New(rv.Type().Elem()))
|
rvval := reflect.Indirect(reflect.New(rv.Type().Elem()))
|
||||||
if err := md.unify(v, rvval); err != nil {
|
|
||||||
|
err := md.unify(v, indirect(rvval))
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
md.context = md.context[0 : len(md.context)-1]
|
md.context = md.context[0 : len(md.context)-1]
|
||||||
|
|
||||||
rvkey.SetString(k)
|
rvkey := indirect(reflect.New(rv.Type().Key()))
|
||||||
|
|
||||||
|
switch keyType {
|
||||||
|
case reflect.Interface:
|
||||||
|
rvkey.Set(reflect.ValueOf(k))
|
||||||
|
case reflect.String:
|
||||||
|
rvkey.SetString(k)
|
||||||
|
}
|
||||||
|
|
||||||
rv.SetMapIndex(rvkey, rvval)
|
rv.SetMapIndex(rvkey, rvval)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MetaData) unifyArray(data interface{}, rv reflect.Value) error {
|
func (md *MetaData) unifyArray(data any, rv reflect.Value) error {
|
||||||
datav := reflect.ValueOf(data)
|
datav := reflect.ValueOf(data)
|
||||||
if datav.Kind() != reflect.Slice {
|
if datav.Kind() != reflect.Slice {
|
||||||
if !datav.IsValid() {
|
if !datav.IsValid() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return badtype("slice", data)
|
return md.badtype("slice", data)
|
||||||
}
|
}
|
||||||
if l := datav.Len(); l != rv.Len() {
|
if l := datav.Len(); l != rv.Len() {
|
||||||
return e("expected array length %d; got TOML array of length %d", rv.Len(), l)
|
return md.e("expected array length %d; got TOML array of length %d", rv.Len(), l)
|
||||||
}
|
}
|
||||||
return md.unifySliceArray(datav, rv)
|
return md.unifySliceArray(datav, rv)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MetaData) unifySlice(data interface{}, rv reflect.Value) error {
|
func (md *MetaData) unifySlice(data any, rv reflect.Value) error {
|
||||||
datav := reflect.ValueOf(data)
|
datav := reflect.ValueOf(data)
|
||||||
if datav.Kind() != reflect.Slice {
|
if datav.Kind() != reflect.Slice {
|
||||||
if !datav.IsValid() {
|
if !datav.IsValid() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return badtype("slice", data)
|
return md.badtype("slice", data)
|
||||||
}
|
}
|
||||||
n := datav.Len()
|
n := datav.Len()
|
||||||
if rv.IsNil() || rv.Cap() < n {
|
if rv.IsNil() || rv.Cap() < n {
|
||||||
|
@ -346,26 +394,35 @@ func (md *MetaData) unifySliceArray(data, rv reflect.Value) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MetaData) unifyDatetime(data interface{}, rv reflect.Value) error {
|
func (md *MetaData) unifyString(data any, rv reflect.Value) error {
|
||||||
if _, ok := data.(time.Time); ok {
|
_, ok := rv.Interface().(json.Number)
|
||||||
rv.Set(reflect.ValueOf(data))
|
if ok {
|
||||||
|
if i, ok := data.(int64); ok {
|
||||||
|
rv.SetString(strconv.FormatInt(i, 10))
|
||||||
|
} else if f, ok := data.(float64); ok {
|
||||||
|
rv.SetString(strconv.FormatFloat(f, 'f', -1, 64))
|
||||||
|
} else {
|
||||||
|
return md.badtype("string", data)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return badtype("time.Time", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (md *MetaData) unifyString(data interface{}, rv reflect.Value) error {
|
|
||||||
if s, ok := data.(string); ok {
|
if s, ok := data.(string); ok {
|
||||||
rv.SetString(s)
|
rv.SetString(s)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return badtype("string", data)
|
return md.badtype("string", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error {
|
func (md *MetaData) unifyFloat64(data any, rv reflect.Value) error {
|
||||||
|
rvk := rv.Kind()
|
||||||
|
|
||||||
if num, ok := data.(float64); ok {
|
if num, ok := data.(float64); ok {
|
||||||
switch rv.Kind() {
|
switch rvk {
|
||||||
case reflect.Float32:
|
case reflect.Float32:
|
||||||
|
if num < -math.MaxFloat32 || num > math.MaxFloat32 {
|
||||||
|
return md.parseErr(errParseRange{i: num, size: rvk.String()})
|
||||||
|
}
|
||||||
fallthrough
|
fallthrough
|
||||||
case reflect.Float64:
|
case reflect.Float64:
|
||||||
rv.SetFloat(num)
|
rv.SetFloat(num)
|
||||||
|
@ -374,73 +431,85 @@ func (md *MetaData) unifyFloat64(data interface{}, rv reflect.Value) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return badtype("float", data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (md *MetaData) unifyInt(data interface{}, rv reflect.Value) error {
|
|
||||||
if num, ok := data.(int64); ok {
|
if num, ok := data.(int64); ok {
|
||||||
if rv.Kind() >= reflect.Int && rv.Kind() <= reflect.Int64 {
|
if (rvk == reflect.Float32 && (num < -maxSafeFloat32Int || num > maxSafeFloat32Int)) ||
|
||||||
switch rv.Kind() {
|
(rvk == reflect.Float64 && (num < -maxSafeFloat64Int || num > maxSafeFloat64Int)) {
|
||||||
case reflect.Int, reflect.Int64:
|
return md.parseErr(errUnsafeFloat{i: num, size: rvk.String()})
|
||||||
// No bounds checking necessary.
|
|
||||||
case reflect.Int8:
|
|
||||||
if num < math.MinInt8 || num > math.MaxInt8 {
|
|
||||||
return e("value %d is out of range for int8", num)
|
|
||||||
}
|
|
||||||
case reflect.Int16:
|
|
||||||
if num < math.MinInt16 || num > math.MaxInt16 {
|
|
||||||
return e("value %d is out of range for int16", num)
|
|
||||||
}
|
|
||||||
case reflect.Int32:
|
|
||||||
if num < math.MinInt32 || num > math.MaxInt32 {
|
|
||||||
return e("value %d is out of range for int32", num)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rv.SetInt(num)
|
|
||||||
} else if rv.Kind() >= reflect.Uint && rv.Kind() <= reflect.Uint64 {
|
|
||||||
unum := uint64(num)
|
|
||||||
switch rv.Kind() {
|
|
||||||
case reflect.Uint, reflect.Uint64:
|
|
||||||
// No bounds checking necessary.
|
|
||||||
case reflect.Uint8:
|
|
||||||
if num < 0 || unum > math.MaxUint8 {
|
|
||||||
return e("value %d is out of range for uint8", num)
|
|
||||||
}
|
|
||||||
case reflect.Uint16:
|
|
||||||
if num < 0 || unum > math.MaxUint16 {
|
|
||||||
return e("value %d is out of range for uint16", num)
|
|
||||||
}
|
|
||||||
case reflect.Uint32:
|
|
||||||
if num < 0 || unum > math.MaxUint32 {
|
|
||||||
return e("value %d is out of range for uint32", num)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rv.SetUint(unum)
|
|
||||||
} else {
|
|
||||||
panic("unreachable")
|
|
||||||
}
|
}
|
||||||
|
rv.SetFloat(float64(num))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return badtype("integer", data)
|
|
||||||
|
return md.badtype("float", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MetaData) unifyBool(data interface{}, rv reflect.Value) error {
|
func (md *MetaData) unifyInt(data any, rv reflect.Value) error {
|
||||||
|
_, ok := rv.Interface().(time.Duration)
|
||||||
|
if ok {
|
||||||
|
// Parse as string duration, and fall back to regular integer parsing
|
||||||
|
// (as nanosecond) if this is not a string.
|
||||||
|
if s, ok := data.(string); ok {
|
||||||
|
dur, err := time.ParseDuration(s)
|
||||||
|
if err != nil {
|
||||||
|
return md.parseErr(errParseDuration{s})
|
||||||
|
}
|
||||||
|
rv.SetInt(int64(dur))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
num, ok := data.(int64)
|
||||||
|
if !ok {
|
||||||
|
return md.badtype("integer", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
rvk := rv.Kind()
|
||||||
|
switch {
|
||||||
|
case rvk >= reflect.Int && rvk <= reflect.Int64:
|
||||||
|
if (rvk == reflect.Int8 && (num < math.MinInt8 || num > math.MaxInt8)) ||
|
||||||
|
(rvk == reflect.Int16 && (num < math.MinInt16 || num > math.MaxInt16)) ||
|
||||||
|
(rvk == reflect.Int32 && (num < math.MinInt32 || num > math.MaxInt32)) {
|
||||||
|
return md.parseErr(errParseRange{i: num, size: rvk.String()})
|
||||||
|
}
|
||||||
|
rv.SetInt(num)
|
||||||
|
case rvk >= reflect.Uint && rvk <= reflect.Uint64:
|
||||||
|
unum := uint64(num)
|
||||||
|
if rvk == reflect.Uint8 && (num < 0 || unum > math.MaxUint8) ||
|
||||||
|
rvk == reflect.Uint16 && (num < 0 || unum > math.MaxUint16) ||
|
||||||
|
rvk == reflect.Uint32 && (num < 0 || unum > math.MaxUint32) {
|
||||||
|
return md.parseErr(errParseRange{i: num, size: rvk.String()})
|
||||||
|
}
|
||||||
|
rv.SetUint(unum)
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *MetaData) unifyBool(data any, rv reflect.Value) error {
|
||||||
if b, ok := data.(bool); ok {
|
if b, ok := data.(bool); ok {
|
||||||
rv.SetBool(b)
|
rv.SetBool(b)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return badtype("boolean", data)
|
return md.badtype("boolean", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MetaData) unifyAnything(data interface{}, rv reflect.Value) error {
|
func (md *MetaData) unifyAnything(data any, rv reflect.Value) error {
|
||||||
rv.Set(reflect.ValueOf(data))
|
rv.Set(reflect.ValueOf(data))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (md *MetaData) unifyText(data interface{}, v encoding.TextUnmarshaler) error {
|
func (md *MetaData) unifyText(data any, v encoding.TextUnmarshaler) error {
|
||||||
var s string
|
var s string
|
||||||
switch sdata := data.(type) {
|
switch sdata := data.(type) {
|
||||||
case TextMarshaler:
|
case Marshaler:
|
||||||
|
text, err := sdata.MarshalTOML()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s = string(text)
|
||||||
|
case encoding.TextMarshaler:
|
||||||
text, err := sdata.MarshalText()
|
text, err := sdata.MarshalText()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -457,30 +526,62 @@ func (md *MetaData) unifyText(data interface{}, v encoding.TextUnmarshaler) erro
|
||||||
case float64:
|
case float64:
|
||||||
s = fmt.Sprintf("%f", sdata)
|
s = fmt.Sprintf("%f", sdata)
|
||||||
default:
|
default:
|
||||||
return badtype("primitive (string-like)", data)
|
return md.badtype("primitive (string-like)", data)
|
||||||
}
|
}
|
||||||
if err := v.UnmarshalText([]byte(s)); err != nil {
|
if err := v.UnmarshalText([]byte(s)); err != nil {
|
||||||
return err
|
return md.parseErr(err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (md *MetaData) badtype(dst string, data any) error {
|
||||||
|
return md.e("incompatible types: TOML value has type %s; destination has type %s", fmtType(data), dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *MetaData) parseErr(err error) error {
|
||||||
|
k := md.context.String()
|
||||||
|
return ParseError{
|
||||||
|
LastKey: k,
|
||||||
|
Position: md.keyInfo[k].pos,
|
||||||
|
Line: md.keyInfo[k].pos.Line,
|
||||||
|
err: err,
|
||||||
|
input: string(md.data),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md *MetaData) e(format string, args ...any) error {
|
||||||
|
f := "toml: "
|
||||||
|
if len(md.context) > 0 {
|
||||||
|
f = fmt.Sprintf("toml: (last key %q): ", md.context)
|
||||||
|
p := md.keyInfo[md.context.String()].pos
|
||||||
|
if p.Line > 0 {
|
||||||
|
f = fmt.Sprintf("toml: line %d (last key %q): ", p.Line, md.context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf(f+format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
// rvalue returns a reflect.Value of `v`. All pointers are resolved.
|
// rvalue returns a reflect.Value of `v`. All pointers are resolved.
|
||||||
func rvalue(v interface{}) reflect.Value {
|
func rvalue(v any) reflect.Value {
|
||||||
return indirect(reflect.ValueOf(v))
|
return indirect(reflect.ValueOf(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
// indirect returns the value pointed to by a pointer.
|
// indirect returns the value pointed to by a pointer.
|
||||||
// Pointers are followed until the value is not a pointer.
|
|
||||||
// New values are allocated for each nil pointer.
|
|
||||||
//
|
//
|
||||||
// An exception to this rule is if the value satisfies an interface of
|
// Pointers are followed until the value is not a pointer. New values are
|
||||||
// interest to us (like encoding.TextUnmarshaler).
|
// allocated for each nil pointer.
|
||||||
|
//
|
||||||
|
// An exception to this rule is if the value satisfies an interface of interest
|
||||||
|
// to us (like encoding.TextUnmarshaler).
|
||||||
func indirect(v reflect.Value) reflect.Value {
|
func indirect(v reflect.Value) reflect.Value {
|
||||||
if v.Kind() != reflect.Ptr {
|
if v.Kind() != reflect.Ptr {
|
||||||
if v.CanSet() {
|
if v.CanSet() {
|
||||||
pv := v.Addr()
|
pv := v.Addr()
|
||||||
if _, ok := pv.Interface().(encoding.TextUnmarshaler); ok {
|
pvi := pv.Interface()
|
||||||
|
if _, ok := pvi.(encoding.TextUnmarshaler); ok {
|
||||||
|
return pv
|
||||||
|
}
|
||||||
|
if _, ok := pvi.(Unmarshaler); ok {
|
||||||
return pv
|
return pv
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -496,16 +597,17 @@ func isUnifiable(rv reflect.Value) bool {
|
||||||
if rv.CanSet() {
|
if rv.CanSet() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if _, ok := rv.Interface().(encoding.TextUnmarshaler); ok {
|
rvi := rv.Interface()
|
||||||
|
if _, ok := rvi.(encoding.TextUnmarshaler); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if _, ok := rvi.(Unmarshaler); ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func e(format string, args ...interface{}) error {
|
// fmt %T with "interface {}" replaced with "any", which is far more readable.
|
||||||
return fmt.Errorf("toml: "+format, args...)
|
func fmtType(t any) string {
|
||||||
}
|
return strings.ReplaceAll(fmt.Sprintf("%T", t), "interface {}", "any")
|
||||||
|
|
||||||
func badtype(expected string, data interface{}) error {
|
|
||||||
return e("cannot load TOML value of type %T into a Go %s", data, expected)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
// +build go1.16
|
|
||||||
|
|
||||||
package toml
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io/fs"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DecodeFS is just like Decode, except it will automatically read the contents
|
|
||||||
// of the file at `path` from a fs.FS instance.
|
|
||||||
func DecodeFS(fsys fs.FS, path string, v interface{}) (MetaData, error) {
|
|
||||||
fp, err := fsys.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return MetaData{}, err
|
|
||||||
}
|
|
||||||
defer fp.Close()
|
|
||||||
return NewDecoder(fp).Decode(v)
|
|
||||||
}
|
|
|
@ -1,123 +0,0 @@
|
||||||
package toml
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
// MetaData allows access to meta information about TOML data that may not be
|
|
||||||
// inferable via reflection. In particular, whether a key has been defined and
|
|
||||||
// the TOML type of a key.
|
|
||||||
type MetaData struct {
|
|
||||||
mapping map[string]interface{}
|
|
||||||
types map[string]tomlType
|
|
||||||
keys []Key
|
|
||||||
decoded map[string]bool
|
|
||||||
context Key // Used only during decoding.
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsDefined reports if the key exists in the TOML data.
|
|
||||||
//
|
|
||||||
// The key should be specified hierarchically, for example to access the TOML
|
|
||||||
// key "a.b.c" you would use:
|
|
||||||
//
|
|
||||||
// IsDefined("a", "b", "c")
|
|
||||||
//
|
|
||||||
// IsDefined will return false if an empty key given. Keys are case sensitive.
|
|
||||||
func (md *MetaData) IsDefined(key ...string) bool {
|
|
||||||
if len(key) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var hash map[string]interface{}
|
|
||||||
var ok bool
|
|
||||||
var hashOrVal interface{} = md.mapping
|
|
||||||
for _, k := range key {
|
|
||||||
if hash, ok = hashOrVal.(map[string]interface{}); !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if hashOrVal, ok = hash[k]; !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type returns a string representation of the type of the key specified.
|
|
||||||
//
|
|
||||||
// Type will return the empty string if given an empty key or a key that does
|
|
||||||
// not exist. Keys are case sensitive.
|
|
||||||
func (md *MetaData) Type(key ...string) string {
|
|
||||||
fullkey := strings.Join(key, ".")
|
|
||||||
if typ, ok := md.types[fullkey]; ok {
|
|
||||||
return typ.typeString()
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key represents any TOML key, including key groups. Use (MetaData).Keys to get
|
|
||||||
// values of this type.
|
|
||||||
type Key []string
|
|
||||||
|
|
||||||
func (k Key) String() string { return strings.Join(k, ".") }
|
|
||||||
|
|
||||||
func (k Key) maybeQuotedAll() string {
|
|
||||||
var ss []string
|
|
||||||
for i := range k {
|
|
||||||
ss = append(ss, k.maybeQuoted(i))
|
|
||||||
}
|
|
||||||
return strings.Join(ss, ".")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k Key) maybeQuoted(i int) string {
|
|
||||||
if k[i] == "" {
|
|
||||||
return `""`
|
|
||||||
}
|
|
||||||
quote := false
|
|
||||||
for _, c := range k[i] {
|
|
||||||
if !isBareKeyChar(c) {
|
|
||||||
quote = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if quote {
|
|
||||||
return `"` + quotedReplacer.Replace(k[i]) + `"`
|
|
||||||
}
|
|
||||||
return k[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k Key) add(piece string) Key {
|
|
||||||
newKey := make(Key, len(k)+1)
|
|
||||||
copy(newKey, k)
|
|
||||||
newKey[len(k)] = piece
|
|
||||||
return newKey
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keys returns a slice of every key in the TOML data, including key groups.
|
|
||||||
//
|
|
||||||
// Each key is itself a slice, where the first element is the top of the
|
|
||||||
// hierarchy and the last is the most specific. The list will have the same
|
|
||||||
// order as the keys appeared in the TOML data.
|
|
||||||
//
|
|
||||||
// All keys returned are non-empty.
|
|
||||||
func (md *MetaData) Keys() []Key {
|
|
||||||
return md.keys
|
|
||||||
}
|
|
||||||
|
|
||||||
// Undecoded returns all keys that have not been decoded in the order in which
|
|
||||||
// they appear in the original TOML document.
|
|
||||||
//
|
|
||||||
// This includes keys that haven't been decoded because of a Primitive value.
|
|
||||||
// Once the Primitive value is decoded, the keys will be considered decoded.
|
|
||||||
//
|
|
||||||
// Also note that decoding into an empty interface will result in no decoding,
|
|
||||||
// and so no keys will be considered decoded.
|
|
||||||
//
|
|
||||||
// In this sense, the Undecoded keys correspond to keys in the TOML document
|
|
||||||
// that do not have a concrete type in your representation.
|
|
||||||
func (md *MetaData) Undecoded() []Key {
|
|
||||||
undecoded := make([]Key, 0, len(md.keys))
|
|
||||||
for _, key := range md.keys {
|
|
||||||
if !md.decoded[key.String()] {
|
|
||||||
undecoded = append(undecoded, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undecoded
|
|
||||||
}
|
|
|
@ -5,29 +5,25 @@ import (
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DEPRECATED!
|
// TextMarshaler is an alias for encoding.TextMarshaler.
|
||||||
//
|
//
|
||||||
// Use the identical encoding.TextMarshaler instead. It is defined here to
|
// Deprecated: use encoding.TextMarshaler
|
||||||
// support Go 1.1 and older.
|
|
||||||
type TextMarshaler encoding.TextMarshaler
|
type TextMarshaler encoding.TextMarshaler
|
||||||
|
|
||||||
// DEPRECATED!
|
// TextUnmarshaler is an alias for encoding.TextUnmarshaler.
|
||||||
//
|
//
|
||||||
// Use the identical encoding.TextUnmarshaler instead. It is defined here to
|
// Deprecated: use encoding.TextUnmarshaler
|
||||||
// support Go 1.1 and older.
|
|
||||||
type TextUnmarshaler encoding.TextUnmarshaler
|
type TextUnmarshaler encoding.TextUnmarshaler
|
||||||
|
|
||||||
// DEPRECATED!
|
// DecodeReader is an alias for NewDecoder(r).Decode(v).
|
||||||
//
|
//
|
||||||
// Use MetaData.PrimitiveDecode instead.
|
// Deprecated: use NewDecoder(reader).Decode(&value).
|
||||||
func PrimitiveDecode(primValue Primitive, v interface{}) error {
|
func DecodeReader(r io.Reader, v any) (MetaData, error) { return NewDecoder(r).Decode(v) }
|
||||||
md := MetaData{decoded: make(map[string]bool)}
|
|
||||||
|
// PrimitiveDecode is an alias for MetaData.PrimitiveDecode().
|
||||||
|
//
|
||||||
|
// Deprecated: use MetaData.PrimitiveDecode.
|
||||||
|
func PrimitiveDecode(primValue Primitive, v any) error {
|
||||||
|
md := MetaData{decoded: make(map[string]struct{})}
|
||||||
return md.unify(primValue.undecoded, rvalue(v))
|
return md.unify(primValue.undecoded, rvalue(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEPRECATED!
|
|
||||||
//
|
|
||||||
// Use NewDecoder(reader).Decode(&v) instead.
|
|
||||||
func DecodeReader(r io.Reader, v interface{}) (MetaData, error) {
|
|
||||||
return NewDecoder(r).Decode(v)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
/*
|
// Package toml implements decoding and encoding of TOML files.
|
||||||
Package toml implements decoding and encoding of TOML files.
|
//
|
||||||
|
// This package supports TOML v1.0.0, as specified at https://toml.io
|
||||||
This package supports TOML v1.0.0, as listed on https://toml.io
|
//
|
||||||
|
// The github.com/BurntSushi/toml/cmd/tomlv package implements a TOML validator,
|
||||||
There is also support for delaying decoding with the Primitive type, and
|
// and can be used to verify if TOML document is valid. It can also be used to
|
||||||
querying the set of keys in a TOML document with the MetaData type.
|
// print the type of each key.
|
||||||
|
|
||||||
The github.com/BurntSushi/toml/cmd/tomlv package implements a TOML validator,
|
|
||||||
and can be used to verify if TOML document is valid. It can also be used to
|
|
||||||
print the type of each key.
|
|
||||||
*/
|
|
||||||
package toml
|
package toml
|
||||||
|
|
|
@ -2,7 +2,9 @@ package toml
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"encoding"
|
"encoding"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
@ -21,12 +23,11 @@ type tomlEncodeError struct{ error }
|
||||||
var (
|
var (
|
||||||
errArrayNilElement = errors.New("toml: cannot encode array with nil element")
|
errArrayNilElement = errors.New("toml: cannot encode array with nil element")
|
||||||
errNonString = errors.New("toml: cannot encode a map with non-string key type")
|
errNonString = errors.New("toml: cannot encode a map with non-string key type")
|
||||||
errAnonNonStruct = errors.New("toml: cannot encode an anonymous field that is not a struct")
|
|
||||||
errNoKey = errors.New("toml: top-level values must be Go maps or structs")
|
errNoKey = errors.New("toml: top-level values must be Go maps or structs")
|
||||||
errAnything = errors.New("") // used in testing
|
errAnything = errors.New("") // used in testing
|
||||||
)
|
)
|
||||||
|
|
||||||
var quotedReplacer = strings.NewReplacer(
|
var dblQuotedReplacer = strings.NewReplacer(
|
||||||
"\"", "\\\"",
|
"\"", "\\\"",
|
||||||
"\\", "\\\\",
|
"\\", "\\\\",
|
||||||
"\x00", `\u0000`,
|
"\x00", `\u0000`,
|
||||||
|
@ -64,52 +65,86 @@ var quotedReplacer = strings.NewReplacer(
|
||||||
"\x7f", `\u007f`,
|
"\x7f", `\u007f`,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
marshalToml = reflect.TypeOf((*Marshaler)(nil)).Elem()
|
||||||
|
marshalText = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()
|
||||||
|
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Marshaler is the interface implemented by types that can marshal themselves
|
||||||
|
// into valid TOML.
|
||||||
|
type Marshaler interface {
|
||||||
|
MarshalTOML() ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal returns a TOML representation of the Go value.
|
||||||
|
//
|
||||||
|
// See [Encoder] for a description of the encoding process.
|
||||||
|
func Marshal(v any) ([]byte, error) {
|
||||||
|
buff := new(bytes.Buffer)
|
||||||
|
if err := NewEncoder(buff).Encode(v); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buff.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Encoder encodes a Go to a TOML document.
|
// Encoder encodes a Go to a TOML document.
|
||||||
//
|
//
|
||||||
// The mapping between Go values and TOML values should be precisely the same as
|
// The mapping between Go values and TOML values should be precisely the same as
|
||||||
// for the Decode* functions. Similarly, the TextMarshaler interface is
|
// for [Decode].
|
||||||
// supported by encoding the resulting bytes as strings. If you want to write
|
//
|
||||||
// arbitrary binary data then you will need to use something like base64 since
|
// time.Time is encoded as a RFC 3339 string, and time.Duration as its string
|
||||||
// TOML does not have any binary types.
|
// representation.
|
||||||
|
//
|
||||||
|
// The [Marshaler] and [encoding.TextMarshaler] interfaces are supported to
|
||||||
|
// encoding the value as custom TOML.
|
||||||
|
//
|
||||||
|
// If you want to write arbitrary binary data then you will need to use
|
||||||
|
// something like base64 since TOML does not have any binary types.
|
||||||
//
|
//
|
||||||
// When encoding TOML hashes (Go maps or structs), keys without any sub-hashes
|
// When encoding TOML hashes (Go maps or structs), keys without any sub-hashes
|
||||||
// are encoded first.
|
// are encoded first.
|
||||||
//
|
//
|
||||||
// Go maps will be sorted alphabetically by key for deterministic output.
|
// Go maps will be sorted alphabetically by key for deterministic output.
|
||||||
//
|
//
|
||||||
|
// The toml struct tag can be used to provide the key name; if omitted the
|
||||||
|
// struct field name will be used. If the "omitempty" option is present the
|
||||||
|
// following value will be skipped:
|
||||||
|
//
|
||||||
|
// - arrays, slices, maps, and string with len of 0
|
||||||
|
// - struct with all zero values
|
||||||
|
// - bool false
|
||||||
|
//
|
||||||
|
// If omitzero is given all int and float types with a value of 0 will be
|
||||||
|
// skipped.
|
||||||
|
//
|
||||||
// Encoding Go values without a corresponding TOML representation will return an
|
// Encoding Go values without a corresponding TOML representation will return an
|
||||||
// error. Examples of this includes maps with non-string keys, slices with nil
|
// error. Examples of this includes maps with non-string keys, slices with nil
|
||||||
// elements, embedded non-struct types, and nested slices containing maps or
|
// elements, embedded non-struct types, and nested slices containing maps or
|
||||||
// structs. (e.g. [][]map[string]string is not allowed but []map[string]string
|
// structs. (e.g. [][]map[string]string is not allowed but []map[string]string
|
||||||
// is okay, as is []map[string][]string).
|
// is okay, as is []map[string][]string).
|
||||||
//
|
//
|
||||||
// NOTE: Only exported keys are encoded due to the use of reflection. Unexported
|
// NOTE: only exported keys are encoded due to the use of reflection. Unexported
|
||||||
// keys are silently discarded.
|
// keys are silently discarded.
|
||||||
type Encoder struct {
|
type Encoder struct {
|
||||||
// The string to use for a single indentation level. The default is two
|
Indent string // string for a single indentation level; default is two spaces.
|
||||||
// spaces.
|
hasWritten bool // written any output to w yet?
|
||||||
Indent string
|
|
||||||
|
|
||||||
// hasWritten is whether we have written any output to w yet.
|
|
||||||
hasWritten bool
|
|
||||||
w *bufio.Writer
|
w *bufio.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEncoder create a new Encoder.
|
// NewEncoder create a new Encoder.
|
||||||
func NewEncoder(w io.Writer) *Encoder {
|
func NewEncoder(w io.Writer) *Encoder {
|
||||||
return &Encoder{
|
return &Encoder{w: bufio.NewWriter(w), Indent: " "}
|
||||||
w: bufio.NewWriter(w),
|
|
||||||
Indent: " ",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode writes a TOML representation of the Go value to the Encoder's writer.
|
// Encode writes a TOML representation of the Go value to the [Encoder]'s writer.
|
||||||
//
|
//
|
||||||
// An error is returned if the value given cannot be encoded to a valid TOML
|
// An error is returned if the value given cannot be encoded to a valid TOML
|
||||||
// document.
|
// document.
|
||||||
func (enc *Encoder) Encode(v interface{}) error {
|
func (enc *Encoder) Encode(v any) error {
|
||||||
rv := eindirect(reflect.ValueOf(v))
|
rv := eindirect(reflect.ValueOf(v))
|
||||||
if err := enc.safeEncode(Key([]string{}), rv); err != nil {
|
err := enc.safeEncode(Key([]string{}), rv)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return enc.w.Flush()
|
return enc.w.Flush()
|
||||||
|
@ -130,17 +165,15 @@ func (enc *Encoder) safeEncode(key Key, rv reflect.Value) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (enc *Encoder) encode(key Key, rv reflect.Value) {
|
func (enc *Encoder) encode(key Key, rv reflect.Value) {
|
||||||
// Special case. Time needs to be in ISO8601 format.
|
// If we can marshal the type to text, then we use that. This prevents the
|
||||||
// Special case. If we can marshal the type to text, then we used that.
|
// encoder for handling these types as generic structs (or whatever the
|
||||||
// Basically, this prevents the encoder for handling these types as
|
// underlying type of a TextMarshaler is).
|
||||||
// generic structs (or whatever the underlying type of a TextMarshaler is).
|
switch {
|
||||||
switch t := rv.Interface().(type) {
|
case isMarshaler(rv):
|
||||||
case time.Time, encoding.TextMarshaler:
|
|
||||||
enc.writeKeyValue(key, rv, false)
|
enc.writeKeyValue(key, rv, false)
|
||||||
return
|
return
|
||||||
// TODO: #76 would make this superfluous after implemented.
|
case rv.Type() == primitiveType: // TODO: #76 would make this superfluous after implemented.
|
||||||
case Primitive:
|
enc.encode(key, reflect.ValueOf(rv.Interface().(Primitive).undecoded))
|
||||||
enc.encode(key, reflect.ValueOf(t.undecoded))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,17 +233,49 @@ func (enc *Encoder) eElement(rv reflect.Value) {
|
||||||
enc.wf(v.In(time.UTC).Format(format))
|
enc.wf(v.In(time.UTC).Format(format))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
case encoding.TextMarshaler:
|
case Marshaler:
|
||||||
// Use text marshaler if it's available for this value.
|
s, err := v.MarshalTOML()
|
||||||
if s, err := v.MarshalText(); err != nil {
|
if err != nil {
|
||||||
encPanic(err)
|
encPanic(err)
|
||||||
} else {
|
|
||||||
enc.writeQuoted(string(s))
|
|
||||||
}
|
}
|
||||||
|
if s == nil {
|
||||||
|
encPanic(errors.New("MarshalTOML returned nil and no error"))
|
||||||
|
}
|
||||||
|
enc.w.Write(s)
|
||||||
return
|
return
|
||||||
|
case encoding.TextMarshaler:
|
||||||
|
s, err := v.MarshalText()
|
||||||
|
if err != nil {
|
||||||
|
encPanic(err)
|
||||||
|
}
|
||||||
|
if s == nil {
|
||||||
|
encPanic(errors.New("MarshalText returned nil and no error"))
|
||||||
|
}
|
||||||
|
enc.writeQuoted(string(s))
|
||||||
|
return
|
||||||
|
case time.Duration:
|
||||||
|
enc.writeQuoted(v.String())
|
||||||
|
return
|
||||||
|
case json.Number:
|
||||||
|
n, _ := rv.Interface().(json.Number)
|
||||||
|
|
||||||
|
if n == "" { /// Useful zero value.
|
||||||
|
enc.w.WriteByte('0')
|
||||||
|
return
|
||||||
|
} else if v, err := n.Int64(); err == nil {
|
||||||
|
enc.eElement(reflect.ValueOf(v))
|
||||||
|
return
|
||||||
|
} else if v, err := n.Float64(); err == nil {
|
||||||
|
enc.eElement(reflect.ValueOf(v))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
encPanic(fmt.Errorf("unable to convert %q to int64 or float64", n))
|
||||||
}
|
}
|
||||||
|
|
||||||
switch rv.Kind() {
|
switch rv.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
enc.eElement(rv.Elem())
|
||||||
|
return
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
enc.writeQuoted(rv.String())
|
enc.writeQuoted(rv.String())
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
|
@ -222,18 +287,30 @@ func (enc *Encoder) eElement(rv reflect.Value) {
|
||||||
case reflect.Float32:
|
case reflect.Float32:
|
||||||
f := rv.Float()
|
f := rv.Float()
|
||||||
if math.IsNaN(f) {
|
if math.IsNaN(f) {
|
||||||
|
if math.Signbit(f) {
|
||||||
|
enc.wf("-")
|
||||||
|
}
|
||||||
enc.wf("nan")
|
enc.wf("nan")
|
||||||
} else if math.IsInf(f, 0) {
|
} else if math.IsInf(f, 0) {
|
||||||
enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)])
|
if math.Signbit(f) {
|
||||||
|
enc.wf("-")
|
||||||
|
}
|
||||||
|
enc.wf("inf")
|
||||||
} else {
|
} else {
|
||||||
enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 32)))
|
enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 32)))
|
||||||
}
|
}
|
||||||
case reflect.Float64:
|
case reflect.Float64:
|
||||||
f := rv.Float()
|
f := rv.Float()
|
||||||
if math.IsNaN(f) {
|
if math.IsNaN(f) {
|
||||||
|
if math.Signbit(f) {
|
||||||
|
enc.wf("-")
|
||||||
|
}
|
||||||
enc.wf("nan")
|
enc.wf("nan")
|
||||||
} else if math.IsInf(f, 0) {
|
} else if math.IsInf(f, 0) {
|
||||||
enc.wf("%cinf", map[bool]byte{true: '-', false: '+'}[math.Signbit(f)])
|
if math.Signbit(f) {
|
||||||
|
enc.wf("-")
|
||||||
|
}
|
||||||
|
enc.wf("inf")
|
||||||
} else {
|
} else {
|
||||||
enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 64)))
|
enc.wf(floatAddDecimal(strconv.FormatFloat(f, 'f', -1, 64)))
|
||||||
}
|
}
|
||||||
|
@ -246,7 +323,7 @@ func (enc *Encoder) eElement(rv reflect.Value) {
|
||||||
case reflect.Interface:
|
case reflect.Interface:
|
||||||
enc.eElement(rv.Elem())
|
enc.eElement(rv.Elem())
|
||||||
default:
|
default:
|
||||||
encPanic(fmt.Errorf("unexpected primitive type: %T", rv.Interface()))
|
encPanic(fmt.Errorf("unexpected type: %s", fmtType(rv.Interface())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,14 +337,14 @@ func floatAddDecimal(fstr string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (enc *Encoder) writeQuoted(s string) {
|
func (enc *Encoder) writeQuoted(s string) {
|
||||||
enc.wf("\"%s\"", quotedReplacer.Replace(s))
|
enc.wf("\"%s\"", dblQuotedReplacer.Replace(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) {
|
func (enc *Encoder) eArrayOrSliceElement(rv reflect.Value) {
|
||||||
length := rv.Len()
|
length := rv.Len()
|
||||||
enc.wf("[")
|
enc.wf("[")
|
||||||
for i := 0; i < length; i++ {
|
for i := 0; i < length; i++ {
|
||||||
elem := rv.Index(i)
|
elem := eindirect(rv.Index(i))
|
||||||
enc.eElement(elem)
|
enc.eElement(elem)
|
||||||
if i != length-1 {
|
if i != length-1 {
|
||||||
enc.wf(", ")
|
enc.wf(", ")
|
||||||
|
@ -281,12 +358,12 @@ func (enc *Encoder) eArrayOfTables(key Key, rv reflect.Value) {
|
||||||
encPanic(errNoKey)
|
encPanic(errNoKey)
|
||||||
}
|
}
|
||||||
for i := 0; i < rv.Len(); i++ {
|
for i := 0; i < rv.Len(); i++ {
|
||||||
trv := rv.Index(i)
|
trv := eindirect(rv.Index(i))
|
||||||
if isNil(trv) {
|
if isNil(trv) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
enc.newline()
|
enc.newline()
|
||||||
enc.wf("%s[[%s]]", enc.indentStr(key), key.maybeQuotedAll())
|
enc.wf("%s[[%s]]", enc.indentStr(key), key)
|
||||||
enc.newline()
|
enc.newline()
|
||||||
enc.eMapOrStruct(key, trv, false)
|
enc.eMapOrStruct(key, trv, false)
|
||||||
}
|
}
|
||||||
|
@ -299,14 +376,14 @@ func (enc *Encoder) eTable(key Key, rv reflect.Value) {
|
||||||
enc.newline()
|
enc.newline()
|
||||||
}
|
}
|
||||||
if len(key) > 0 {
|
if len(key) > 0 {
|
||||||
enc.wf("%s[%s]", enc.indentStr(key), key.maybeQuotedAll())
|
enc.wf("%s[%s]", enc.indentStr(key), key)
|
||||||
enc.newline()
|
enc.newline()
|
||||||
}
|
}
|
||||||
enc.eMapOrStruct(key, rv, false)
|
enc.eMapOrStruct(key, rv, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value, inline bool) {
|
func (enc *Encoder) eMapOrStruct(key Key, rv reflect.Value, inline bool) {
|
||||||
switch rv := eindirect(rv); rv.Kind() {
|
switch rv.Kind() {
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
enc.eMap(key, rv, inline)
|
enc.eMap(key, rv, inline)
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
|
@ -328,7 +405,7 @@ func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) {
|
||||||
var mapKeysDirect, mapKeysSub []string
|
var mapKeysDirect, mapKeysSub []string
|
||||||
for _, mapKey := range rv.MapKeys() {
|
for _, mapKey := range rv.MapKeys() {
|
||||||
k := mapKey.String()
|
k := mapKey.String()
|
||||||
if typeIsHash(tomlTypeOfGo(rv.MapIndex(mapKey))) {
|
if typeIsTable(tomlTypeOfGo(eindirect(rv.MapIndex(mapKey)))) {
|
||||||
mapKeysSub = append(mapKeysSub, k)
|
mapKeysSub = append(mapKeysSub, k)
|
||||||
} else {
|
} else {
|
||||||
mapKeysDirect = append(mapKeysDirect, k)
|
mapKeysDirect = append(mapKeysDirect, k)
|
||||||
|
@ -338,7 +415,7 @@ func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) {
|
||||||
var writeMapKeys = func(mapKeys []string, trailC bool) {
|
var writeMapKeys = func(mapKeys []string, trailC bool) {
|
||||||
sort.Strings(mapKeys)
|
sort.Strings(mapKeys)
|
||||||
for i, mapKey := range mapKeys {
|
for i, mapKey := range mapKeys {
|
||||||
val := rv.MapIndex(reflect.ValueOf(mapKey))
|
val := eindirect(rv.MapIndex(reflect.ValueOf(mapKey)))
|
||||||
if isNil(val) {
|
if isNil(val) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -364,6 +441,15 @@ func (enc *Encoder) eMap(key Key, rv reflect.Value, inline bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const is32Bit = (32 << (^uint(0) >> 63)) == 32
|
||||||
|
|
||||||
|
func pointerTo(t reflect.Type) reflect.Type {
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
return pointerTo(t.Elem())
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
|
func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
|
||||||
// Write keys for fields directly under this key first, because if we write
|
// Write keys for fields directly under this key first, because if we write
|
||||||
// a field that creates a new table then all keys under it will be in that
|
// a field that creates a new table then all keys under it will be in that
|
||||||
|
@ -380,35 +466,39 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
|
||||||
addFields = func(rt reflect.Type, rv reflect.Value, start []int) {
|
addFields = func(rt reflect.Type, rv reflect.Value, start []int) {
|
||||||
for i := 0; i < rt.NumField(); i++ {
|
for i := 0; i < rt.NumField(); i++ {
|
||||||
f := rt.Field(i)
|
f := rt.Field(i)
|
||||||
if f.PkgPath != "" && !f.Anonymous { /// Skip unexported fields.
|
isEmbed := f.Anonymous && pointerTo(f.Type).Kind() == reflect.Struct
|
||||||
|
if f.PkgPath != "" && !isEmbed { /// Skip unexported fields.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
opts := getOptions(f.Tag)
|
||||||
|
if opts.skip {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
frv := rv.Field(i)
|
frv := eindirect(rv.Field(i))
|
||||||
|
|
||||||
|
if is32Bit {
|
||||||
|
// Copy so it works correct on 32bit archs; not clear why this
|
||||||
|
// is needed. See #314, and https://www.reddit.com/r/golang/comments/pnx8v4
|
||||||
|
// This also works fine on 64bit, but 32bit archs are somewhat
|
||||||
|
// rare and this is a wee bit faster.
|
||||||
|
copyStart := make([]int, len(start))
|
||||||
|
copy(copyStart, start)
|
||||||
|
start = copyStart
|
||||||
|
}
|
||||||
|
|
||||||
// Treat anonymous struct fields with tag names as though they are
|
// Treat anonymous struct fields with tag names as though they are
|
||||||
// not anonymous, like encoding/json does.
|
// not anonymous, like encoding/json does.
|
||||||
//
|
//
|
||||||
// Non-struct anonymous fields use the normal encoding logic.
|
// Non-struct anonymous fields use the normal encoding logic.
|
||||||
if f.Anonymous {
|
if isEmbed {
|
||||||
t := f.Type
|
if getOptions(f.Tag).name == "" && frv.Kind() == reflect.Struct {
|
||||||
switch t.Kind() {
|
addFields(frv.Type(), frv, append(start, f.Index...))
|
||||||
case reflect.Struct:
|
continue
|
||||||
if getOptions(f.Tag).name == "" {
|
|
||||||
addFields(t, frv, append(start, f.Index...))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
case reflect.Ptr:
|
|
||||||
if t.Elem().Kind() == reflect.Struct && getOptions(f.Tag).name == "" {
|
|
||||||
if !frv.IsNil() {
|
|
||||||
addFields(t.Elem(), frv.Elem(), append(start, f.Index...))
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if typeIsHash(tomlTypeOfGo(frv)) {
|
if typeIsTable(tomlTypeOfGo(frv)) {
|
||||||
fieldsSub = append(fieldsSub, append(start, f.Index...))
|
fieldsSub = append(fieldsSub, append(start, f.Index...))
|
||||||
} else {
|
} else {
|
||||||
fieldsDirect = append(fieldsDirect, append(start, f.Index...))
|
fieldsDirect = append(fieldsDirect, append(start, f.Index...))
|
||||||
|
@ -422,21 +512,25 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
|
||||||
fieldType := rt.FieldByIndex(fieldIndex)
|
fieldType := rt.FieldByIndex(fieldIndex)
|
||||||
fieldVal := rv.FieldByIndex(fieldIndex)
|
fieldVal := rv.FieldByIndex(fieldIndex)
|
||||||
|
|
||||||
if isNil(fieldVal) { /// Don't write anything for nil fields.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := getOptions(fieldType.Tag)
|
opts := getOptions(fieldType.Tag)
|
||||||
if opts.skip {
|
if opts.skip {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if opts.omitempty && isEmpty(fieldVal) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldVal = eindirect(fieldVal)
|
||||||
|
|
||||||
|
if isNil(fieldVal) { /// Don't write anything for nil fields.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
keyName := fieldType.Name
|
keyName := fieldType.Name
|
||||||
if opts.name != "" {
|
if opts.name != "" {
|
||||||
keyName = opts.name
|
keyName = opts.name
|
||||||
}
|
}
|
||||||
if opts.omitempty && isEmpty(fieldVal) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if opts.omitzero && isZero(fieldVal) {
|
if opts.omitzero && isZero(fieldVal) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -462,17 +556,32 @@ func (enc *Encoder) eStruct(key Key, rv reflect.Value, inline bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tomlTypeName returns the TOML type name of the Go value's type. It is
|
// tomlTypeOfGo returns the TOML type name of the Go value's type.
|
||||||
// used to determine whether the types of array elements are mixed (which is
|
//
|
||||||
// forbidden). If the Go value is nil, then it is illegal for it to be an array
|
// It is used to determine whether the types of array elements are mixed (which
|
||||||
// element, and valueIsNil is returned as true.
|
// is forbidden). If the Go value is nil, then it is illegal for it to be an
|
||||||
|
// array element, and valueIsNil is returned as true.
|
||||||
// Returns the TOML type of a Go value. The type may be `nil`, which means
|
//
|
||||||
// no concrete TOML type could be found.
|
// The type may be `nil`, which means no concrete TOML type could be found.
|
||||||
func tomlTypeOfGo(rv reflect.Value) tomlType {
|
func tomlTypeOfGo(rv reflect.Value) tomlType {
|
||||||
if isNil(rv) || !rv.IsValid() {
|
if isNil(rv) || !rv.IsValid() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if rv.Kind() == reflect.Struct {
|
||||||
|
if rv.Type() == timeType {
|
||||||
|
return tomlDatetime
|
||||||
|
}
|
||||||
|
if isMarshaler(rv) {
|
||||||
|
return tomlString
|
||||||
|
}
|
||||||
|
return tomlHash
|
||||||
|
}
|
||||||
|
|
||||||
|
if isMarshaler(rv) {
|
||||||
|
return tomlString
|
||||||
|
}
|
||||||
|
|
||||||
switch rv.Kind() {
|
switch rv.Kind() {
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
return tomlBool
|
return tomlBool
|
||||||
|
@ -484,7 +593,7 @@ func tomlTypeOfGo(rv reflect.Value) tomlType {
|
||||||
case reflect.Float32, reflect.Float64:
|
case reflect.Float32, reflect.Float64:
|
||||||
return tomlFloat
|
return tomlFloat
|
||||||
case reflect.Array, reflect.Slice:
|
case reflect.Array, reflect.Slice:
|
||||||
if typeEqual(tomlHash, tomlArrayType(rv)) {
|
if isTableArray(rv) {
|
||||||
return tomlArrayHash
|
return tomlArrayHash
|
||||||
}
|
}
|
||||||
return tomlArray
|
return tomlArray
|
||||||
|
@ -494,56 +603,35 @@ func tomlTypeOfGo(rv reflect.Value) tomlType {
|
||||||
return tomlString
|
return tomlString
|
||||||
case reflect.Map:
|
case reflect.Map:
|
||||||
return tomlHash
|
return tomlHash
|
||||||
case reflect.Struct:
|
|
||||||
switch rv.Interface().(type) {
|
|
||||||
case time.Time:
|
|
||||||
return tomlDatetime
|
|
||||||
case encoding.TextMarshaler:
|
|
||||||
return tomlString
|
|
||||||
default:
|
|
||||||
// Someone used a pointer receiver: we can make it work for pointer
|
|
||||||
// values.
|
|
||||||
if rv.CanAddr() {
|
|
||||||
_, ok := rv.Addr().Interface().(encoding.TextMarshaler)
|
|
||||||
if ok {
|
|
||||||
return tomlString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tomlHash
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
_, ok := rv.Interface().(encoding.TextMarshaler)
|
|
||||||
if ok {
|
|
||||||
return tomlString
|
|
||||||
}
|
|
||||||
encPanic(errors.New("unsupported type: " + rv.Kind().String()))
|
encPanic(errors.New("unsupported type: " + rv.Kind().String()))
|
||||||
panic("") // Need *some* return value
|
panic("unreachable")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tomlArrayType returns the element type of a TOML array. The type returned
|
func isMarshaler(rv reflect.Value) bool {
|
||||||
// may be nil if it cannot be determined (e.g., a nil slice or a zero length
|
return rv.Type().Implements(marshalText) || rv.Type().Implements(marshalToml)
|
||||||
// slize). This function may also panic if it finds a type that cannot be
|
}
|
||||||
// expressed in TOML (such as nil elements, heterogeneous arrays or directly
|
|
||||||
// nested arrays of tables).
|
// isTableArray reports if all entries in the array or slice are a table.
|
||||||
func tomlArrayType(rv reflect.Value) tomlType {
|
func isTableArray(arr reflect.Value) bool {
|
||||||
if isNil(rv) || !rv.IsValid() || rv.Len() == 0 {
|
if isNil(arr) || !arr.IsValid() || arr.Len() == 0 {
|
||||||
return nil
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Don't allow nil.
|
ret := true
|
||||||
rvlen := rv.Len()
|
for i := 0; i < arr.Len(); i++ {
|
||||||
for i := 1; i < rvlen; i++ {
|
tt := tomlTypeOfGo(eindirect(arr.Index(i)))
|
||||||
if tomlTypeOfGo(rv.Index(i)) == nil {
|
// Don't allow nil.
|
||||||
|
if tt == nil {
|
||||||
encPanic(errArrayNilElement)
|
encPanic(errArrayNilElement)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
firstType := tomlTypeOfGo(rv.Index(0))
|
if ret && !typeEqual(tomlHash, tt) {
|
||||||
if firstType == nil {
|
ret = false
|
||||||
encPanic(errArrayNilElement)
|
}
|
||||||
}
|
}
|
||||||
return firstType
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
type tagOptions struct {
|
type tagOptions struct {
|
||||||
|
@ -588,8 +676,26 @@ func isEmpty(rv reflect.Value) bool {
|
||||||
switch rv.Kind() {
|
switch rv.Kind() {
|
||||||
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
|
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
|
||||||
return rv.Len() == 0
|
return rv.Len() == 0
|
||||||
|
case reflect.Struct:
|
||||||
|
if rv.Type().Comparable() {
|
||||||
|
return reflect.Zero(rv.Type()).Interface() == rv.Interface()
|
||||||
|
}
|
||||||
|
// Need to also check if all the fields are empty, otherwise something
|
||||||
|
// like this with uncomparable types will always return true:
|
||||||
|
//
|
||||||
|
// type a struct{ field b }
|
||||||
|
// type b struct{ s []string }
|
||||||
|
// s := a{field: b{s: []string{"AAA"}}}
|
||||||
|
for i := 0; i < rv.NumField(); i++ {
|
||||||
|
if !isEmpty(rv.Field(i)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
return !rv.Bool()
|
return !rv.Bool()
|
||||||
|
case reflect.Ptr:
|
||||||
|
return rv.IsNil()
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -602,12 +708,21 @@ func (enc *Encoder) newline() {
|
||||||
|
|
||||||
// Write a key/value pair:
|
// Write a key/value pair:
|
||||||
//
|
//
|
||||||
// key = <any value>
|
// key = <any value>
|
||||||
//
|
//
|
||||||
// If inline is true it won't add a newline at the end.
|
// This is also used for "k = v" in inline tables; so something like this will
|
||||||
|
// be written in three calls:
|
||||||
|
//
|
||||||
|
// ┌───────────────────┐
|
||||||
|
// │ ┌───┐ ┌────┐│
|
||||||
|
// v v v v vv
|
||||||
|
// key = {k = 1, k2 = 2}
|
||||||
func (enc *Encoder) writeKeyValue(key Key, val reflect.Value, inline bool) {
|
func (enc *Encoder) writeKeyValue(key Key, val reflect.Value, inline bool) {
|
||||||
|
/// Marshaler used on top-level document; call eElement() to just call
|
||||||
|
/// Marshal{TOML,Text}.
|
||||||
if len(key) == 0 {
|
if len(key) == 0 {
|
||||||
encPanic(errNoKey)
|
enc.eElement(val)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
|
enc.wf("%s%s = ", enc.indentStr(key), key.maybeQuoted(len(key)-1))
|
||||||
enc.eElement(val)
|
enc.eElement(val)
|
||||||
|
@ -616,8 +731,9 @@ func (enc *Encoder) writeKeyValue(key Key, val reflect.Value, inline bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (enc *Encoder) wf(format string, v ...interface{}) {
|
func (enc *Encoder) wf(format string, v ...any) {
|
||||||
if _, err := fmt.Fprintf(enc.w, format, v...); err != nil {
|
_, err := fmt.Fprintf(enc.w, format, v...)
|
||||||
|
if err != nil {
|
||||||
encPanic(err)
|
encPanic(err)
|
||||||
}
|
}
|
||||||
enc.hasWritten = true
|
enc.hasWritten = true
|
||||||
|
@ -631,13 +747,25 @@ func encPanic(err error) {
|
||||||
panic(tomlEncodeError{err})
|
panic(tomlEncodeError{err})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve any level of pointers to the actual value (e.g. **string → string).
|
||||||
func eindirect(v reflect.Value) reflect.Value {
|
func eindirect(v reflect.Value) reflect.Value {
|
||||||
switch v.Kind() {
|
if v.Kind() != reflect.Ptr && v.Kind() != reflect.Interface {
|
||||||
case reflect.Ptr, reflect.Interface:
|
if isMarshaler(v) {
|
||||||
return eindirect(v.Elem())
|
return v
|
||||||
default:
|
}
|
||||||
|
if v.CanAddr() { /// Special case for marshalers; see #358.
|
||||||
|
if pv := v.Addr(); isMarshaler(pv) {
|
||||||
|
return pv
|
||||||
|
}
|
||||||
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v.IsNil() {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
return eindirect(v.Elem())
|
||||||
}
|
}
|
||||||
|
|
||||||
func isNil(rv reflect.Value) bool {
|
func isNil(rv reflect.Value) bool {
|
||||||
|
|
|
@ -0,0 +1,356 @@
|
||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseError is returned when there is an error parsing the TOML syntax such as
|
||||||
|
// invalid syntax, duplicate keys, etc.
|
||||||
|
//
|
||||||
|
// In addition to the error message itself, you can also print detailed location
|
||||||
|
// information with context by using [ErrorWithPosition]:
|
||||||
|
//
|
||||||
|
// toml: error: Key 'fruit' was already created and cannot be used as an array.
|
||||||
|
//
|
||||||
|
// At line 4, column 2-7:
|
||||||
|
//
|
||||||
|
// 2 | fruit = []
|
||||||
|
// 3 |
|
||||||
|
// 4 | [[fruit]] # Not allowed
|
||||||
|
// ^^^^^
|
||||||
|
//
|
||||||
|
// [ErrorWithUsage] can be used to print the above with some more detailed usage
|
||||||
|
// guidance:
|
||||||
|
//
|
||||||
|
// toml: error: newlines not allowed within inline tables
|
||||||
|
//
|
||||||
|
// At line 1, column 18:
|
||||||
|
//
|
||||||
|
// 1 | x = [{ key = 42 #
|
||||||
|
// ^
|
||||||
|
//
|
||||||
|
// Error help:
|
||||||
|
//
|
||||||
|
// Inline tables must always be on a single line:
|
||||||
|
//
|
||||||
|
// table = {key = 42, second = 43}
|
||||||
|
//
|
||||||
|
// It is invalid to split them over multiple lines like so:
|
||||||
|
//
|
||||||
|
// # INVALID
|
||||||
|
// table = {
|
||||||
|
// key = 42,
|
||||||
|
// second = 43
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Use regular for this:
|
||||||
|
//
|
||||||
|
// [table]
|
||||||
|
// key = 42
|
||||||
|
// second = 43
|
||||||
|
type ParseError struct {
|
||||||
|
Message string // Short technical message.
|
||||||
|
Usage string // Longer message with usage guidance; may be blank.
|
||||||
|
Position Position // Position of the error
|
||||||
|
LastKey string // Last parsed key, may be blank.
|
||||||
|
|
||||||
|
// Line the error occurred.
|
||||||
|
//
|
||||||
|
// Deprecated: use [Position].
|
||||||
|
Line int
|
||||||
|
|
||||||
|
err error
|
||||||
|
input string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position of an error.
|
||||||
|
type Position struct {
|
||||||
|
Line int // Line number, starting at 1.
|
||||||
|
Start int // Start of error, as byte offset starting at 0.
|
||||||
|
Len int // Lenght in bytes.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pe ParseError) Error() string {
|
||||||
|
msg := pe.Message
|
||||||
|
if msg == "" { // Error from errorf()
|
||||||
|
msg = pe.err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
if pe.LastKey == "" {
|
||||||
|
return fmt.Sprintf("toml: line %d: %s", pe.Position.Line, msg)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("toml: line %d (last key %q): %s",
|
||||||
|
pe.Position.Line, pe.LastKey, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorWithPosition returns the error with detailed location context.
|
||||||
|
//
|
||||||
|
// See the documentation on [ParseError].
|
||||||
|
func (pe ParseError) ErrorWithPosition() string {
|
||||||
|
if pe.input == "" { // Should never happen, but just in case.
|
||||||
|
return pe.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
lines = strings.Split(pe.input, "\n")
|
||||||
|
col = pe.column(lines)
|
||||||
|
b = new(strings.Builder)
|
||||||
|
)
|
||||||
|
|
||||||
|
msg := pe.Message
|
||||||
|
if msg == "" {
|
||||||
|
msg = pe.err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: don't show control characters as literals? This may not show up
|
||||||
|
// well everywhere.
|
||||||
|
|
||||||
|
if pe.Position.Len == 1 {
|
||||||
|
fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d:\n\n",
|
||||||
|
msg, pe.Position.Line, col+1)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(b, "toml: error: %s\n\nAt line %d, column %d-%d:\n\n",
|
||||||
|
msg, pe.Position.Line, col, col+pe.Position.Len)
|
||||||
|
}
|
||||||
|
if pe.Position.Line > 2 {
|
||||||
|
fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-2, expandTab(lines[pe.Position.Line-3]))
|
||||||
|
}
|
||||||
|
if pe.Position.Line > 1 {
|
||||||
|
fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line-1, expandTab(lines[pe.Position.Line-2]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expand tabs, so that the ^^^s are at the correct position, but leave
|
||||||
|
/// "column 10-13" intact. Adjusting this to the visual column would be
|
||||||
|
/// better, but we don't know the tabsize of the user in their editor, which
|
||||||
|
/// can be 8, 4, 2, or something else. We can't know. So leaving it as the
|
||||||
|
/// character index is probably the "most correct".
|
||||||
|
expanded := expandTab(lines[pe.Position.Line-1])
|
||||||
|
diff := len(expanded) - len(lines[pe.Position.Line-1])
|
||||||
|
|
||||||
|
fmt.Fprintf(b, "% 7d | %s\n", pe.Position.Line, expanded)
|
||||||
|
fmt.Fprintf(b, "% 10s%s%s\n", "", strings.Repeat(" ", col+diff), strings.Repeat("^", pe.Position.Len))
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorWithUsage returns the error with detailed location context and usage
|
||||||
|
// guidance.
|
||||||
|
//
|
||||||
|
// See the documentation on [ParseError].
|
||||||
|
func (pe ParseError) ErrorWithUsage() string {
|
||||||
|
m := pe.ErrorWithPosition()
|
||||||
|
if u, ok := pe.err.(interface{ Usage() string }); ok && u.Usage() != "" {
|
||||||
|
lines := strings.Split(strings.TrimSpace(u.Usage()), "\n")
|
||||||
|
for i := range lines {
|
||||||
|
if lines[i] != "" {
|
||||||
|
lines[i] = " " + lines[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m + "Error help:\n\n" + strings.Join(lines, "\n") + "\n"
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pe ParseError) column(lines []string) int {
|
||||||
|
var pos, col int
|
||||||
|
for i := range lines {
|
||||||
|
ll := len(lines[i]) + 1 // +1 for the removed newline
|
||||||
|
if pos+ll >= pe.Position.Start {
|
||||||
|
col = pe.Position.Start - pos
|
||||||
|
if col < 0 { // Should never happen, but just in case.
|
||||||
|
col = 0
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pos += ll
|
||||||
|
}
|
||||||
|
|
||||||
|
return col
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandTab(s string) string {
|
||||||
|
var (
|
||||||
|
b strings.Builder
|
||||||
|
l int
|
||||||
|
fill = func(n int) string {
|
||||||
|
b := make([]byte, n)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = ' '
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
b.Grow(len(s))
|
||||||
|
for _, r := range s {
|
||||||
|
switch r {
|
||||||
|
case '\t':
|
||||||
|
tw := 8 - l%8
|
||||||
|
b.WriteString(fill(tw))
|
||||||
|
l += tw
|
||||||
|
default:
|
||||||
|
b.WriteRune(r)
|
||||||
|
l += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
errLexControl struct{ r rune }
|
||||||
|
errLexEscape struct{ r rune }
|
||||||
|
errLexUTF8 struct{ b byte }
|
||||||
|
errParseDate struct{ v string }
|
||||||
|
errLexInlineTableNL struct{}
|
||||||
|
errLexStringNL struct{}
|
||||||
|
errParseRange struct {
|
||||||
|
i any // int or float
|
||||||
|
size string // "int64", "uint16", etc.
|
||||||
|
}
|
||||||
|
errUnsafeFloat struct {
|
||||||
|
i interface{} // float32 or float64
|
||||||
|
size string // "float32" or "float64"
|
||||||
|
}
|
||||||
|
errParseDuration struct{ d string }
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e errLexControl) Error() string {
|
||||||
|
return fmt.Sprintf("TOML files cannot contain control characters: '0x%02x'", e.r)
|
||||||
|
}
|
||||||
|
func (e errLexControl) Usage() string { return "" }
|
||||||
|
|
||||||
|
func (e errLexEscape) Error() string { return fmt.Sprintf(`invalid escape in string '\%c'`, e.r) }
|
||||||
|
func (e errLexEscape) Usage() string { return usageEscape }
|
||||||
|
func (e errLexUTF8) Error() string { return fmt.Sprintf("invalid UTF-8 byte: 0x%02x", e.b) }
|
||||||
|
func (e errLexUTF8) Usage() string { return "" }
|
||||||
|
func (e errParseDate) Error() string { return fmt.Sprintf("invalid datetime: %q", e.v) }
|
||||||
|
func (e errParseDate) Usage() string { return usageDate }
|
||||||
|
func (e errLexInlineTableNL) Error() string { return "newlines not allowed within inline tables" }
|
||||||
|
func (e errLexInlineTableNL) Usage() string { return usageInlineNewline }
|
||||||
|
func (e errLexStringNL) Error() string { return "strings cannot contain newlines" }
|
||||||
|
func (e errLexStringNL) Usage() string { return usageStringNewline }
|
||||||
|
func (e errParseRange) Error() string { return fmt.Sprintf("%v is out of range for %s", e.i, e.size) }
|
||||||
|
func (e errParseRange) Usage() string { return usageIntOverflow }
|
||||||
|
func (e errUnsafeFloat) Error() string {
|
||||||
|
return fmt.Sprintf("%v is out of the safe %s range", e.i, e.size)
|
||||||
|
}
|
||||||
|
func (e errUnsafeFloat) Usage() string { return usageUnsafeFloat }
|
||||||
|
func (e errParseDuration) Error() string { return fmt.Sprintf("invalid duration: %q", e.d) }
|
||||||
|
func (e errParseDuration) Usage() string { return usageDuration }
|
||||||
|
|
||||||
|
const usageEscape = `
|
||||||
|
A '\' inside a "-delimited string is interpreted as an escape character.
|
||||||
|
|
||||||
|
The following escape sequences are supported:
|
||||||
|
\b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX
|
||||||
|
|
||||||
|
To prevent a '\' from being recognized as an escape character, use either:
|
||||||
|
|
||||||
|
- a ' or '''-delimited string; escape characters aren't processed in them; or
|
||||||
|
- write two backslashes to get a single backslash: '\\'.
|
||||||
|
|
||||||
|
If you're trying to add a Windows path (e.g. "C:\Users\martin") then using '/'
|
||||||
|
instead of '\' will usually also work: "C:/Users/martin".
|
||||||
|
`
|
||||||
|
|
||||||
|
const usageInlineNewline = `
|
||||||
|
Inline tables must always be on a single line:
|
||||||
|
|
||||||
|
table = {key = 42, second = 43}
|
||||||
|
|
||||||
|
It is invalid to split them over multiple lines like so:
|
||||||
|
|
||||||
|
# INVALID
|
||||||
|
table = {
|
||||||
|
key = 42,
|
||||||
|
second = 43
|
||||||
|
}
|
||||||
|
|
||||||
|
Use regular for this:
|
||||||
|
|
||||||
|
[table]
|
||||||
|
key = 42
|
||||||
|
second = 43
|
||||||
|
`
|
||||||
|
|
||||||
|
const usageStringNewline = `
|
||||||
|
Strings must always be on a single line, and cannot span more than one line:
|
||||||
|
|
||||||
|
# INVALID
|
||||||
|
string = "Hello,
|
||||||
|
world!"
|
||||||
|
|
||||||
|
Instead use """ or ''' to split strings over multiple lines:
|
||||||
|
|
||||||
|
string = """Hello,
|
||||||
|
world!"""
|
||||||
|
`
|
||||||
|
|
||||||
|
const usageIntOverflow = `
|
||||||
|
This number is too large; this may be an error in the TOML, but it can also be a
|
||||||
|
bug in the program that uses too small of an integer.
|
||||||
|
|
||||||
|
The maximum and minimum values are:
|
||||||
|
|
||||||
|
size │ lowest │ highest
|
||||||
|
───────┼────────────────┼──────────────
|
||||||
|
int8 │ -128 │ 127
|
||||||
|
int16 │ -32,768 │ 32,767
|
||||||
|
int32 │ -2,147,483,648 │ 2,147,483,647
|
||||||
|
int64 │ -9.2 × 10¹⁷ │ 9.2 × 10¹⁷
|
||||||
|
uint8 │ 0 │ 255
|
||||||
|
uint16 │ 0 │ 65,535
|
||||||
|
uint32 │ 0 │ 4,294,967,295
|
||||||
|
uint64 │ 0 │ 1.8 × 10¹⁸
|
||||||
|
|
||||||
|
int refers to int32 on 32-bit systems and int64 on 64-bit systems.
|
||||||
|
`
|
||||||
|
|
||||||
|
const usageUnsafeFloat = `
|
||||||
|
This number is outside of the "safe" range for floating point numbers; whole
|
||||||
|
(non-fractional) numbers outside the below range can not always be represented
|
||||||
|
accurately in a float, leading to some loss of accuracy.
|
||||||
|
|
||||||
|
Explicitly mark a number as a fractional unit by adding ".0", which will incur
|
||||||
|
some loss of accuracy; for example:
|
||||||
|
|
||||||
|
f = 2_000_000_000.0
|
||||||
|
|
||||||
|
Accuracy ranges:
|
||||||
|
|
||||||
|
float32 = 16,777,215
|
||||||
|
float64 = 9,007,199,254,740,991
|
||||||
|
`
|
||||||
|
|
||||||
|
const usageDuration = `
|
||||||
|
A duration must be as "number<unit>", without any spaces. Valid units are:
|
||||||
|
|
||||||
|
ns nanoseconds (billionth of a second)
|
||||||
|
us, µs microseconds (millionth of a second)
|
||||||
|
ms milliseconds (thousands of a second)
|
||||||
|
s seconds
|
||||||
|
m minutes
|
||||||
|
h hours
|
||||||
|
|
||||||
|
You can combine multiple units; for example "5m10s" for 5 minutes and 10
|
||||||
|
seconds.
|
||||||
|
`
|
||||||
|
|
||||||
|
const usageDate = `
|
||||||
|
A TOML datetime must be in one of the following formats:
|
||||||
|
|
||||||
|
2006-01-02T15:04:05Z07:00 Date and time, with timezone.
|
||||||
|
2006-01-02T15:04:05 Date and time, but without timezone.
|
||||||
|
2006-01-02 Date without a time or timezone.
|
||||||
|
15:04:05 Just a time, without any timezone.
|
||||||
|
|
||||||
|
Seconds may optionally have a fraction, up to nanosecond precision:
|
||||||
|
|
||||||
|
15:04:05.123
|
||||||
|
15:04:05.856018510
|
||||||
|
`
|
||||||
|
|
||||||
|
// TOML 1.1:
|
||||||
|
// The seconds part in times is optional, and may be omitted:
|
||||||
|
// 2006-01-02T15:04Z07:00
|
||||||
|
// 2006-01-02T15:04
|
||||||
|
// 15:04
|
|
@ -17,6 +17,7 @@ const (
|
||||||
itemEOF
|
itemEOF
|
||||||
itemText
|
itemText
|
||||||
itemString
|
itemString
|
||||||
|
itemStringEsc
|
||||||
itemRawString
|
itemRawString
|
||||||
itemMultilineString
|
itemMultilineString
|
||||||
itemRawMultilineString
|
itemRawMultilineString
|
||||||
|
@ -37,56 +38,44 @@ const (
|
||||||
itemInlineTableEnd
|
itemInlineTableEnd
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const eof = 0
|
||||||
eof = 0
|
|
||||||
comma = ','
|
|
||||||
tableStart = '['
|
|
||||||
tableEnd = ']'
|
|
||||||
arrayTableStart = '['
|
|
||||||
arrayTableEnd = ']'
|
|
||||||
tableSep = '.'
|
|
||||||
keySep = '='
|
|
||||||
arrayStart = '['
|
|
||||||
arrayEnd = ']'
|
|
||||||
commentStart = '#'
|
|
||||||
stringStart = '"'
|
|
||||||
stringEnd = '"'
|
|
||||||
rawStringStart = '\''
|
|
||||||
rawStringEnd = '\''
|
|
||||||
inlineTableStart = '{'
|
|
||||||
inlineTableEnd = '}'
|
|
||||||
)
|
|
||||||
|
|
||||||
type stateFn func(lx *lexer) stateFn
|
type stateFn func(lx *lexer) stateFn
|
||||||
|
|
||||||
type lexer struct {
|
func (p Position) String() string {
|
||||||
input string
|
return fmt.Sprintf("at line %d; start %d; length %d", p.Line, p.Start, p.Len)
|
||||||
start int
|
}
|
||||||
pos int
|
|
||||||
line int
|
|
||||||
state stateFn
|
|
||||||
items chan item
|
|
||||||
|
|
||||||
// Allow for backing up up to four runes.
|
type lexer struct {
|
||||||
// This is necessary because TOML contains 3-rune tokens (""" and ''').
|
input string
|
||||||
|
start int
|
||||||
|
pos int
|
||||||
|
line int
|
||||||
|
state stateFn
|
||||||
|
items chan item
|
||||||
|
tomlNext bool
|
||||||
|
esc bool
|
||||||
|
|
||||||
|
// Allow for backing up up to 4 runes. This is necessary because TOML
|
||||||
|
// contains 3-rune tokens (""" and ''').
|
||||||
prevWidths [4]int
|
prevWidths [4]int
|
||||||
nprev int // how many of prevWidths are in use
|
nprev int // how many of prevWidths are in use
|
||||||
// If we emit an eof, we can still back up, but it is not OK to call
|
atEOF bool // If we emit an eof, we can still back up, but it is not OK to call next again.
|
||||||
// next again.
|
|
||||||
atEOF bool
|
|
||||||
|
|
||||||
// A stack of state functions used to maintain context.
|
// A stack of state functions used to maintain context.
|
||||||
// The idea is to reuse parts of the state machine in various places.
|
//
|
||||||
// For example, values can appear at the top level or within arbitrarily
|
// The idea is to reuse parts of the state machine in various places. For
|
||||||
// nested arrays. The last state on the stack is used after a value has
|
// example, values can appear at the top level or within arbitrarily nested
|
||||||
// been lexed. Similarly for comments.
|
// arrays. The last state on the stack is used after a value has been lexed.
|
||||||
|
// Similarly for comments.
|
||||||
stack []stateFn
|
stack []stateFn
|
||||||
}
|
}
|
||||||
|
|
||||||
type item struct {
|
type item struct {
|
||||||
typ itemType
|
typ itemType
|
||||||
val string
|
val string
|
||||||
line int
|
err error
|
||||||
|
pos Position
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lx *lexer) nextItem() item {
|
func (lx *lexer) nextItem() item {
|
||||||
|
@ -96,18 +85,19 @@ func (lx *lexer) nextItem() item {
|
||||||
return item
|
return item
|
||||||
default:
|
default:
|
||||||
lx.state = lx.state(lx)
|
lx.state = lx.state(lx)
|
||||||
//fmt.Printf(" STATE %-24s current: %-10q stack: %s\n", lx.state, lx.current(), lx.stack)
|
//fmt.Printf(" STATE %-24s current: %-10s stack: %s\n", lx.state, lx.current(), lx.stack)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func lex(input string) *lexer {
|
func lex(input string, tomlNext bool) *lexer {
|
||||||
lx := &lexer{
|
lx := &lexer{
|
||||||
input: input,
|
input: input,
|
||||||
state: lexTop,
|
state: lexTop,
|
||||||
line: 1,
|
items: make(chan item, 10),
|
||||||
items: make(chan item, 10),
|
stack: make([]stateFn, 0, 10),
|
||||||
stack: make([]stateFn, 0, 10),
|
line: 1,
|
||||||
|
tomlNext: tomlNext,
|
||||||
}
|
}
|
||||||
return lx
|
return lx
|
||||||
}
|
}
|
||||||
|
@ -129,13 +119,30 @@ func (lx *lexer) current() string {
|
||||||
return lx.input[lx.start:lx.pos]
|
return lx.input[lx.start:lx.pos]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (lx lexer) getPos() Position {
|
||||||
|
p := Position{
|
||||||
|
Line: lx.line,
|
||||||
|
Start: lx.start,
|
||||||
|
Len: lx.pos - lx.start,
|
||||||
|
}
|
||||||
|
if p.Len <= 0 {
|
||||||
|
p.Len = 1
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
func (lx *lexer) emit(typ itemType) {
|
func (lx *lexer) emit(typ itemType) {
|
||||||
lx.items <- item{typ, lx.current(), lx.line}
|
// Needed for multiline strings ending with an incomplete UTF-8 sequence.
|
||||||
|
if lx.start > lx.pos {
|
||||||
|
lx.error(errLexUTF8{lx.input[lx.pos]})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lx.items <- item{typ: typ, pos: lx.getPos(), val: lx.current()}
|
||||||
lx.start = lx.pos
|
lx.start = lx.pos
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lx *lexer) emitTrim(typ itemType) {
|
func (lx *lexer) emitTrim(typ itemType) {
|
||||||
lx.items <- item{typ, strings.TrimSpace(lx.current()), lx.line}
|
lx.items <- item{typ: typ, pos: lx.getPos(), val: strings.TrimSpace(lx.current())}
|
||||||
lx.start = lx.pos
|
lx.start = lx.pos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -159,8 +166,14 @@ func (lx *lexer) next() (r rune) {
|
||||||
}
|
}
|
||||||
|
|
||||||
r, w := utf8.DecodeRuneInString(lx.input[lx.pos:])
|
r, w := utf8.DecodeRuneInString(lx.input[lx.pos:])
|
||||||
if r == utf8.RuneError {
|
if r == utf8.RuneError && w == 1 {
|
||||||
lx.errorf("invalid UTF-8 byte at position %d (line %d): 0x%02x", lx.pos, lx.line, lx.input[lx.pos])
|
lx.error(errLexUTF8{lx.input[lx.pos]})
|
||||||
|
return utf8.RuneError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: don't use peek() here, as this calls next().
|
||||||
|
if isControl(r) || (r == '\r' && (len(lx.input)-1 == lx.pos || lx.input[lx.pos+1] != '\n')) {
|
||||||
|
lx.errorControlChar(r)
|
||||||
return utf8.RuneError
|
return utf8.RuneError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,6 +201,7 @@ func (lx *lexer) backup() {
|
||||||
lx.prevWidths[1] = lx.prevWidths[2]
|
lx.prevWidths[1] = lx.prevWidths[2]
|
||||||
lx.prevWidths[2] = lx.prevWidths[3]
|
lx.prevWidths[2] = lx.prevWidths[3]
|
||||||
lx.nprev--
|
lx.nprev--
|
||||||
|
|
||||||
lx.pos -= w
|
lx.pos -= w
|
||||||
if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' {
|
if lx.pos < len(lx.input) && lx.input[lx.pos] == '\n' {
|
||||||
lx.line--
|
lx.line--
|
||||||
|
@ -223,18 +237,58 @@ func (lx *lexer) skip(pred func(rune) bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// errorf stops all lexing by emitting an error and returning `nil`.
|
// error stops all lexing by emitting an error and returning `nil`.
|
||||||
|
//
|
||||||
// Note that any value that is a character is escaped if it's a special
|
// Note that any value that is a character is escaped if it's a special
|
||||||
// character (newlines, tabs, etc.).
|
// character (newlines, tabs, etc.).
|
||||||
func (lx *lexer) errorf(format string, values ...interface{}) stateFn {
|
func (lx *lexer) error(err error) stateFn {
|
||||||
lx.items <- item{
|
if lx.atEOF {
|
||||||
itemError,
|
return lx.errorPrevLine(err)
|
||||||
fmt.Sprintf(format, values...),
|
|
||||||
lx.line,
|
|
||||||
}
|
}
|
||||||
|
lx.items <- item{typ: itemError, pos: lx.getPos(), err: err}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// errorfPrevline is like error(), but sets the position to the last column of
|
||||||
|
// the previous line.
|
||||||
|
//
|
||||||
|
// This is so that unexpected EOF or NL errors don't show on a new blank line.
|
||||||
|
func (lx *lexer) errorPrevLine(err error) stateFn {
|
||||||
|
pos := lx.getPos()
|
||||||
|
pos.Line--
|
||||||
|
pos.Len = 1
|
||||||
|
pos.Start = lx.pos - 1
|
||||||
|
lx.items <- item{typ: itemError, pos: pos, err: err}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorPos is like error(), but allows explicitly setting the position.
|
||||||
|
func (lx *lexer) errorPos(start, length int, err error) stateFn {
|
||||||
|
pos := lx.getPos()
|
||||||
|
pos.Start = start
|
||||||
|
pos.Len = length
|
||||||
|
lx.items <- item{typ: itemError, pos: pos, err: err}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// errorf is like error, and creates a new error.
|
||||||
|
func (lx *lexer) errorf(format string, values ...any) stateFn {
|
||||||
|
if lx.atEOF {
|
||||||
|
pos := lx.getPos()
|
||||||
|
pos.Line--
|
||||||
|
pos.Len = 1
|
||||||
|
pos.Start = lx.pos - 1
|
||||||
|
lx.items <- item{typ: itemError, pos: pos, err: fmt.Errorf(format, values...)}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lx.items <- item{typ: itemError, pos: lx.getPos(), err: fmt.Errorf(format, values...)}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lx *lexer) errorControlChar(cc rune) stateFn {
|
||||||
|
return lx.errorPos(lx.pos-1, 1, errLexControl{cc})
|
||||||
|
}
|
||||||
|
|
||||||
// lexTop consumes elements at the top level of TOML data.
|
// lexTop consumes elements at the top level of TOML data.
|
||||||
func lexTop(lx *lexer) stateFn {
|
func lexTop(lx *lexer) stateFn {
|
||||||
r := lx.next()
|
r := lx.next()
|
||||||
|
@ -242,10 +296,10 @@ func lexTop(lx *lexer) stateFn {
|
||||||
return lexSkip(lx, lexTop)
|
return lexSkip(lx, lexTop)
|
||||||
}
|
}
|
||||||
switch r {
|
switch r {
|
||||||
case commentStart:
|
case '#':
|
||||||
lx.push(lexTop)
|
lx.push(lexTop)
|
||||||
return lexCommentStart
|
return lexCommentStart
|
||||||
case tableStart:
|
case '[':
|
||||||
return lexTableStart
|
return lexTableStart
|
||||||
case eof:
|
case eof:
|
||||||
if lx.pos > lx.start {
|
if lx.pos > lx.start {
|
||||||
|
@ -268,7 +322,7 @@ func lexTop(lx *lexer) stateFn {
|
||||||
func lexTopEnd(lx *lexer) stateFn {
|
func lexTopEnd(lx *lexer) stateFn {
|
||||||
r := lx.next()
|
r := lx.next()
|
||||||
switch {
|
switch {
|
||||||
case r == commentStart:
|
case r == '#':
|
||||||
// a comment will read to a newline for us.
|
// a comment will read to a newline for us.
|
||||||
lx.push(lexTop)
|
lx.push(lexTop)
|
||||||
return lexCommentStart
|
return lexCommentStart
|
||||||
|
@ -281,9 +335,7 @@ func lexTopEnd(lx *lexer) stateFn {
|
||||||
lx.emit(itemEOF)
|
lx.emit(itemEOF)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return lx.errorf(
|
return lx.errorf("expected a top-level item to end with a newline, comment, or EOF, but got %q instead", r)
|
||||||
"expected a top-level item to end with a newline, comment, or EOF, but got %q instead",
|
|
||||||
r)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// lexTable lexes the beginning of a table. Namely, it makes sure that
|
// lexTable lexes the beginning of a table. Namely, it makes sure that
|
||||||
|
@ -292,7 +344,7 @@ func lexTopEnd(lx *lexer) stateFn {
|
||||||
// It also handles the case that this is an item in an array of tables.
|
// It also handles the case that this is an item in an array of tables.
|
||||||
// e.g., '[[name]]'.
|
// e.g., '[[name]]'.
|
||||||
func lexTableStart(lx *lexer) stateFn {
|
func lexTableStart(lx *lexer) stateFn {
|
||||||
if lx.peek() == arrayTableStart {
|
if lx.peek() == '[' {
|
||||||
lx.next()
|
lx.next()
|
||||||
lx.emit(itemArrayTableStart)
|
lx.emit(itemArrayTableStart)
|
||||||
lx.push(lexArrayTableEnd)
|
lx.push(lexArrayTableEnd)
|
||||||
|
@ -309,10 +361,8 @@ func lexTableEnd(lx *lexer) stateFn {
|
||||||
}
|
}
|
||||||
|
|
||||||
func lexArrayTableEnd(lx *lexer) stateFn {
|
func lexArrayTableEnd(lx *lexer) stateFn {
|
||||||
if r := lx.next(); r != arrayTableEnd {
|
if r := lx.next(); r != ']' {
|
||||||
return lx.errorf(
|
return lx.errorf("expected end of table array name delimiter ']', but got %q instead", r)
|
||||||
"expected end of table array name delimiter %q, but got %q instead",
|
|
||||||
arrayTableEnd, r)
|
|
||||||
}
|
}
|
||||||
lx.emit(itemArrayTableEnd)
|
lx.emit(itemArrayTableEnd)
|
||||||
return lexTopEnd
|
return lexTopEnd
|
||||||
|
@ -321,11 +371,11 @@ func lexArrayTableEnd(lx *lexer) stateFn {
|
||||||
func lexTableNameStart(lx *lexer) stateFn {
|
func lexTableNameStart(lx *lexer) stateFn {
|
||||||
lx.skip(isWhitespace)
|
lx.skip(isWhitespace)
|
||||||
switch r := lx.peek(); {
|
switch r := lx.peek(); {
|
||||||
case r == tableEnd || r == eof:
|
case r == ']' || r == eof:
|
||||||
return lx.errorf("unexpected end of table name (table names cannot be empty)")
|
return lx.errorf("unexpected end of table name (table names cannot be empty)")
|
||||||
case r == tableSep:
|
case r == '.':
|
||||||
return lx.errorf("unexpected table separator (table names cannot be empty)")
|
return lx.errorf("unexpected table separator (table names cannot be empty)")
|
||||||
case r == stringStart || r == rawStringStart:
|
case r == '"' || r == '\'':
|
||||||
lx.ignore()
|
lx.ignore()
|
||||||
lx.push(lexTableNameEnd)
|
lx.push(lexTableNameEnd)
|
||||||
return lexQuotedName
|
return lexQuotedName
|
||||||
|
@ -342,10 +392,10 @@ func lexTableNameEnd(lx *lexer) stateFn {
|
||||||
switch r := lx.next(); {
|
switch r := lx.next(); {
|
||||||
case isWhitespace(r):
|
case isWhitespace(r):
|
||||||
return lexTableNameEnd
|
return lexTableNameEnd
|
||||||
case r == tableSep:
|
case r == '.':
|
||||||
lx.ignore()
|
lx.ignore()
|
||||||
return lexTableNameStart
|
return lexTableNameStart
|
||||||
case r == tableEnd:
|
case r == ']':
|
||||||
return lx.pop()
|
return lx.pop()
|
||||||
default:
|
default:
|
||||||
return lx.errorf("expected '.' or ']' to end table name, but got %q instead", r)
|
return lx.errorf("expected '.' or ']' to end table name, but got %q instead", r)
|
||||||
|
@ -360,7 +410,7 @@ func lexTableNameEnd(lx *lexer) stateFn {
|
||||||
// Lexes only one part, e.g. only 'a' inside 'a.b'.
|
// Lexes only one part, e.g. only 'a' inside 'a.b'.
|
||||||
func lexBareName(lx *lexer) stateFn {
|
func lexBareName(lx *lexer) stateFn {
|
||||||
r := lx.next()
|
r := lx.next()
|
||||||
if isBareKeyChar(r) {
|
if isBareKeyChar(r, lx.tomlNext) {
|
||||||
return lexBareName
|
return lexBareName
|
||||||
}
|
}
|
||||||
lx.backup()
|
lx.backup()
|
||||||
|
@ -379,10 +429,10 @@ func lexQuotedName(lx *lexer) stateFn {
|
||||||
switch {
|
switch {
|
||||||
case isWhitespace(r):
|
case isWhitespace(r):
|
||||||
return lexSkip(lx, lexValue)
|
return lexSkip(lx, lexValue)
|
||||||
case r == stringStart:
|
case r == '"':
|
||||||
lx.ignore() // ignore the '"'
|
lx.ignore() // ignore the '"'
|
||||||
return lexString
|
return lexString
|
||||||
case r == rawStringStart:
|
case r == '\'':
|
||||||
lx.ignore() // ignore the "'"
|
lx.ignore() // ignore the "'"
|
||||||
return lexRawString
|
return lexRawString
|
||||||
case r == eof:
|
case r == eof:
|
||||||
|
@ -400,7 +450,7 @@ func lexKeyStart(lx *lexer) stateFn {
|
||||||
return lx.errorf("unexpected '=': key name appears blank")
|
return lx.errorf("unexpected '=': key name appears blank")
|
||||||
case r == '.':
|
case r == '.':
|
||||||
return lx.errorf("unexpected '.': keys cannot start with a '.'")
|
return lx.errorf("unexpected '.': keys cannot start with a '.'")
|
||||||
case r == stringStart || r == rawStringStart:
|
case r == '"' || r == '\'':
|
||||||
lx.ignore()
|
lx.ignore()
|
||||||
fallthrough
|
fallthrough
|
||||||
default: // Bare key
|
default: // Bare key
|
||||||
|
@ -416,7 +466,7 @@ func lexKeyNameStart(lx *lexer) stateFn {
|
||||||
return lx.errorf("unexpected '='")
|
return lx.errorf("unexpected '='")
|
||||||
case r == '.':
|
case r == '.':
|
||||||
return lx.errorf("unexpected '.'")
|
return lx.errorf("unexpected '.'")
|
||||||
case r == stringStart || r == rawStringStart:
|
case r == '"' || r == '\'':
|
||||||
lx.ignore()
|
lx.ignore()
|
||||||
lx.push(lexKeyEnd)
|
lx.push(lexKeyEnd)
|
||||||
return lexQuotedName
|
return lexQuotedName
|
||||||
|
@ -434,7 +484,7 @@ func lexKeyEnd(lx *lexer) stateFn {
|
||||||
case isWhitespace(r):
|
case isWhitespace(r):
|
||||||
return lexSkip(lx, lexKeyEnd)
|
return lexSkip(lx, lexKeyEnd)
|
||||||
case r == eof:
|
case r == eof:
|
||||||
return lx.errorf("unexpected EOF; expected key separator %q", keySep)
|
return lx.errorf("unexpected EOF; expected key separator '='")
|
||||||
case r == '.':
|
case r == '.':
|
||||||
lx.ignore()
|
lx.ignore()
|
||||||
return lexKeyNameStart
|
return lexKeyNameStart
|
||||||
|
@ -461,17 +511,17 @@ func lexValue(lx *lexer) stateFn {
|
||||||
return lexNumberOrDateStart
|
return lexNumberOrDateStart
|
||||||
}
|
}
|
||||||
switch r {
|
switch r {
|
||||||
case arrayStart:
|
case '[':
|
||||||
lx.ignore()
|
lx.ignore()
|
||||||
lx.emit(itemArray)
|
lx.emit(itemArray)
|
||||||
return lexArrayValue
|
return lexArrayValue
|
||||||
case inlineTableStart:
|
case '{':
|
||||||
lx.ignore()
|
lx.ignore()
|
||||||
lx.emit(itemInlineTableStart)
|
lx.emit(itemInlineTableStart)
|
||||||
return lexInlineTableValue
|
return lexInlineTableValue
|
||||||
case stringStart:
|
case '"':
|
||||||
if lx.accept(stringStart) {
|
if lx.accept('"') {
|
||||||
if lx.accept(stringStart) {
|
if lx.accept('"') {
|
||||||
lx.ignore() // Ignore """
|
lx.ignore() // Ignore """
|
||||||
return lexMultilineString
|
return lexMultilineString
|
||||||
}
|
}
|
||||||
|
@ -479,9 +529,9 @@ func lexValue(lx *lexer) stateFn {
|
||||||
}
|
}
|
||||||
lx.ignore() // ignore the '"'
|
lx.ignore() // ignore the '"'
|
||||||
return lexString
|
return lexString
|
||||||
case rawStringStart:
|
case '\'':
|
||||||
if lx.accept(rawStringStart) {
|
if lx.accept('\'') {
|
||||||
if lx.accept(rawStringStart) {
|
if lx.accept('\'') {
|
||||||
lx.ignore() // Ignore """
|
lx.ignore() // Ignore """
|
||||||
return lexMultilineRawString
|
return lexMultilineRawString
|
||||||
}
|
}
|
||||||
|
@ -520,14 +570,12 @@ func lexArrayValue(lx *lexer) stateFn {
|
||||||
switch {
|
switch {
|
||||||
case isWhitespace(r) || isNL(r):
|
case isWhitespace(r) || isNL(r):
|
||||||
return lexSkip(lx, lexArrayValue)
|
return lexSkip(lx, lexArrayValue)
|
||||||
case r == commentStart:
|
case r == '#':
|
||||||
lx.push(lexArrayValue)
|
lx.push(lexArrayValue)
|
||||||
return lexCommentStart
|
return lexCommentStart
|
||||||
case r == comma:
|
case r == ',':
|
||||||
return lx.errorf("unexpected comma")
|
return lx.errorf("unexpected comma")
|
||||||
case r == arrayEnd:
|
case r == ']':
|
||||||
// NOTE(caleb): The spec isn't clear about whether you can have
|
|
||||||
// a trailing comma or not, so we'll allow it.
|
|
||||||
return lexArrayEnd
|
return lexArrayEnd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -540,22 +588,20 @@ func lexArrayValue(lx *lexer) stateFn {
|
||||||
// the next value (or the end of the array): it ignores whitespace and newlines
|
// the next value (or the end of the array): it ignores whitespace and newlines
|
||||||
// and expects either a ',' or a ']'.
|
// and expects either a ',' or a ']'.
|
||||||
func lexArrayValueEnd(lx *lexer) stateFn {
|
func lexArrayValueEnd(lx *lexer) stateFn {
|
||||||
r := lx.next()
|
switch r := lx.next(); {
|
||||||
switch {
|
|
||||||
case isWhitespace(r) || isNL(r):
|
case isWhitespace(r) || isNL(r):
|
||||||
return lexSkip(lx, lexArrayValueEnd)
|
return lexSkip(lx, lexArrayValueEnd)
|
||||||
case r == commentStart:
|
case r == '#':
|
||||||
lx.push(lexArrayValueEnd)
|
lx.push(lexArrayValueEnd)
|
||||||
return lexCommentStart
|
return lexCommentStart
|
||||||
case r == comma:
|
case r == ',':
|
||||||
lx.ignore()
|
lx.ignore()
|
||||||
return lexArrayValue // move on to the next value
|
return lexArrayValue // move on to the next value
|
||||||
case r == arrayEnd:
|
case r == ']':
|
||||||
return lexArrayEnd
|
return lexArrayEnd
|
||||||
|
default:
|
||||||
|
return lx.errorf("expected a comma (',') or array terminator (']'), but got %s", runeOrEOF(r))
|
||||||
}
|
}
|
||||||
return lx.errorf(
|
|
||||||
"expected a comma or array terminator %q, but got %s instead",
|
|
||||||
arrayEnd, runeOrEOF(r))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// lexArrayEnd finishes the lexing of an array.
|
// lexArrayEnd finishes the lexing of an array.
|
||||||
|
@ -574,13 +620,16 @@ func lexInlineTableValue(lx *lexer) stateFn {
|
||||||
case isWhitespace(r):
|
case isWhitespace(r):
|
||||||
return lexSkip(lx, lexInlineTableValue)
|
return lexSkip(lx, lexInlineTableValue)
|
||||||
case isNL(r):
|
case isNL(r):
|
||||||
return lx.errorf("newlines not allowed within inline tables")
|
if lx.tomlNext {
|
||||||
case r == commentStart:
|
return lexSkip(lx, lexInlineTableValue)
|
||||||
|
}
|
||||||
|
return lx.errorPrevLine(errLexInlineTableNL{})
|
||||||
|
case r == '#':
|
||||||
lx.push(lexInlineTableValue)
|
lx.push(lexInlineTableValue)
|
||||||
return lexCommentStart
|
return lexCommentStart
|
||||||
case r == comma:
|
case r == ',':
|
||||||
return lx.errorf("unexpected comma")
|
return lx.errorf("unexpected comma")
|
||||||
case r == inlineTableEnd:
|
case r == '}':
|
||||||
return lexInlineTableEnd
|
return lexInlineTableEnd
|
||||||
}
|
}
|
||||||
lx.backup()
|
lx.backup()
|
||||||
|
@ -596,23 +645,27 @@ func lexInlineTableValueEnd(lx *lexer) stateFn {
|
||||||
case isWhitespace(r):
|
case isWhitespace(r):
|
||||||
return lexSkip(lx, lexInlineTableValueEnd)
|
return lexSkip(lx, lexInlineTableValueEnd)
|
||||||
case isNL(r):
|
case isNL(r):
|
||||||
return lx.errorf("newlines not allowed within inline tables")
|
if lx.tomlNext {
|
||||||
case r == commentStart:
|
return lexSkip(lx, lexInlineTableValueEnd)
|
||||||
|
}
|
||||||
|
return lx.errorPrevLine(errLexInlineTableNL{})
|
||||||
|
case r == '#':
|
||||||
lx.push(lexInlineTableValueEnd)
|
lx.push(lexInlineTableValueEnd)
|
||||||
return lexCommentStart
|
return lexCommentStart
|
||||||
case r == comma:
|
case r == ',':
|
||||||
lx.ignore()
|
lx.ignore()
|
||||||
lx.skip(isWhitespace)
|
lx.skip(isWhitespace)
|
||||||
if lx.peek() == '}' {
|
if lx.peek() == '}' {
|
||||||
|
if lx.tomlNext {
|
||||||
|
return lexInlineTableValueEnd
|
||||||
|
}
|
||||||
return lx.errorf("trailing comma not allowed in inline tables")
|
return lx.errorf("trailing comma not allowed in inline tables")
|
||||||
}
|
}
|
||||||
return lexInlineTableValue
|
return lexInlineTableValue
|
||||||
case r == inlineTableEnd:
|
case r == '}':
|
||||||
return lexInlineTableEnd
|
return lexInlineTableEnd
|
||||||
default:
|
default:
|
||||||
return lx.errorf(
|
return lx.errorf("expected a comma or an inline table terminator '}', but got %s instead", runeOrEOF(r))
|
||||||
"expected a comma or an inline table terminator %q, but got %s instead",
|
|
||||||
inlineTableEnd, runeOrEOF(r))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -638,16 +691,19 @@ func lexString(lx *lexer) stateFn {
|
||||||
switch {
|
switch {
|
||||||
case r == eof:
|
case r == eof:
|
||||||
return lx.errorf(`unexpected EOF; expected '"'`)
|
return lx.errorf(`unexpected EOF; expected '"'`)
|
||||||
case isControl(r) || r == '\r':
|
|
||||||
return lx.errorf("control characters are not allowed inside strings: '0x%02x'", r)
|
|
||||||
case isNL(r):
|
case isNL(r):
|
||||||
return lx.errorf("strings cannot contain newlines")
|
return lx.errorPrevLine(errLexStringNL{})
|
||||||
case r == '\\':
|
case r == '\\':
|
||||||
lx.push(lexString)
|
lx.push(lexString)
|
||||||
return lexStringEscape
|
return lexStringEscape
|
||||||
case r == stringEnd:
|
case r == '"':
|
||||||
lx.backup()
|
lx.backup()
|
||||||
lx.emit(itemString)
|
if lx.esc {
|
||||||
|
lx.esc = false
|
||||||
|
lx.emit(itemStringEsc)
|
||||||
|
} else {
|
||||||
|
lx.emit(itemString)
|
||||||
|
}
|
||||||
lx.next()
|
lx.next()
|
||||||
lx.ignore()
|
lx.ignore()
|
||||||
return lx.pop()
|
return lx.pop()
|
||||||
|
@ -660,26 +716,33 @@ func lexString(lx *lexer) stateFn {
|
||||||
func lexMultilineString(lx *lexer) stateFn {
|
func lexMultilineString(lx *lexer) stateFn {
|
||||||
r := lx.next()
|
r := lx.next()
|
||||||
switch r {
|
switch r {
|
||||||
|
default:
|
||||||
|
return lexMultilineString
|
||||||
case eof:
|
case eof:
|
||||||
return lx.errorf(`unexpected EOF; expected '"""'`)
|
return lx.errorf(`unexpected EOF; expected '"""'`)
|
||||||
case '\r':
|
|
||||||
if lx.peek() != '\n' {
|
|
||||||
return lx.errorf("control characters are not allowed inside strings: '0x%02x'", r)
|
|
||||||
}
|
|
||||||
return lexMultilineString
|
|
||||||
case '\\':
|
case '\\':
|
||||||
return lexMultilineStringEscape
|
return lexMultilineStringEscape
|
||||||
case stringEnd:
|
case '"':
|
||||||
/// Found " → try to read two more "".
|
/// Found " → try to read two more "".
|
||||||
if lx.accept(stringEnd) {
|
if lx.accept('"') {
|
||||||
if lx.accept(stringEnd) {
|
if lx.accept('"') {
|
||||||
/// Peek ahead: the string can contain " and "", including at the
|
/// Peek ahead: the string can contain " and "", including at the
|
||||||
/// end: """str"""""
|
/// end: """str"""""
|
||||||
/// 6 or more at the end, however, is an error.
|
/// 6 or more at the end, however, is an error.
|
||||||
if lx.peek() == stringEnd {
|
if lx.peek() == '"' {
|
||||||
/// Check if we already lexed 5 's; if so we have 6 now, and
|
/// Check if we already lexed 5 's; if so we have 6 now, and
|
||||||
/// that's just too many man!
|
/// that's just too many man!
|
||||||
if strings.HasSuffix(lx.current(), `"""""`) {
|
///
|
||||||
|
/// Second check is for the edge case:
|
||||||
|
///
|
||||||
|
/// two quotes allowed.
|
||||||
|
/// vv
|
||||||
|
/// """lol \""""""
|
||||||
|
/// ^^ ^^^---- closing three
|
||||||
|
/// escaped
|
||||||
|
///
|
||||||
|
/// But ugly, but it works
|
||||||
|
if strings.HasSuffix(lx.current(), `"""""`) && !strings.HasSuffix(lx.current(), `\"""""`) {
|
||||||
return lx.errorf(`unexpected '""""""'`)
|
return lx.errorf(`unexpected '""""""'`)
|
||||||
}
|
}
|
||||||
lx.backup()
|
lx.backup()
|
||||||
|
@ -690,6 +753,7 @@ func lexMultilineString(lx *lexer) stateFn {
|
||||||
lx.backup() /// backup: don't include the """ in the item.
|
lx.backup() /// backup: don't include the """ in the item.
|
||||||
lx.backup()
|
lx.backup()
|
||||||
lx.backup()
|
lx.backup()
|
||||||
|
lx.esc = false
|
||||||
lx.emit(itemMultilineString)
|
lx.emit(itemMultilineString)
|
||||||
lx.next() /// Read over ''' again and discard it.
|
lx.next() /// Read over ''' again and discard it.
|
||||||
lx.next()
|
lx.next()
|
||||||
|
@ -699,12 +763,8 @@ func lexMultilineString(lx *lexer) stateFn {
|
||||||
}
|
}
|
||||||
lx.backup()
|
lx.backup()
|
||||||
}
|
}
|
||||||
|
return lexMultilineString
|
||||||
}
|
}
|
||||||
|
|
||||||
if isControl(r) {
|
|
||||||
return lx.errorf("control characters are not allowed inside strings: '0x%02x'", r)
|
|
||||||
}
|
|
||||||
return lexMultilineString
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// lexRawString consumes a raw string. Nothing can be escaped in such a string.
|
// lexRawString consumes a raw string. Nothing can be escaped in such a string.
|
||||||
|
@ -712,43 +772,39 @@ func lexMultilineString(lx *lexer) stateFn {
|
||||||
func lexRawString(lx *lexer) stateFn {
|
func lexRawString(lx *lexer) stateFn {
|
||||||
r := lx.next()
|
r := lx.next()
|
||||||
switch {
|
switch {
|
||||||
|
default:
|
||||||
|
return lexRawString
|
||||||
case r == eof:
|
case r == eof:
|
||||||
return lx.errorf(`unexpected EOF; expected "'"`)
|
return lx.errorf(`unexpected EOF; expected "'"`)
|
||||||
case isControl(r) || r == '\r':
|
|
||||||
return lx.errorf("control characters are not allowed inside strings: '0x%02x'", r)
|
|
||||||
case isNL(r):
|
case isNL(r):
|
||||||
return lx.errorf("strings cannot contain newlines")
|
return lx.errorPrevLine(errLexStringNL{})
|
||||||
case r == rawStringEnd:
|
case r == '\'':
|
||||||
lx.backup()
|
lx.backup()
|
||||||
lx.emit(itemRawString)
|
lx.emit(itemRawString)
|
||||||
lx.next()
|
lx.next()
|
||||||
lx.ignore()
|
lx.ignore()
|
||||||
return lx.pop()
|
return lx.pop()
|
||||||
}
|
}
|
||||||
return lexRawString
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// lexMultilineRawString consumes a raw string. Nothing can be escaped in such
|
// lexMultilineRawString consumes a raw string. Nothing can be escaped in such a
|
||||||
// a string. It assumes that the beginning "'''" has already been consumed and
|
// string. It assumes that the beginning triple-' has already been consumed and
|
||||||
// ignored.
|
// ignored.
|
||||||
func lexMultilineRawString(lx *lexer) stateFn {
|
func lexMultilineRawString(lx *lexer) stateFn {
|
||||||
r := lx.next()
|
r := lx.next()
|
||||||
switch r {
|
switch r {
|
||||||
|
default:
|
||||||
|
return lexMultilineRawString
|
||||||
case eof:
|
case eof:
|
||||||
return lx.errorf(`unexpected EOF; expected "'''"`)
|
return lx.errorf(`unexpected EOF; expected "'''"`)
|
||||||
case '\r':
|
case '\'':
|
||||||
if lx.peek() != '\n' {
|
|
||||||
return lx.errorf("control characters are not allowed inside strings: '0x%02x'", r)
|
|
||||||
}
|
|
||||||
return lexMultilineRawString
|
|
||||||
case rawStringEnd:
|
|
||||||
/// Found ' → try to read two more ''.
|
/// Found ' → try to read two more ''.
|
||||||
if lx.accept(rawStringEnd) {
|
if lx.accept('\'') {
|
||||||
if lx.accept(rawStringEnd) {
|
if lx.accept('\'') {
|
||||||
/// Peek ahead: the string can contain ' and '', including at the
|
/// Peek ahead: the string can contain ' and '', including at the
|
||||||
/// end: '''str'''''
|
/// end: '''str'''''
|
||||||
/// 6 or more at the end, however, is an error.
|
/// 6 or more at the end, however, is an error.
|
||||||
if lx.peek() == rawStringEnd {
|
if lx.peek() == '\'' {
|
||||||
/// Check if we already lexed 5 's; if so we have 6 now, and
|
/// Check if we already lexed 5 's; if so we have 6 now, and
|
||||||
/// that's just too many man!
|
/// that's just too many man!
|
||||||
if strings.HasSuffix(lx.current(), "'''''") {
|
if strings.HasSuffix(lx.current(), "'''''") {
|
||||||
|
@ -771,19 +827,14 @@ func lexMultilineRawString(lx *lexer) stateFn {
|
||||||
}
|
}
|
||||||
lx.backup()
|
lx.backup()
|
||||||
}
|
}
|
||||||
|
return lexMultilineRawString
|
||||||
}
|
}
|
||||||
|
|
||||||
if isControl(r) {
|
|
||||||
return lx.errorf("control characters are not allowed inside strings: '0x%02x'", r)
|
|
||||||
}
|
|
||||||
return lexMultilineRawString
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// lexMultilineStringEscape consumes an escaped character. It assumes that the
|
// lexMultilineStringEscape consumes an escaped character. It assumes that the
|
||||||
// preceding '\\' has already been consumed.
|
// preceding '\\' has already been consumed.
|
||||||
func lexMultilineStringEscape(lx *lexer) stateFn {
|
func lexMultilineStringEscape(lx *lexer) stateFn {
|
||||||
// Handle the special case first:
|
if isNL(lx.next()) { /// \ escaping newline.
|
||||||
if isNL(lx.next()) {
|
|
||||||
return lexMultilineString
|
return lexMultilineString
|
||||||
}
|
}
|
||||||
lx.backup()
|
lx.backup()
|
||||||
|
@ -792,8 +843,14 @@ func lexMultilineStringEscape(lx *lexer) stateFn {
|
||||||
}
|
}
|
||||||
|
|
||||||
func lexStringEscape(lx *lexer) stateFn {
|
func lexStringEscape(lx *lexer) stateFn {
|
||||||
|
lx.esc = true
|
||||||
r := lx.next()
|
r := lx.next()
|
||||||
switch r {
|
switch r {
|
||||||
|
case 'e':
|
||||||
|
if !lx.tomlNext {
|
||||||
|
return lx.error(errLexEscape{r})
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
case 'b':
|
case 'b':
|
||||||
fallthrough
|
fallthrough
|
||||||
case 't':
|
case 't':
|
||||||
|
@ -812,23 +869,36 @@ func lexStringEscape(lx *lexer) stateFn {
|
||||||
fallthrough
|
fallthrough
|
||||||
case '\\':
|
case '\\':
|
||||||
return lx.pop()
|
return lx.pop()
|
||||||
|
case 'x':
|
||||||
|
if !lx.tomlNext {
|
||||||
|
return lx.error(errLexEscape{r})
|
||||||
|
}
|
||||||
|
return lexHexEscape
|
||||||
case 'u':
|
case 'u':
|
||||||
return lexShortUnicodeEscape
|
return lexShortUnicodeEscape
|
||||||
case 'U':
|
case 'U':
|
||||||
return lexLongUnicodeEscape
|
return lexLongUnicodeEscape
|
||||||
}
|
}
|
||||||
return lx.errorf("invalid escape character %q; only the following escape characters are allowed: "+
|
return lx.error(errLexEscape{r})
|
||||||
`\b, \t, \n, \f, \r, \", \\, \uXXXX, and \UXXXXXXXX`, r)
|
}
|
||||||
|
|
||||||
|
func lexHexEscape(lx *lexer) stateFn {
|
||||||
|
var r rune
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
r = lx.next()
|
||||||
|
if !isHex(r) {
|
||||||
|
return lx.errorf(`expected two hexadecimal digits after '\x', but got %q instead`, lx.current())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lx.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
func lexShortUnicodeEscape(lx *lexer) stateFn {
|
func lexShortUnicodeEscape(lx *lexer) stateFn {
|
||||||
var r rune
|
var r rune
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
r = lx.next()
|
r = lx.next()
|
||||||
if !isHexadecimal(r) {
|
if !isHex(r) {
|
||||||
return lx.errorf(
|
return lx.errorf(`expected four hexadecimal digits after '\u', but got %q instead`, lx.current())
|
||||||
`expected four hexadecimal digits after '\u', but got %q instead`,
|
|
||||||
lx.current())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return lx.pop()
|
return lx.pop()
|
||||||
|
@ -838,10 +908,8 @@ func lexLongUnicodeEscape(lx *lexer) stateFn {
|
||||||
var r rune
|
var r rune
|
||||||
for i := 0; i < 8; i++ {
|
for i := 0; i < 8; i++ {
|
||||||
r = lx.next()
|
r = lx.next()
|
||||||
if !isHexadecimal(r) {
|
if !isHex(r) {
|
||||||
return lx.errorf(
|
return lx.errorf(`expected eight hexadecimal digits after '\U', but got %q instead`, lx.current())
|
||||||
`expected eight hexadecimal digits after '\U', but got %q instead`,
|
|
||||||
lx.current())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return lx.pop()
|
return lx.pop()
|
||||||
|
@ -908,7 +976,7 @@ func lexDatetime(lx *lexer) stateFn {
|
||||||
// lexHexInteger consumes a hexadecimal integer after seeing the '0x' prefix.
|
// lexHexInteger consumes a hexadecimal integer after seeing the '0x' prefix.
|
||||||
func lexHexInteger(lx *lexer) stateFn {
|
func lexHexInteger(lx *lexer) stateFn {
|
||||||
r := lx.next()
|
r := lx.next()
|
||||||
if isHexadecimal(r) {
|
if isHex(r) {
|
||||||
return lexHexInteger
|
return lexHexInteger
|
||||||
}
|
}
|
||||||
switch r {
|
switch r {
|
||||||
|
@ -1042,7 +1110,7 @@ func lexBaseNumberOrDate(lx *lexer) stateFn {
|
||||||
return lexOctalInteger
|
return lexOctalInteger
|
||||||
case 'x':
|
case 'x':
|
||||||
r = lx.peek()
|
r = lx.peek()
|
||||||
if !isHexadecimal(r) {
|
if !isHex(r) {
|
||||||
lx.errorf("not a hexidecimal number: '%s%c'", lx.current(), r)
|
lx.errorf("not a hexidecimal number: '%s%c'", lx.current(), r)
|
||||||
}
|
}
|
||||||
return lexHexInteger
|
return lexHexInteger
|
||||||
|
@ -1108,8 +1176,6 @@ func lexComment(lx *lexer) stateFn {
|
||||||
lx.backup()
|
lx.backup()
|
||||||
lx.emit(itemText)
|
lx.emit(itemText)
|
||||||
return lx.pop()
|
return lx.pop()
|
||||||
case isControl(r):
|
|
||||||
return lx.errorf("control characters are not allowed inside comments: '0x%02x'", r)
|
|
||||||
default:
|
default:
|
||||||
return lexComment
|
return lexComment
|
||||||
}
|
}
|
||||||
|
@ -1121,52 +1187,6 @@ func lexSkip(lx *lexer, nextState stateFn) stateFn {
|
||||||
return nextState
|
return nextState
|
||||||
}
|
}
|
||||||
|
|
||||||
// isWhitespace returns true if `r` is a whitespace character according
|
|
||||||
// to the spec.
|
|
||||||
func isWhitespace(r rune) bool {
|
|
||||||
return r == '\t' || r == ' '
|
|
||||||
}
|
|
||||||
|
|
||||||
func isNL(r rune) bool {
|
|
||||||
return r == '\n' || r == '\r'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Control characters except \n, \t
|
|
||||||
func isControl(r rune) bool {
|
|
||||||
switch r {
|
|
||||||
case '\t', '\r', '\n':
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
return (r >= 0x00 && r <= 0x1f) || r == 0x7f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isDigit(r rune) bool {
|
|
||||||
return r >= '0' && r <= '9'
|
|
||||||
}
|
|
||||||
|
|
||||||
func isHexadecimal(r rune) bool {
|
|
||||||
return (r >= '0' && r <= '9') ||
|
|
||||||
(r >= 'a' && r <= 'f') ||
|
|
||||||
(r >= 'A' && r <= 'F')
|
|
||||||
}
|
|
||||||
|
|
||||||
func isOctal(r rune) bool {
|
|
||||||
return r >= '0' && r <= '7'
|
|
||||||
}
|
|
||||||
|
|
||||||
func isBinary(r rune) bool {
|
|
||||||
return r == '0' || r == '1'
|
|
||||||
}
|
|
||||||
|
|
||||||
func isBareKeyChar(r rune) bool {
|
|
||||||
return (r >= 'A' && r <= 'Z') ||
|
|
||||||
(r >= 'a' && r <= 'z') ||
|
|
||||||
(r >= '0' && r <= '9') ||
|
|
||||||
r == '_' ||
|
|
||||||
r == '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s stateFn) String() string {
|
func (s stateFn) String() string {
|
||||||
name := runtime.FuncForPC(reflect.ValueOf(s).Pointer()).Name()
|
name := runtime.FuncForPC(reflect.ValueOf(s).Pointer()).Name()
|
||||||
if i := strings.LastIndexByte(name, '.'); i > -1 {
|
if i := strings.LastIndexByte(name, '.'); i > -1 {
|
||||||
|
@ -1188,7 +1208,7 @@ func (itype itemType) String() string {
|
||||||
return "EOF"
|
return "EOF"
|
||||||
case itemText:
|
case itemText:
|
||||||
return "Text"
|
return "Text"
|
||||||
case itemString, itemRawString, itemMultilineString, itemRawMultilineString:
|
case itemString, itemStringEsc, itemRawString, itemMultilineString, itemRawMultilineString:
|
||||||
return "String"
|
return "String"
|
||||||
case itemBool:
|
case itemBool:
|
||||||
return "Bool"
|
return "Bool"
|
||||||
|
@ -1221,5 +1241,41 @@ func (itype itemType) String() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (item item) String() string {
|
func (item item) String() string {
|
||||||
return fmt.Sprintf("(%s, %s)", item.typ.String(), item.val)
|
return fmt.Sprintf("(%s, %s)", item.typ, item.val)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWhitespace(r rune) bool { return r == '\t' || r == ' ' }
|
||||||
|
func isNL(r rune) bool { return r == '\n' || r == '\r' }
|
||||||
|
func isControl(r rune) bool { // Control characters except \t, \r, \n
|
||||||
|
switch r {
|
||||||
|
case '\t', '\r', '\n':
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return (r >= 0x00 && r <= 0x1f) || r == 0x7f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func isDigit(r rune) bool { return r >= '0' && r <= '9' }
|
||||||
|
func isBinary(r rune) bool { return r == '0' || r == '1' }
|
||||||
|
func isOctal(r rune) bool { return r >= '0' && r <= '7' }
|
||||||
|
func isHex(r rune) bool { return (r >= '0' && r <= '9') || (r|0x20 >= 'a' && r|0x20 <= 'f') }
|
||||||
|
func isBareKeyChar(r rune, tomlNext bool) bool {
|
||||||
|
if tomlNext {
|
||||||
|
return (r >= 'A' && r <= 'Z') ||
|
||||||
|
(r >= 'a' && r <= 'z') ||
|
||||||
|
(r >= '0' && r <= '9') ||
|
||||||
|
r == '_' || r == '-' ||
|
||||||
|
r == 0xb2 || r == 0xb3 || r == 0xb9 || (r >= 0xbc && r <= 0xbe) ||
|
||||||
|
(r >= 0xc0 && r <= 0xd6) || (r >= 0xd8 && r <= 0xf6) || (r >= 0xf8 && r <= 0x037d) ||
|
||||||
|
(r >= 0x037f && r <= 0x1fff) ||
|
||||||
|
(r >= 0x200c && r <= 0x200d) || (r >= 0x203f && r <= 0x2040) ||
|
||||||
|
(r >= 0x2070 && r <= 0x218f) || (r >= 0x2460 && r <= 0x24ff) ||
|
||||||
|
(r >= 0x2c00 && r <= 0x2fef) || (r >= 0x3001 && r <= 0xd7ff) ||
|
||||||
|
(r >= 0xf900 && r <= 0xfdcf) || (r >= 0xfdf0 && r <= 0xfffd) ||
|
||||||
|
(r >= 0x10000 && r <= 0xeffff)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (r >= 'A' && r <= 'Z') ||
|
||||||
|
(r >= 'a' && r <= 'z') ||
|
||||||
|
(r >= '0' && r <= '9') ||
|
||||||
|
r == '_' || r == '-'
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
package toml
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MetaData allows access to meta information about TOML data that's not
|
||||||
|
// accessible otherwise.
|
||||||
|
//
|
||||||
|
// It allows checking if a key is defined in the TOML data, whether any keys
|
||||||
|
// were undecoded, and the TOML type of a key.
|
||||||
|
type MetaData struct {
|
||||||
|
context Key // Used only during decoding.
|
||||||
|
|
||||||
|
keyInfo map[string]keyInfo
|
||||||
|
mapping map[string]any
|
||||||
|
keys []Key
|
||||||
|
decoded map[string]struct{}
|
||||||
|
data []byte // Input file; for errors.
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDefined reports if the key exists in the TOML data.
|
||||||
|
//
|
||||||
|
// The key should be specified hierarchically, for example to access the TOML
|
||||||
|
// key "a.b.c" you would use IsDefined("a", "b", "c"). Keys are case sensitive.
|
||||||
|
//
|
||||||
|
// Returns false for an empty key.
|
||||||
|
func (md *MetaData) IsDefined(key ...string) bool {
|
||||||
|
if len(key) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
hash map[string]any
|
||||||
|
ok bool
|
||||||
|
hashOrVal any = md.mapping
|
||||||
|
)
|
||||||
|
for _, k := range key {
|
||||||
|
if hash, ok = hashOrVal.(map[string]any); !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if hashOrVal, ok = hash[k]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns a string representation of the type of the key specified.
|
||||||
|
//
|
||||||
|
// Type will return the empty string if given an empty key or a key that does
|
||||||
|
// not exist. Keys are case sensitive.
|
||||||
|
func (md *MetaData) Type(key ...string) string {
|
||||||
|
if ki, ok := md.keyInfo[Key(key).String()]; ok {
|
||||||
|
return ki.tomlType.typeString()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys returns a slice of every key in the TOML data, including key groups.
|
||||||
|
//
|
||||||
|
// Each key is itself a slice, where the first element is the top of the
|
||||||
|
// hierarchy and the last is the most specific. The list will have the same
|
||||||
|
// order as the keys appeared in the TOML data.
|
||||||
|
//
|
||||||
|
// All keys returned are non-empty.
|
||||||
|
func (md *MetaData) Keys() []Key {
|
||||||
|
return md.keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undecoded returns all keys that have not been decoded in the order in which
|
||||||
|
// they appear in the original TOML document.
|
||||||
|
//
|
||||||
|
// This includes keys that haven't been decoded because of a [Primitive] value.
|
||||||
|
// Once the Primitive value is decoded, the keys will be considered decoded.
|
||||||
|
//
|
||||||
|
// Also note that decoding into an empty interface will result in no decoding,
|
||||||
|
// and so no keys will be considered decoded.
|
||||||
|
//
|
||||||
|
// In this sense, the Undecoded keys correspond to keys in the TOML document
|
||||||
|
// that do not have a concrete type in your representation.
|
||||||
|
func (md *MetaData) Undecoded() []Key {
|
||||||
|
undecoded := make([]Key, 0, len(md.keys))
|
||||||
|
for _, key := range md.keys {
|
||||||
|
if _, ok := md.decoded[key.String()]; !ok {
|
||||||
|
undecoded = append(undecoded, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undecoded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key represents any TOML key, including key groups. Use [MetaData.Keys] to get
|
||||||
|
// values of this type.
|
||||||
|
type Key []string
|
||||||
|
|
||||||
|
func (k Key) String() string {
|
||||||
|
// This is called quite often, so it's a bit funky to make it faster.
|
||||||
|
var b strings.Builder
|
||||||
|
b.Grow(len(k) * 25)
|
||||||
|
outer:
|
||||||
|
for i, kk := range k {
|
||||||
|
if i > 0 {
|
||||||
|
b.WriteByte('.')
|
||||||
|
}
|
||||||
|
if kk == "" {
|
||||||
|
b.WriteString(`""`)
|
||||||
|
} else {
|
||||||
|
for _, r := range kk {
|
||||||
|
// "Inline" isBareKeyChar
|
||||||
|
if !((r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' || r == '-') {
|
||||||
|
b.WriteByte('"')
|
||||||
|
b.WriteString(dblQuotedReplacer.Replace(kk))
|
||||||
|
b.WriteByte('"')
|
||||||
|
continue outer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteString(kk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k Key) maybeQuoted(i int) string {
|
||||||
|
if k[i] == "" {
|
||||||
|
return `""`
|
||||||
|
}
|
||||||
|
for _, r := range k[i] {
|
||||||
|
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' || r == '-' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return `"` + dblQuotedReplacer.Replace(k[i]) + `"`
|
||||||
|
}
|
||||||
|
return k[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Like append(), but only increase the cap by 1.
|
||||||
|
func (k Key) add(piece string) Key {
|
||||||
|
if cap(k) > len(k) {
|
||||||
|
return append(k, piece)
|
||||||
|
}
|
||||||
|
newKey := make(Key, len(k)+1)
|
||||||
|
copy(newKey, k)
|
||||||
|
newKey[len(k)] = piece
|
||||||
|
return newKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k Key) parent() Key { return k[:len(k)-1] } // all except the last piece.
|
||||||
|
func (k Key) last() string { return k[len(k)-1] } // last piece of this key.
|
|
@ -1,8 +1,9 @@
|
||||||
package toml
|
package toml
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -12,35 +13,32 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type parser struct {
|
type parser struct {
|
||||||
mapping map[string]interface{}
|
lx *lexer
|
||||||
types map[string]tomlType
|
context Key // Full key for the current hash in scope.
|
||||||
lx *lexer
|
currentKey string // Base key name for everything except hashes.
|
||||||
|
pos Position // Current position in the TOML file.
|
||||||
|
tomlNext bool
|
||||||
|
|
||||||
ordered []Key // List of keys in the order that they appear in the TOML data.
|
ordered []Key // List of keys in the order that they appear in the TOML data.
|
||||||
context Key // Full key for the current hash in scope.
|
|
||||||
currentKey string // Base key name for everything except hashes.
|
keyInfo map[string]keyInfo // Map keyname → info about the TOML key.
|
||||||
approxLine int // Rough approximation of line number
|
mapping map[string]any // Map keyname → key value.
|
||||||
implicits map[string]bool // Record implied keys (e.g. 'key.group.names').
|
implicits map[string]struct{} // Record implicit keys (e.g. "key.group.names").
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseError is used when a file can't be parsed: for example invalid integer
|
type keyInfo struct {
|
||||||
// literals, duplicate keys, etc.
|
pos Position
|
||||||
type ParseError struct {
|
tomlType tomlType
|
||||||
Message string
|
|
||||||
Line int
|
|
||||||
LastKey string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pe ParseError) Error() string {
|
|
||||||
return fmt.Sprintf("Near line %d (last key parsed '%s'): %s",
|
|
||||||
pe.Line, pe.LastKey, pe.Message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parse(data string) (p *parser, err error) {
|
func parse(data string) (p *parser, err error) {
|
||||||
|
_, tomlNext := os.LookupEnv("BURNTSUSHI_TOML_110")
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
var ok bool
|
if pErr, ok := r.(ParseError); ok {
|
||||||
if err, ok = r.(ParseError); ok {
|
pErr.input = data
|
||||||
|
err = pErr
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
panic(r)
|
panic(r)
|
||||||
|
@ -48,9 +46,13 @@ func parse(data string) (p *parser, err error) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Read over BOM; do this here as the lexer calls utf8.DecodeRuneInString()
|
// Read over BOM; do this here as the lexer calls utf8.DecodeRuneInString()
|
||||||
// which mangles stuff.
|
// which mangles stuff. UTF-16 BOM isn't strictly valid, but some tools add
|
||||||
if strings.HasPrefix(data, "\xff\xfe") || strings.HasPrefix(data, "\xfe\xff") {
|
// it anyway.
|
||||||
|
if strings.HasPrefix(data, "\xff\xfe") || strings.HasPrefix(data, "\xfe\xff") { // UTF-16
|
||||||
data = data[2:]
|
data = data[2:]
|
||||||
|
//lint:ignore S1017 https://github.com/dominikh/go-tools/issues/1447
|
||||||
|
} else if strings.HasPrefix(data, "\xef\xbb\xbf") { // UTF-8
|
||||||
|
data = data[3:]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Examine first few bytes for NULL bytes; this probably means it's a UTF-16
|
// Examine first few bytes for NULL bytes; this probably means it's a UTF-16
|
||||||
|
@ -60,16 +62,22 @@ func parse(data string) (p *parser, err error) {
|
||||||
if len(data) < 6 {
|
if len(data) < 6 {
|
||||||
ex = len(data)
|
ex = len(data)
|
||||||
}
|
}
|
||||||
if strings.ContainsRune(data[:ex], 0) {
|
if i := strings.IndexRune(data[:ex], 0); i > -1 {
|
||||||
return nil, errors.New("files cannot contain NULL bytes; probably using UTF-16; TOML files must be UTF-8")
|
return nil, ParseError{
|
||||||
|
Message: "files cannot contain NULL bytes; probably using UTF-16; TOML files must be UTF-8",
|
||||||
|
Position: Position{Line: 1, Start: i, Len: 1},
|
||||||
|
Line: 1,
|
||||||
|
input: data,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p = &parser{
|
p = &parser{
|
||||||
mapping: make(map[string]interface{}),
|
keyInfo: make(map[string]keyInfo),
|
||||||
types: make(map[string]tomlType),
|
mapping: make(map[string]any),
|
||||||
lx: lex(data),
|
lx: lex(data, tomlNext),
|
||||||
ordered: make([]Key, 0),
|
ordered: make([]Key, 0),
|
||||||
implicits: make(map[string]bool),
|
implicits: make(map[string]struct{}),
|
||||||
|
tomlNext: tomlNext,
|
||||||
}
|
}
|
||||||
for {
|
for {
|
||||||
item := p.next()
|
item := p.next()
|
||||||
|
@ -82,25 +90,58 @@ func parse(data string) (p *parser, err error) {
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) panicf(format string, v ...interface{}) {
|
func (p *parser) panicErr(it item, err error) {
|
||||||
msg := fmt.Sprintf(format, v...)
|
|
||||||
panic(ParseError{
|
panic(ParseError{
|
||||||
Message: msg,
|
err: err,
|
||||||
Line: p.approxLine,
|
Position: it.pos,
|
||||||
LastKey: p.current(),
|
Line: it.pos.Len,
|
||||||
|
LastKey: p.current(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) panicItemf(it item, format string, v ...any) {
|
||||||
|
panic(ParseError{
|
||||||
|
Message: fmt.Sprintf(format, v...),
|
||||||
|
Position: it.pos,
|
||||||
|
Line: it.pos.Len,
|
||||||
|
LastKey: p.current(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) panicf(format string, v ...any) {
|
||||||
|
panic(ParseError{
|
||||||
|
Message: fmt.Sprintf(format, v...),
|
||||||
|
Position: p.pos,
|
||||||
|
Line: p.pos.Line,
|
||||||
|
LastKey: p.current(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) next() item {
|
func (p *parser) next() item {
|
||||||
it := p.lx.nextItem()
|
it := p.lx.nextItem()
|
||||||
//fmt.Printf("ITEM %-18s line %-3d │ %q\n", it.typ, it.line, it.val)
|
//fmt.Printf("ITEM %-18s line %-3d │ %q\n", it.typ, it.pos.Line, it.val)
|
||||||
if it.typ == itemError {
|
if it.typ == itemError {
|
||||||
p.panicf("%s", it.val)
|
if it.err != nil {
|
||||||
|
panic(ParseError{
|
||||||
|
Position: it.pos,
|
||||||
|
Line: it.pos.Line,
|
||||||
|
LastKey: p.current(),
|
||||||
|
err: it.err,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
p.panicItemf(it, "%s", it.val)
|
||||||
}
|
}
|
||||||
return it
|
return it
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) bug(format string, v ...interface{}) {
|
func (p *parser) nextPos() item {
|
||||||
|
it := p.next()
|
||||||
|
p.pos = it.pos
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) bug(format string, v ...any) {
|
||||||
panic(fmt.Sprintf("BUG: "+format+"\n\n", v...))
|
panic(fmt.Sprintf("BUG: "+format+"\n\n", v...))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,11 +160,9 @@ func (p *parser) assertEqual(expected, got itemType) {
|
||||||
func (p *parser) topLevel(item item) {
|
func (p *parser) topLevel(item item) {
|
||||||
switch item.typ {
|
switch item.typ {
|
||||||
case itemCommentStart: // # ..
|
case itemCommentStart: // # ..
|
||||||
p.approxLine = item.line
|
|
||||||
p.expect(itemText)
|
p.expect(itemText)
|
||||||
case itemTableStart: // [ .. ]
|
case itemTableStart: // [ .. ]
|
||||||
name := p.next()
|
name := p.nextPos()
|
||||||
p.approxLine = name.line
|
|
||||||
|
|
||||||
var key Key
|
var key Key
|
||||||
for ; name.typ != itemTableEnd && name.typ != itemEOF; name = p.next() {
|
for ; name.typ != itemTableEnd && name.typ != itemEOF; name = p.next() {
|
||||||
|
@ -132,11 +171,10 @@ func (p *parser) topLevel(item item) {
|
||||||
p.assertEqual(itemTableEnd, name.typ)
|
p.assertEqual(itemTableEnd, name.typ)
|
||||||
|
|
||||||
p.addContext(key, false)
|
p.addContext(key, false)
|
||||||
p.setType("", tomlHash)
|
p.setType("", tomlHash, item.pos)
|
||||||
p.ordered = append(p.ordered, key)
|
p.ordered = append(p.ordered, key)
|
||||||
case itemArrayTableStart: // [[ .. ]]
|
case itemArrayTableStart: // [[ .. ]]
|
||||||
name := p.next()
|
name := p.nextPos()
|
||||||
p.approxLine = name.line
|
|
||||||
|
|
||||||
var key Key
|
var key Key
|
||||||
for ; name.typ != itemArrayTableEnd && name.typ != itemEOF; name = p.next() {
|
for ; name.typ != itemArrayTableEnd && name.typ != itemEOF; name = p.next() {
|
||||||
|
@ -145,13 +183,12 @@ func (p *parser) topLevel(item item) {
|
||||||
p.assertEqual(itemArrayTableEnd, name.typ)
|
p.assertEqual(itemArrayTableEnd, name.typ)
|
||||||
|
|
||||||
p.addContext(key, true)
|
p.addContext(key, true)
|
||||||
p.setType("", tomlArrayHash)
|
p.setType("", tomlArrayHash, item.pos)
|
||||||
p.ordered = append(p.ordered, key)
|
p.ordered = append(p.ordered, key)
|
||||||
case itemKeyStart: // key = ..
|
case itemKeyStart: // key = ..
|
||||||
outerContext := p.context
|
outerContext := p.context
|
||||||
/// Read all the key parts (e.g. 'a' and 'b' in 'a.b')
|
/// Read all the key parts (e.g. 'a' and 'b' in 'a.b')
|
||||||
k := p.next()
|
k := p.nextPos()
|
||||||
p.approxLine = k.line
|
|
||||||
var key Key
|
var key Key
|
||||||
for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() {
|
for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() {
|
||||||
key = append(key, p.keyString(k))
|
key = append(key, p.keyString(k))
|
||||||
|
@ -159,19 +196,21 @@ func (p *parser) topLevel(item item) {
|
||||||
p.assertEqual(itemKeyEnd, k.typ)
|
p.assertEqual(itemKeyEnd, k.typ)
|
||||||
|
|
||||||
/// The current key is the last part.
|
/// The current key is the last part.
|
||||||
p.currentKey = key[len(key)-1]
|
p.currentKey = key.last()
|
||||||
|
|
||||||
/// All the other parts (if any) are the context; need to set each part
|
/// All the other parts (if any) are the context; need to set each part
|
||||||
/// as implicit.
|
/// as implicit.
|
||||||
context := key[:len(key)-1]
|
context := key.parent()
|
||||||
for i := range context {
|
for i := range context {
|
||||||
p.addImplicitContext(append(p.context, context[i:i+1]...))
|
p.addImplicitContext(append(p.context, context[i:i+1]...))
|
||||||
}
|
}
|
||||||
|
p.ordered = append(p.ordered, p.context.add(p.currentKey))
|
||||||
|
|
||||||
/// Set value.
|
/// Set value.
|
||||||
val, typ := p.value(p.next(), false)
|
vItem := p.next()
|
||||||
p.set(p.currentKey, val, typ)
|
val, typ := p.value(vItem, false)
|
||||||
p.ordered = append(p.ordered, p.context.add(p.currentKey))
|
p.setValue(p.currentKey, val)
|
||||||
|
p.setType(p.currentKey, typ, vItem.pos)
|
||||||
|
|
||||||
/// Remove the context we added (preserving any context from [tbl] lines).
|
/// Remove the context we added (preserving any context from [tbl] lines).
|
||||||
p.context = outerContext
|
p.context = outerContext
|
||||||
|
@ -186,7 +225,7 @@ func (p *parser) keyString(it item) string {
|
||||||
switch it.typ {
|
switch it.typ {
|
||||||
case itemText:
|
case itemText:
|
||||||
return it.val
|
return it.val
|
||||||
case itemString, itemMultilineString,
|
case itemString, itemStringEsc, itemMultilineString,
|
||||||
itemRawString, itemRawMultilineString:
|
itemRawString, itemRawMultilineString:
|
||||||
s, _ := p.value(it, false)
|
s, _ := p.value(it, false)
|
||||||
return s.(string)
|
return s.(string)
|
||||||
|
@ -203,12 +242,14 @@ var datetimeRepl = strings.NewReplacer(
|
||||||
|
|
||||||
// value translates an expected value from the lexer into a Go value wrapped
|
// value translates an expected value from the lexer into a Go value wrapped
|
||||||
// as an empty interface.
|
// as an empty interface.
|
||||||
func (p *parser) value(it item, parentIsArray bool) (interface{}, tomlType) {
|
func (p *parser) value(it item, parentIsArray bool) (any, tomlType) {
|
||||||
switch it.typ {
|
switch it.typ {
|
||||||
case itemString:
|
case itemString:
|
||||||
return p.replaceEscapes(it.val), p.typeOfPrimitive(it)
|
return it.val, p.typeOfPrimitive(it)
|
||||||
|
case itemStringEsc:
|
||||||
|
return p.replaceEscapes(it, it.val), p.typeOfPrimitive(it)
|
||||||
case itemMultilineString:
|
case itemMultilineString:
|
||||||
return p.replaceEscapes(stripFirstNewline(stripEscapedNewlines(it.val))), p.typeOfPrimitive(it)
|
return p.replaceEscapes(it, p.stripEscapedNewlines(stripFirstNewline(it.val))), p.typeOfPrimitive(it)
|
||||||
case itemRawString:
|
case itemRawString:
|
||||||
return it.val, p.typeOfPrimitive(it)
|
return it.val, p.typeOfPrimitive(it)
|
||||||
case itemRawMultilineString:
|
case itemRawMultilineString:
|
||||||
|
@ -238,12 +279,12 @@ func (p *parser) value(it item, parentIsArray bool) (interface{}, tomlType) {
|
||||||
panic("unreachable")
|
panic("unreachable")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) valueInteger(it item) (interface{}, tomlType) {
|
func (p *parser) valueInteger(it item) (any, tomlType) {
|
||||||
if !numUnderscoresOK(it.val) {
|
if !numUnderscoresOK(it.val) {
|
||||||
p.panicf("Invalid integer %q: underscores must be surrounded by digits", it.val)
|
p.panicItemf(it, "Invalid integer %q: underscores must be surrounded by digits", it.val)
|
||||||
}
|
}
|
||||||
if numHasLeadingZero(it.val) {
|
if numHasLeadingZero(it.val) {
|
||||||
p.panicf("Invalid integer %q: cannot have leading zeroes", it.val)
|
p.panicItemf(it, "Invalid integer %q: cannot have leading zeroes", it.val)
|
||||||
}
|
}
|
||||||
|
|
||||||
num, err := strconv.ParseInt(it.val, 0, 64)
|
num, err := strconv.ParseInt(it.val, 0, 64)
|
||||||
|
@ -254,7 +295,7 @@ func (p *parser) valueInteger(it item) (interface{}, tomlType) {
|
||||||
// So mark the former as a bug but the latter as a legitimate user
|
// So mark the former as a bug but the latter as a legitimate user
|
||||||
// error.
|
// error.
|
||||||
if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange {
|
if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange {
|
||||||
p.panicf("Integer '%s' is out of the range of 64-bit signed integers.", it.val)
|
p.panicErr(it, errParseRange{i: it.val, size: "int64"})
|
||||||
} else {
|
} else {
|
||||||
p.bug("Expected integer value, but got '%s'.", it.val)
|
p.bug("Expected integer value, but got '%s'.", it.val)
|
||||||
}
|
}
|
||||||
|
@ -262,7 +303,7 @@ func (p *parser) valueInteger(it item) (interface{}, tomlType) {
|
||||||
return num, p.typeOfPrimitive(it)
|
return num, p.typeOfPrimitive(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) valueFloat(it item) (interface{}, tomlType) {
|
func (p *parser) valueFloat(it item) (any, tomlType) {
|
||||||
parts := strings.FieldsFunc(it.val, func(r rune) bool {
|
parts := strings.FieldsFunc(it.val, func(r rune) bool {
|
||||||
switch r {
|
switch r {
|
||||||
case '.', 'e', 'E':
|
case '.', 'e', 'E':
|
||||||
|
@ -272,45 +313,56 @@ func (p *parser) valueFloat(it item) (interface{}, tomlType) {
|
||||||
})
|
})
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
if !numUnderscoresOK(part) {
|
if !numUnderscoresOK(part) {
|
||||||
p.panicf("Invalid float %q: underscores must be surrounded by digits", it.val)
|
p.panicItemf(it, "Invalid float %q: underscores must be surrounded by digits", it.val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(parts) > 0 && numHasLeadingZero(parts[0]) {
|
if len(parts) > 0 && numHasLeadingZero(parts[0]) {
|
||||||
p.panicf("Invalid float %q: cannot have leading zeroes", it.val)
|
p.panicItemf(it, "Invalid float %q: cannot have leading zeroes", it.val)
|
||||||
}
|
}
|
||||||
if !numPeriodsOK(it.val) {
|
if !numPeriodsOK(it.val) {
|
||||||
// As a special case, numbers like '123.' or '1.e2',
|
// As a special case, numbers like '123.' or '1.e2',
|
||||||
// which are valid as far as Go/strconv are concerned,
|
// which are valid as far as Go/strconv are concerned,
|
||||||
// must be rejected because TOML says that a fractional
|
// must be rejected because TOML says that a fractional
|
||||||
// part consists of '.' followed by 1+ digits.
|
// part consists of '.' followed by 1+ digits.
|
||||||
p.panicf("Invalid float %q: '.' must be followed by one or more digits", it.val)
|
p.panicItemf(it, "Invalid float %q: '.' must be followed by one or more digits", it.val)
|
||||||
}
|
}
|
||||||
val := strings.Replace(it.val, "_", "", -1)
|
val := strings.Replace(it.val, "_", "", -1)
|
||||||
if val == "+nan" || val == "-nan" { // Go doesn't support this, but TOML spec does.
|
signbit := false
|
||||||
|
if val == "+nan" || val == "-nan" {
|
||||||
|
signbit = val == "-nan"
|
||||||
val = "nan"
|
val = "nan"
|
||||||
}
|
}
|
||||||
num, err := strconv.ParseFloat(val, 64)
|
num, err := strconv.ParseFloat(val, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange {
|
if e, ok := err.(*strconv.NumError); ok && e.Err == strconv.ErrRange {
|
||||||
p.panicf("Float '%s' is out of the range of 64-bit IEEE-754 floating-point numbers.", it.val)
|
p.panicErr(it, errParseRange{i: it.val, size: "float64"})
|
||||||
} else {
|
} else {
|
||||||
p.panicf("Invalid float value: %q", it.val)
|
p.panicItemf(it, "Invalid float value: %q", it.val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if signbit {
|
||||||
|
num = math.Copysign(num, -1)
|
||||||
|
}
|
||||||
return num, p.typeOfPrimitive(it)
|
return num, p.typeOfPrimitive(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
var dtTypes = []struct {
|
var dtTypes = []struct {
|
||||||
fmt string
|
fmt string
|
||||||
zone *time.Location
|
zone *time.Location
|
||||||
|
next bool
|
||||||
}{
|
}{
|
||||||
{time.RFC3339Nano, time.Local},
|
{time.RFC3339Nano, time.Local, false},
|
||||||
{"2006-01-02T15:04:05.999999999", internal.LocalDatetime},
|
{"2006-01-02T15:04:05.999999999", internal.LocalDatetime, false},
|
||||||
{"2006-01-02", internal.LocalDate},
|
{"2006-01-02", internal.LocalDate, false},
|
||||||
{"15:04:05.999999999", internal.LocalTime},
|
{"15:04:05.999999999", internal.LocalTime, false},
|
||||||
|
|
||||||
|
// tomlNext
|
||||||
|
{"2006-01-02T15:04Z07:00", time.Local, true},
|
||||||
|
{"2006-01-02T15:04", internal.LocalDatetime, true},
|
||||||
|
{"15:04", internal.LocalTime, true},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) valueDatetime(it item) (interface{}, tomlType) {
|
func (p *parser) valueDatetime(it item) (any, tomlType) {
|
||||||
it.val = datetimeRepl.Replace(it.val)
|
it.val = datetimeRepl.Replace(it.val)
|
||||||
var (
|
var (
|
||||||
t time.Time
|
t time.Time
|
||||||
|
@ -318,25 +370,49 @@ func (p *parser) valueDatetime(it item) (interface{}, tomlType) {
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
for _, dt := range dtTypes {
|
for _, dt := range dtTypes {
|
||||||
|
if dt.next && !p.tomlNext {
|
||||||
|
continue
|
||||||
|
}
|
||||||
t, err = time.ParseInLocation(dt.fmt, it.val, dt.zone)
|
t, err = time.ParseInLocation(dt.fmt, it.val, dt.zone)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
if missingLeadingZero(it.val, dt.fmt) {
|
||||||
|
p.panicErr(it, errParseDate{it.val})
|
||||||
|
}
|
||||||
ok = true
|
ok = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
p.panicf("Invalid TOML Datetime: %q.", it.val)
|
p.panicErr(it, errParseDate{it.val})
|
||||||
}
|
}
|
||||||
return t, p.typeOfPrimitive(it)
|
return t, p.typeOfPrimitive(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) valueArray(it item) (interface{}, tomlType) {
|
// Go's time.Parse() will accept numbers without a leading zero; there isn't any
|
||||||
p.setType(p.currentKey, tomlArray)
|
// way to require it. https://github.com/golang/go/issues/29911
|
||||||
|
//
|
||||||
|
// Depend on the fact that the separators (- and :) should always be at the same
|
||||||
|
// location.
|
||||||
|
func missingLeadingZero(d, l string) bool {
|
||||||
|
for i, c := range []byte(l) {
|
||||||
|
if c == '.' || c == 'Z' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (c < '0' || c > '9') && d[i] != c {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *parser) valueArray(it item) (any, tomlType) {
|
||||||
|
p.setType(p.currentKey, tomlArray, it.pos)
|
||||||
|
|
||||||
// p.setType(p.currentKey, typ)
|
|
||||||
var (
|
var (
|
||||||
array []interface{}
|
// Initialize to a non-nil slice to make it consistent with how S = []
|
||||||
types []tomlType
|
// decodes into a non-nil slice inside something like struct { S
|
||||||
|
// []string }. See #338
|
||||||
|
array = make([]any, 0, 2)
|
||||||
)
|
)
|
||||||
for it = p.next(); it.typ != itemArrayEnd; it = p.next() {
|
for it = p.next(); it.typ != itemArrayEnd; it = p.next() {
|
||||||
if it.typ == itemCommentStart {
|
if it.typ == itemCommentStart {
|
||||||
|
@ -346,14 +422,20 @@ func (p *parser) valueArray(it item) (interface{}, tomlType) {
|
||||||
|
|
||||||
val, typ := p.value(it, true)
|
val, typ := p.value(it, true)
|
||||||
array = append(array, val)
|
array = append(array, val)
|
||||||
types = append(types, typ)
|
|
||||||
|
// XXX: type isn't used here, we need it to record the accurate type
|
||||||
|
// information.
|
||||||
|
//
|
||||||
|
// Not entirely sure how to best store this; could use "key[0]",
|
||||||
|
// "key[1]" notation, or maybe store it on the Array type?
|
||||||
|
_ = typ
|
||||||
}
|
}
|
||||||
return array, tomlArray
|
return array, tomlArray
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) valueInlineTable(it item, parentIsArray bool) (interface{}, tomlType) {
|
func (p *parser) valueInlineTable(it item, parentIsArray bool) (any, tomlType) {
|
||||||
var (
|
var (
|
||||||
hash = make(map[string]interface{})
|
topHash = make(map[string]any)
|
||||||
outerContext = p.context
|
outerContext = p.context
|
||||||
outerKey = p.currentKey
|
outerKey = p.currentKey
|
||||||
)
|
)
|
||||||
|
@ -373,8 +455,7 @@ func (p *parser) valueInlineTable(it item, parentIsArray bool) (interface{}, tom
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read all key parts.
|
/// Read all key parts.
|
||||||
k := p.next()
|
k := p.nextPos()
|
||||||
p.approxLine = k.line
|
|
||||||
var key Key
|
var key Key
|
||||||
for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() {
|
for ; k.typ != itemKeyEnd && k.typ != itemEOF; k = p.next() {
|
||||||
key = append(key, p.keyString(k))
|
key = append(key, p.keyString(k))
|
||||||
|
@ -382,19 +463,33 @@ func (p *parser) valueInlineTable(it item, parentIsArray bool) (interface{}, tom
|
||||||
p.assertEqual(itemKeyEnd, k.typ)
|
p.assertEqual(itemKeyEnd, k.typ)
|
||||||
|
|
||||||
/// The current key is the last part.
|
/// The current key is the last part.
|
||||||
p.currentKey = key[len(key)-1]
|
p.currentKey = key.last()
|
||||||
|
|
||||||
/// All the other parts (if any) are the context; need to set each part
|
/// All the other parts (if any) are the context; need to set each part
|
||||||
/// as implicit.
|
/// as implicit.
|
||||||
context := key[:len(key)-1]
|
context := key.parent()
|
||||||
for i := range context {
|
for i := range context {
|
||||||
p.addImplicitContext(append(p.context, context[i:i+1]...))
|
p.addImplicitContext(append(p.context, context[i:i+1]...))
|
||||||
}
|
}
|
||||||
|
p.ordered = append(p.ordered, p.context.add(p.currentKey))
|
||||||
|
|
||||||
/// Set the value.
|
/// Set the value.
|
||||||
val, typ := p.value(p.next(), false)
|
val, typ := p.value(p.next(), false)
|
||||||
p.set(p.currentKey, val, typ)
|
p.setValue(p.currentKey, val)
|
||||||
p.ordered = append(p.ordered, p.context.add(p.currentKey))
|
p.setType(p.currentKey, typ, it.pos)
|
||||||
|
|
||||||
|
hash := topHash
|
||||||
|
for _, c := range context {
|
||||||
|
h, ok := hash[c]
|
||||||
|
if !ok {
|
||||||
|
h = make(map[string]any)
|
||||||
|
hash[c] = h
|
||||||
|
}
|
||||||
|
hash, ok = h.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
p.panicf("%q is not a table", p.context)
|
||||||
|
}
|
||||||
|
}
|
||||||
hash[p.currentKey] = val
|
hash[p.currentKey] = val
|
||||||
|
|
||||||
/// Restore context.
|
/// Restore context.
|
||||||
|
@ -402,13 +497,13 @@ func (p *parser) valueInlineTable(it item, parentIsArray bool) (interface{}, tom
|
||||||
}
|
}
|
||||||
p.context = outerContext
|
p.context = outerContext
|
||||||
p.currentKey = outerKey
|
p.currentKey = outerKey
|
||||||
return hash, tomlHash
|
return topHash, tomlHash
|
||||||
}
|
}
|
||||||
|
|
||||||
// numHasLeadingZero checks if this number has leading zeroes, allowing for '0',
|
// numHasLeadingZero checks if this number has leading zeroes, allowing for '0',
|
||||||
// +/- signs, and base prefixes.
|
// +/- signs, and base prefixes.
|
||||||
func numHasLeadingZero(s string) bool {
|
func numHasLeadingZero(s string) bool {
|
||||||
if len(s) > 1 && s[0] == '0' && isDigit(rune(s[1])) { // >1 to allow "0" and isDigit to allow 0x
|
if len(s) > 1 && s[0] == '0' && !(s[1] == 'b' || s[1] == 'o' || s[1] == 'x') { // Allow 0b, 0o, 0x
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if len(s) > 2 && (s[0] == '-' || s[0] == '+') && s[1] == '0' {
|
if len(s) > 2 && (s[0] == '-' || s[0] == '+') && s[1] == '0' {
|
||||||
|
@ -432,9 +527,9 @@ func numUnderscoresOK(s string) bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// isHexadecimal is a superset of all the permissable characters
|
// isHexis a superset of all the permissable characters surrounding an
|
||||||
// surrounding an underscore.
|
// underscore.
|
||||||
accept = isHexadecimal(r)
|
accept = isHex(r)
|
||||||
}
|
}
|
||||||
return accept
|
return accept
|
||||||
}
|
}
|
||||||
|
@ -457,21 +552,19 @@ func numPeriodsOK(s string) bool {
|
||||||
// Establishing the context also makes sure that the key isn't a duplicate, and
|
// Establishing the context also makes sure that the key isn't a duplicate, and
|
||||||
// will create implicit hashes automatically.
|
// will create implicit hashes automatically.
|
||||||
func (p *parser) addContext(key Key, array bool) {
|
func (p *parser) addContext(key Key, array bool) {
|
||||||
var ok bool
|
/// Always start at the top level and drill down for our context.
|
||||||
|
|
||||||
// Always start at the top level and drill down for our context.
|
|
||||||
hashContext := p.mapping
|
hashContext := p.mapping
|
||||||
keyContext := make(Key, 0)
|
keyContext := make(Key, 0, len(key)-1)
|
||||||
|
|
||||||
// We only need implicit hashes for key[0:-1]
|
/// We only need implicit hashes for the parents.
|
||||||
for _, k := range key[0 : len(key)-1] {
|
for _, k := range key.parent() {
|
||||||
_, ok = hashContext[k]
|
_, ok := hashContext[k]
|
||||||
keyContext = append(keyContext, k)
|
keyContext = append(keyContext, k)
|
||||||
|
|
||||||
// No key? Make an implicit hash and move on.
|
// No key? Make an implicit hash and move on.
|
||||||
if !ok {
|
if !ok {
|
||||||
p.addImplicit(keyContext)
|
p.addImplicit(keyContext)
|
||||||
hashContext[k] = make(map[string]interface{})
|
hashContext[k] = make(map[string]any)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the hash context is actually an array of tables, then set
|
// If the hash context is actually an array of tables, then set
|
||||||
|
@ -480,9 +573,9 @@ func (p *parser) addContext(key Key, array bool) {
|
||||||
// Otherwise, it better be a table, since this MUST be a key group (by
|
// Otherwise, it better be a table, since this MUST be a key group (by
|
||||||
// virtue of it not being the last element in a key).
|
// virtue of it not being the last element in a key).
|
||||||
switch t := hashContext[k].(type) {
|
switch t := hashContext[k].(type) {
|
||||||
case []map[string]interface{}:
|
case []map[string]any:
|
||||||
hashContext = t[len(t)-1]
|
hashContext = t[len(t)-1]
|
||||||
case map[string]interface{}:
|
case map[string]any:
|
||||||
hashContext = t
|
hashContext = t
|
||||||
default:
|
default:
|
||||||
p.panicf("Key '%s' was already created as a hash.", keyContext)
|
p.panicf("Key '%s' was already created as a hash.", keyContext)
|
||||||
|
@ -493,39 +586,33 @@ func (p *parser) addContext(key Key, array bool) {
|
||||||
if array {
|
if array {
|
||||||
// If this is the first element for this array, then allocate a new
|
// If this is the first element for this array, then allocate a new
|
||||||
// list of tables for it.
|
// list of tables for it.
|
||||||
k := key[len(key)-1]
|
k := key.last()
|
||||||
if _, ok := hashContext[k]; !ok {
|
if _, ok := hashContext[k]; !ok {
|
||||||
hashContext[k] = make([]map[string]interface{}, 0, 4)
|
hashContext[k] = make([]map[string]any, 0, 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a new table. But make sure the key hasn't already been used
|
// Add a new table. But make sure the key hasn't already been used
|
||||||
// for something else.
|
// for something else.
|
||||||
if hash, ok := hashContext[k].([]map[string]interface{}); ok {
|
if hash, ok := hashContext[k].([]map[string]any); ok {
|
||||||
hashContext[k] = append(hash, make(map[string]interface{}))
|
hashContext[k] = append(hash, make(map[string]any))
|
||||||
} else {
|
} else {
|
||||||
p.panicf("Key '%s' was already created and cannot be used as an array.", keyContext)
|
p.panicf("Key '%s' was already created and cannot be used as an array.", key)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
p.setValue(key[len(key)-1], make(map[string]interface{}))
|
p.setValue(key.last(), make(map[string]any))
|
||||||
}
|
}
|
||||||
p.context = append(p.context, key[len(key)-1])
|
p.context = append(p.context, key.last())
|
||||||
}
|
|
||||||
|
|
||||||
// set calls setValue and setType.
|
|
||||||
func (p *parser) set(key string, val interface{}, typ tomlType) {
|
|
||||||
p.setValue(p.currentKey, val)
|
|
||||||
p.setType(p.currentKey, typ)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// setValue sets the given key to the given value in the current context.
|
// setValue sets the given key to the given value in the current context.
|
||||||
// It will make sure that the key hasn't already been defined, account for
|
// It will make sure that the key hasn't already been defined, account for
|
||||||
// implicit key groups.
|
// implicit key groups.
|
||||||
func (p *parser) setValue(key string, value interface{}) {
|
func (p *parser) setValue(key string, value any) {
|
||||||
var (
|
var (
|
||||||
tmpHash interface{}
|
tmpHash any
|
||||||
ok bool
|
ok bool
|
||||||
hash = p.mapping
|
hash = p.mapping
|
||||||
keyContext Key
|
keyContext = make(Key, 0, len(p.context)+1)
|
||||||
)
|
)
|
||||||
for _, k := range p.context {
|
for _, k := range p.context {
|
||||||
keyContext = append(keyContext, k)
|
keyContext = append(keyContext, k)
|
||||||
|
@ -533,11 +620,11 @@ func (p *parser) setValue(key string, value interface{}) {
|
||||||
p.bug("Context for key '%s' has not been established.", keyContext)
|
p.bug("Context for key '%s' has not been established.", keyContext)
|
||||||
}
|
}
|
||||||
switch t := tmpHash.(type) {
|
switch t := tmpHash.(type) {
|
||||||
case []map[string]interface{}:
|
case []map[string]any:
|
||||||
// The context is a table of hashes. Pick the most recent table
|
// The context is a table of hashes. Pick the most recent table
|
||||||
// defined as the current hash.
|
// defined as the current hash.
|
||||||
hash = t[len(t)-1]
|
hash = t[len(t)-1]
|
||||||
case map[string]interface{}:
|
case map[string]any:
|
||||||
hash = t
|
hash = t
|
||||||
default:
|
default:
|
||||||
p.panicf("Key '%s' has already been defined.", keyContext)
|
p.panicf("Key '%s' has already been defined.", keyContext)
|
||||||
|
@ -564,41 +651,41 @@ func (p *parser) setValue(key string, value interface{}) {
|
||||||
p.removeImplicit(keyContext)
|
p.removeImplicit(keyContext)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Otherwise, we have a concrete key trying to override a previous key,
|
||||||
// Otherwise, we have a concrete key trying to override a previous
|
// which is *always* wrong.
|
||||||
// key, which is *always* wrong.
|
|
||||||
p.panicf("Key '%s' has already been defined.", keyContext)
|
p.panicf("Key '%s' has already been defined.", keyContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
hash[key] = value
|
hash[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
// setType sets the type of a particular value at a given key.
|
// setType sets the type of a particular value at a given key. It should be
|
||||||
// It should be called immediately AFTER setValue.
|
// called immediately AFTER setValue.
|
||||||
//
|
//
|
||||||
// Note that if `key` is empty, then the type given will be applied to the
|
// Note that if `key` is empty, then the type given will be applied to the
|
||||||
// current context (which is either a table or an array of tables).
|
// current context (which is either a table or an array of tables).
|
||||||
func (p *parser) setType(key string, typ tomlType) {
|
func (p *parser) setType(key string, typ tomlType, pos Position) {
|
||||||
keyContext := make(Key, 0, len(p.context)+1)
|
keyContext := make(Key, 0, len(p.context)+1)
|
||||||
for _, k := range p.context {
|
keyContext = append(keyContext, p.context...)
|
||||||
keyContext = append(keyContext, k)
|
|
||||||
}
|
|
||||||
if len(key) > 0 { // allow type setting for hashes
|
if len(key) > 0 { // allow type setting for hashes
|
||||||
keyContext = append(keyContext, key)
|
keyContext = append(keyContext, key)
|
||||||
}
|
}
|
||||||
p.types[keyContext.String()] = typ
|
// Special case to make empty keys ("" = 1) work.
|
||||||
|
// Without it it will set "" rather than `""`.
|
||||||
|
// TODO: why is this needed? And why is this only needed here?
|
||||||
|
if len(keyContext) == 0 {
|
||||||
|
keyContext = Key{""}
|
||||||
|
}
|
||||||
|
p.keyInfo[keyContext.String()] = keyInfo{tomlType: typ, pos: pos}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implicit keys need to be created when tables are implied in "a.b.c.d = 1" and
|
// Implicit keys need to be created when tables are implied in "a.b.c.d = 1" and
|
||||||
// "[a.b.c]" (the "a", "b", and "c" hashes are never created explicitly).
|
// "[a.b.c]" (the "a", "b", and "c" hashes are never created explicitly).
|
||||||
func (p *parser) addImplicit(key Key) { p.implicits[key.String()] = true }
|
func (p *parser) addImplicit(key Key) { p.implicits[key.String()] = struct{}{} }
|
||||||
func (p *parser) removeImplicit(key Key) { p.implicits[key.String()] = false }
|
func (p *parser) removeImplicit(key Key) { delete(p.implicits, key.String()) }
|
||||||
func (p *parser) isImplicit(key Key) bool { return p.implicits[key.String()] }
|
func (p *parser) isImplicit(key Key) bool { _, ok := p.implicits[key.String()]; return ok }
|
||||||
func (p *parser) isArray(key Key) bool { return p.types[key.String()] == tomlArray }
|
func (p *parser) isArray(key Key) bool { return p.keyInfo[key.String()].tomlType == tomlArray }
|
||||||
func (p *parser) addImplicitContext(key Key) {
|
func (p *parser) addImplicitContext(key Key) { p.addImplicit(key); p.addContext(key, false) }
|
||||||
p.addImplicit(key)
|
|
||||||
p.addContext(key, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// current returns the full key name of the current context.
|
// current returns the full key name of the current context.
|
||||||
func (p *parser) current() string {
|
func (p *parser) current() string {
|
||||||
|
@ -621,119 +708,137 @@ func stripFirstNewline(s string) string {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove newlines inside triple-quoted strings if a line ends with "\".
|
// stripEscapedNewlines removes whitespace after line-ending backslashes in
|
||||||
func stripEscapedNewlines(s string) string {
|
// multiline strings.
|
||||||
split := strings.Split(s, "\n")
|
//
|
||||||
if len(split) < 1 {
|
// A line-ending backslash is an unescaped \ followed only by whitespace until
|
||||||
return s
|
// the next newline. After a line-ending backslash, all whitespace is removed
|
||||||
}
|
// until the next non-whitespace character.
|
||||||
|
func (p *parser) stripEscapedNewlines(s string) string {
|
||||||
|
var (
|
||||||
|
b strings.Builder
|
||||||
|
i int
|
||||||
|
)
|
||||||
|
b.Grow(len(s))
|
||||||
|
for {
|
||||||
|
ix := strings.Index(s[i:], `\`)
|
||||||
|
if ix < 0 {
|
||||||
|
b.WriteString(s)
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
i += ix
|
||||||
|
|
||||||
escNL := false // Keep track of the last non-blank line was escaped.
|
if len(s) > i+1 && s[i+1] == '\\' {
|
||||||
for i, line := range split {
|
// Escaped backslash.
|
||||||
line = strings.TrimRight(line, " \t\r")
|
i += 2
|
||||||
|
continue
|
||||||
if len(line) == 0 || line[len(line)-1] != '\\' {
|
}
|
||||||
split[i] = strings.TrimRight(split[i], "\r")
|
// Scan until the next non-whitespace.
|
||||||
if !escNL && i != len(split)-1 {
|
j := i + 1
|
||||||
split[i] += "\n"
|
whitespaceLoop:
|
||||||
|
for ; j < len(s); j++ {
|
||||||
|
switch s[j] {
|
||||||
|
case ' ', '\t', '\r', '\n':
|
||||||
|
default:
|
||||||
|
break whitespaceLoop
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if j == i+1 {
|
||||||
|
// Not a whitespace escape.
|
||||||
|
i++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(s[i:j], "\n") {
|
||||||
escBS := true
|
// This is not a line-ending backslash. (It's a bad escape sequence,
|
||||||
for j := len(line) - 1; j >= 0 && line[j] == '\\'; j-- {
|
// but we can let replaceEscapes catch it.)
|
||||||
escBS = !escBS
|
i++
|
||||||
}
|
|
||||||
if escNL {
|
|
||||||
line = strings.TrimLeft(line, " \t\r")
|
|
||||||
}
|
|
||||||
escNL = !escBS
|
|
||||||
|
|
||||||
if escBS {
|
|
||||||
split[i] += "\n"
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
b.WriteString(s[:i])
|
||||||
split[i] = line[:len(line)-1] // Remove \
|
s = s[j:]
|
||||||
if len(split)-1 > i {
|
i = 0
|
||||||
split[i+1] = strings.TrimLeft(split[i+1], " \t\r")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return strings.Join(split, "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) replaceEscapes(str string) string {
|
func (p *parser) replaceEscapes(it item, str string) string {
|
||||||
var replaced []rune
|
var (
|
||||||
s := []byte(str)
|
b strings.Builder
|
||||||
r := 0
|
skip = 0
|
||||||
for r < len(s) {
|
)
|
||||||
if s[r] != '\\' {
|
b.Grow(len(str))
|
||||||
c, size := utf8.DecodeRune(s[r:])
|
for i, c := range str {
|
||||||
r += size
|
if skip > 0 {
|
||||||
replaced = append(replaced, c)
|
skip--
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
r += 1
|
if c != '\\' {
|
||||||
if r >= len(s) {
|
b.WriteRune(c)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if i >= len(str) {
|
||||||
p.bug("Escape sequence at end of string.")
|
p.bug("Escape sequence at end of string.")
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
switch s[r] {
|
switch str[i+1] {
|
||||||
default:
|
default:
|
||||||
p.bug("Expected valid escape code after \\, but got %q.", s[r])
|
p.bug("Expected valid escape code after \\, but got %q.", str[i+1])
|
||||||
return ""
|
|
||||||
case ' ', '\t':
|
case ' ', '\t':
|
||||||
p.panicf("invalid escape: '\\%c'", s[r])
|
p.panicItemf(it, "invalid escape: '\\%c'", str[i+1])
|
||||||
return ""
|
|
||||||
case 'b':
|
case 'b':
|
||||||
replaced = append(replaced, rune(0x0008))
|
b.WriteByte(0x08)
|
||||||
r += 1
|
skip = 1
|
||||||
case 't':
|
case 't':
|
||||||
replaced = append(replaced, rune(0x0009))
|
b.WriteByte(0x09)
|
||||||
r += 1
|
skip = 1
|
||||||
case 'n':
|
case 'n':
|
||||||
replaced = append(replaced, rune(0x000A))
|
b.WriteByte(0x0a)
|
||||||
r += 1
|
skip = 1
|
||||||
case 'f':
|
case 'f':
|
||||||
replaced = append(replaced, rune(0x000C))
|
b.WriteByte(0x0c)
|
||||||
r += 1
|
skip = 1
|
||||||
case 'r':
|
case 'r':
|
||||||
replaced = append(replaced, rune(0x000D))
|
b.WriteByte(0x0d)
|
||||||
r += 1
|
skip = 1
|
||||||
|
case 'e':
|
||||||
|
if p.tomlNext {
|
||||||
|
b.WriteByte(0x1b)
|
||||||
|
skip = 1
|
||||||
|
}
|
||||||
case '"':
|
case '"':
|
||||||
replaced = append(replaced, rune(0x0022))
|
b.WriteByte(0x22)
|
||||||
r += 1
|
skip = 1
|
||||||
case '\\':
|
case '\\':
|
||||||
replaced = append(replaced, rune(0x005C))
|
b.WriteByte(0x5c)
|
||||||
r += 1
|
skip = 1
|
||||||
|
// The lexer guarantees the correct number of characters are present;
|
||||||
|
// don't need to check here.
|
||||||
|
case 'x':
|
||||||
|
if p.tomlNext {
|
||||||
|
escaped := p.asciiEscapeToUnicode(it, str[i+2:i+4])
|
||||||
|
b.WriteRune(escaped)
|
||||||
|
skip = 3
|
||||||
|
}
|
||||||
case 'u':
|
case 'u':
|
||||||
// At this point, we know we have a Unicode escape of the form
|
escaped := p.asciiEscapeToUnicode(it, str[i+2:i+6])
|
||||||
// `uXXXX` at [r, r+5). (Because the lexer guarantees this
|
b.WriteRune(escaped)
|
||||||
// for us.)
|
skip = 5
|
||||||
escaped := p.asciiEscapeToUnicode(s[r+1 : r+5])
|
|
||||||
replaced = append(replaced, escaped)
|
|
||||||
r += 5
|
|
||||||
case 'U':
|
case 'U':
|
||||||
// At this point, we know we have a Unicode escape of the form
|
escaped := p.asciiEscapeToUnicode(it, str[i+2:i+10])
|
||||||
// `uXXXX` at [r, r+9). (Because the lexer guarantees this
|
b.WriteRune(escaped)
|
||||||
// for us.)
|
skip = 9
|
||||||
escaped := p.asciiEscapeToUnicode(s[r+1 : r+9])
|
|
||||||
replaced = append(replaced, escaped)
|
|
||||||
r += 9
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return string(replaced)
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *parser) asciiEscapeToUnicode(bs []byte) rune {
|
func (p *parser) asciiEscapeToUnicode(it item, s string) rune {
|
||||||
s := string(bs)
|
|
||||||
hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32)
|
hex, err := strconv.ParseUint(strings.ToLower(s), 16, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.bug("Could not parse '%s' as a hexadecimal number, but the "+
|
p.bug("Could not parse '%s' as a hexadecimal number, but the lexer claims it's OK: %s", s, err)
|
||||||
"lexer claims it's OK: %s", s, err)
|
|
||||||
}
|
}
|
||||||
if !utf8.ValidRune(rune(hex)) {
|
if !utf8.ValidRune(rune(hex)) {
|
||||||
p.panicf("Escaped character '\\u%s' is not valid UTF-8.", s)
|
p.panicItemf(it, "Escaped character '\\u%s' is not valid UTF-8.", s)
|
||||||
}
|
}
|
||||||
return rune(hex)
|
return rune(hex)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
package toml
|
|
||||||
|
|
||||||
// tomlType represents any Go type that corresponds to a TOML type.
|
|
||||||
// While the first draft of the TOML spec has a simplistic type system that
|
|
||||||
// probably doesn't need this level of sophistication, we seem to be militating
|
|
||||||
// toward adding real composite types.
|
|
||||||
type tomlType interface {
|
|
||||||
typeString() string
|
|
||||||
}
|
|
||||||
|
|
||||||
// typeEqual accepts any two types and returns true if they are equal.
|
|
||||||
func typeEqual(t1, t2 tomlType) bool {
|
|
||||||
if t1 == nil || t2 == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return t1.typeString() == t2.typeString()
|
|
||||||
}
|
|
||||||
|
|
||||||
func typeIsHash(t tomlType) bool {
|
|
||||||
return typeEqual(t, tomlHash) || typeEqual(t, tomlArrayHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
type tomlBaseType string
|
|
||||||
|
|
||||||
func (btype tomlBaseType) typeString() string {
|
|
||||||
return string(btype)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (btype tomlBaseType) String() string {
|
|
||||||
return btype.typeString()
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
tomlInteger tomlBaseType = "Integer"
|
|
||||||
tomlFloat tomlBaseType = "Float"
|
|
||||||
tomlDatetime tomlBaseType = "Datetime"
|
|
||||||
tomlString tomlBaseType = "String"
|
|
||||||
tomlBool tomlBaseType = "Bool"
|
|
||||||
tomlArray tomlBaseType = "Array"
|
|
||||||
tomlHash tomlBaseType = "Hash"
|
|
||||||
tomlArrayHash tomlBaseType = "ArrayHash"
|
|
||||||
)
|
|
||||||
|
|
||||||
// typeOfPrimitive returns a tomlType of any primitive value in TOML.
|
|
||||||
// Primitive values are: Integer, Float, Datetime, String and Bool.
|
|
||||||
//
|
|
||||||
// Passing a lexer item other than the following will cause a BUG message
|
|
||||||
// to occur: itemString, itemBool, itemInteger, itemFloat, itemDatetime.
|
|
||||||
func (p *parser) typeOfPrimitive(lexItem item) tomlType {
|
|
||||||
switch lexItem.typ {
|
|
||||||
case itemInteger:
|
|
||||||
return tomlInteger
|
|
||||||
case itemFloat:
|
|
||||||
return tomlFloat
|
|
||||||
case itemDatetime:
|
|
||||||
return tomlDatetime
|
|
||||||
case itemString:
|
|
||||||
return tomlString
|
|
||||||
case itemMultilineString:
|
|
||||||
return tomlString
|
|
||||||
case itemRawString:
|
|
||||||
return tomlString
|
|
||||||
case itemRawMultilineString:
|
|
||||||
return tomlString
|
|
||||||
case itemBool:
|
|
||||||
return tomlBool
|
|
||||||
}
|
|
||||||
p.bug("Cannot infer primitive type of lex item '%s'.", lexItem)
|
|
||||||
panic("unreachable")
|
|
||||||
}
|
|
|
@ -25,10 +25,8 @@ type field struct {
|
||||||
// breaking ties with index sequence.
|
// breaking ties with index sequence.
|
||||||
type byName []field
|
type byName []field
|
||||||
|
|
||||||
func (x byName) Len() int { return len(x) }
|
func (x byName) Len() int { return len(x) }
|
||||||
|
|
||||||
func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
func (x byName) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||||
|
|
||||||
func (x byName) Less(i, j int) bool {
|
func (x byName) Less(i, j int) bool {
|
||||||
if x[i].name != x[j].name {
|
if x[i].name != x[j].name {
|
||||||
return x[i].name < x[j].name
|
return x[i].name < x[j].name
|
||||||
|
@ -45,10 +43,8 @@ func (x byName) Less(i, j int) bool {
|
||||||
// byIndex sorts field by index sequence.
|
// byIndex sorts field by index sequence.
|
||||||
type byIndex []field
|
type byIndex []field
|
||||||
|
|
||||||
func (x byIndex) Len() int { return len(x) }
|
func (x byIndex) Len() int { return len(x) }
|
||||||
|
|
||||||
func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||||
|
|
||||||
func (x byIndex) Less(i, j int) bool {
|
func (x byIndex) Less(i, j int) bool {
|
||||||
for k, xik := range x[i].index {
|
for k, xik := range x[i].index {
|
||||||
if k >= len(x[j].index) {
|
if k >= len(x[j].index) {
|
||||||
|
@ -70,8 +66,8 @@ func typeFields(t reflect.Type) []field {
|
||||||
next := []field{{typ: t}}
|
next := []field{{typ: t}}
|
||||||
|
|
||||||
// Count of queued names for current level and the next.
|
// Count of queued names for current level and the next.
|
||||||
count := map[reflect.Type]int{}
|
var count map[reflect.Type]int
|
||||||
nextCount := map[reflect.Type]int{}
|
var nextCount map[reflect.Type]int
|
||||||
|
|
||||||
// Types already visited at an earlier level.
|
// Types already visited at an earlier level.
|
||||||
visited := map[reflect.Type]bool{}
|
visited := map[reflect.Type]bool{}
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
package toml
|
||||||
|
|
||||||
|
// tomlType represents any Go type that corresponds to a TOML type.
|
||||||
|
// While the first draft of the TOML spec has a simplistic type system that
|
||||||
|
// probably doesn't need this level of sophistication, we seem to be militating
|
||||||
|
// toward adding real composite types.
|
||||||
|
type tomlType interface {
|
||||||
|
typeString() string
|
||||||
|
}
|
||||||
|
|
||||||
|
// typeEqual accepts any two types and returns true if they are equal.
|
||||||
|
func typeEqual(t1, t2 tomlType) bool {
|
||||||
|
if t1 == nil || t2 == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return t1.typeString() == t2.typeString()
|
||||||
|
}
|
||||||
|
|
||||||
|
func typeIsTable(t tomlType) bool {
|
||||||
|
return typeEqual(t, tomlHash) || typeEqual(t, tomlArrayHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
type tomlBaseType string
|
||||||
|
|
||||||
|
func (btype tomlBaseType) typeString() string { return string(btype) }
|
||||||
|
func (btype tomlBaseType) String() string { return btype.typeString() }
|
||||||
|
|
||||||
|
var (
|
||||||
|
tomlInteger tomlBaseType = "Integer"
|
||||||
|
tomlFloat tomlBaseType = "Float"
|
||||||
|
tomlDatetime tomlBaseType = "Datetime"
|
||||||
|
tomlString tomlBaseType = "String"
|
||||||
|
tomlBool tomlBaseType = "Bool"
|
||||||
|
tomlArray tomlBaseType = "Array"
|
||||||
|
tomlHash tomlBaseType = "Hash"
|
||||||
|
tomlArrayHash tomlBaseType = "ArrayHash"
|
||||||
|
)
|
||||||
|
|
||||||
|
// typeOfPrimitive returns a tomlType of any primitive value in TOML.
|
||||||
|
// Primitive values are: Integer, Float, Datetime, String and Bool.
|
||||||
|
//
|
||||||
|
// Passing a lexer item other than the following will cause a BUG message
|
||||||
|
// to occur: itemString, itemBool, itemInteger, itemFloat, itemDatetime.
|
||||||
|
func (p *parser) typeOfPrimitive(lexItem item) tomlType {
|
||||||
|
switch lexItem.typ {
|
||||||
|
case itemInteger:
|
||||||
|
return tomlInteger
|
||||||
|
case itemFloat:
|
||||||
|
return tomlFloat
|
||||||
|
case itemDatetime:
|
||||||
|
return tomlDatetime
|
||||||
|
case itemString, itemStringEsc:
|
||||||
|
return tomlString
|
||||||
|
case itemMultilineString:
|
||||||
|
return tomlString
|
||||||
|
case itemRawString:
|
||||||
|
return tomlString
|
||||||
|
case itemRawMultilineString:
|
||||||
|
return tomlString
|
||||||
|
case itemBool:
|
||||||
|
return tomlBool
|
||||||
|
}
|
||||||
|
p.bug("Cannot infer primitive type of lex item '%s'.", lexItem)
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
|
@ -1,15 +0,0 @@
|
||||||
# Binaries for programs and plugins
|
|
||||||
*.exe
|
|
||||||
*.exe~
|
|
||||||
*.dll
|
|
||||||
*.so
|
|
||||||
*.dylib
|
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
|
||||||
*.test
|
|
||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
|
||||||
*.out
|
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
|
||||||
# vendor/
|
|
|
@ -1,150 +0,0 @@
|
||||||
# This file contains all available configuration options
|
|
||||||
# with their default values.
|
|
||||||
|
|
||||||
# options for analysis running
|
|
||||||
run:
|
|
||||||
# default concurrency is a available CPU number
|
|
||||||
concurrency: 4
|
|
||||||
|
|
||||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
|
||||||
deadline: 15m
|
|
||||||
|
|
||||||
# exit code when at least one issue was found, default is 1
|
|
||||||
issues-exit-code: 1
|
|
||||||
|
|
||||||
# include test files or not, default is true
|
|
||||||
tests: false
|
|
||||||
|
|
||||||
# list of build tags, all linters use it. Default is empty list.
|
|
||||||
#build-tags:
|
|
||||||
# - mytag
|
|
||||||
|
|
||||||
# which dirs to skip: they won't be analyzed;
|
|
||||||
# can use regexp here: generated.*, regexp is applied on full path;
|
|
||||||
# default value is empty list, but next dirs are always skipped independently
|
|
||||||
# from this option's value:
|
|
||||||
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
|
|
||||||
skip-dirs:
|
|
||||||
- /gen$
|
|
||||||
|
|
||||||
# which files to skip: they will be analyzed, but issues from them
|
|
||||||
# won't be reported. Default value is empty list, but there is
|
|
||||||
# no need to include all autogenerated files, we confidently recognize
|
|
||||||
# autogenerated files. If it's not please let us know.
|
|
||||||
skip-files:
|
|
||||||
- ".*\\.my\\.go$"
|
|
||||||
- lib/bad.go
|
|
||||||
- ".*\\.template\\.go$"
|
|
||||||
|
|
||||||
# output configuration options
|
|
||||||
output:
|
|
||||||
# colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number"
|
|
||||||
format: colored-line-number
|
|
||||||
|
|
||||||
# print lines of code with issue, default is true
|
|
||||||
print-issued-lines: true
|
|
||||||
|
|
||||||
# print linter name in the end of issue text, default is true
|
|
||||||
print-linter-name: true
|
|
||||||
|
|
||||||
# all available settings of specific linters
|
|
||||||
linters-settings:
|
|
||||||
errcheck:
|
|
||||||
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
|
|
||||||
# default is false: such cases aren't reported by default.
|
|
||||||
check-type-assertions: false
|
|
||||||
|
|
||||||
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
|
|
||||||
# default is false: such cases aren't reported by default.
|
|
||||||
check-blank: false
|
|
||||||
govet:
|
|
||||||
# report about shadowed variables
|
|
||||||
check-shadowing: true
|
|
||||||
|
|
||||||
# Obtain type information from installed (to $GOPATH/pkg) package files:
|
|
||||||
# golangci-lint will execute `go install -i` and `go test -i` for analyzed packages
|
|
||||||
# before analyzing them.
|
|
||||||
# By default this option is disabled and govet gets type information by loader from source code.
|
|
||||||
# Loading from source code is slow, but it's done only once for all linters.
|
|
||||||
# Go-installing of packages first time is much slower than loading them from source code,
|
|
||||||
# therefore this option is disabled by default.
|
|
||||||
# But repeated installation is fast in go >= 1.10 because of build caching.
|
|
||||||
# Enable this option only if all conditions are met:
|
|
||||||
# 1. you use only "fast" linters (--fast e.g.): no program loading occurs
|
|
||||||
# 2. you use go >= 1.10
|
|
||||||
# 3. you do repeated runs (false for CI) or cache $GOPATH/pkg or `go env GOCACHE` dir in CI.
|
|
||||||
use-installed-packages: false
|
|
||||||
golint:
|
|
||||||
# minimal confidence for issues, default is 0.8
|
|
||||||
min-confidence: 0.8
|
|
||||||
gofmt:
|
|
||||||
# simplify code: gofmt with `-s` option, true by default
|
|
||||||
simplify: true
|
|
||||||
gocyclo:
|
|
||||||
# minimal code complexity to report, 30 by default (but we recommend 10-20)
|
|
||||||
min-complexity: 10
|
|
||||||
maligned:
|
|
||||||
# print struct with more effective memory layout or not, false by default
|
|
||||||
suggest-new: true
|
|
||||||
dupl:
|
|
||||||
# tokens count to trigger issue, 150 by default
|
|
||||||
threshold: 100
|
|
||||||
goconst:
|
|
||||||
# minimal length of string constant, 3 by default
|
|
||||||
min-len: 3
|
|
||||||
# minimal occurrences count to trigger, 3 by default
|
|
||||||
min-occurrences: 3
|
|
||||||
depguard:
|
|
||||||
list-type: blacklist
|
|
||||||
include-go-root: false
|
|
||||||
packages:
|
|
||||||
- github.com/davecgh/go-spew/spew
|
|
||||||
|
|
||||||
linters:
|
|
||||||
#enable:
|
|
||||||
# - staticcheck
|
|
||||||
# - unused
|
|
||||||
# - gosimple
|
|
||||||
enable-all: true
|
|
||||||
disable:
|
|
||||||
- lll
|
|
||||||
disable-all: false
|
|
||||||
#presets:
|
|
||||||
# - bugs
|
|
||||||
# - unused
|
|
||||||
fast: false
|
|
||||||
|
|
||||||
issues:
|
|
||||||
# List of regexps of issue texts to exclude, empty list by default.
|
|
||||||
# But independently from this option we use default exclude patterns,
|
|
||||||
# it can be disabled by `exclude-use-default: false`. To list all
|
|
||||||
# excluded by default patterns execute `golangci-lint run --help`
|
|
||||||
exclude:
|
|
||||||
- "`parseTained` is unused"
|
|
||||||
- "`parseState` is unused"
|
|
||||||
|
|
||||||
# Independently from option `exclude` we use default exclude patterns,
|
|
||||||
# it can be disabled by this option. To list all
|
|
||||||
# excluded by default patterns execute `golangci-lint run --help`.
|
|
||||||
# Default value for this option is false.
|
|
||||||
exclude-use-default: false
|
|
||||||
|
|
||||||
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
|
|
||||||
max-per-linter: 0
|
|
||||||
|
|
||||||
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
|
|
||||||
max-same: 0
|
|
||||||
|
|
||||||
# Show only new issues: if there are unstaged changes or untracked files,
|
|
||||||
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
|
|
||||||
# It's a super-useful option for integration of golangci-lint into existing
|
|
||||||
# large codebase. It's not practical to fix all existing issues at the moment
|
|
||||||
# of integration: much better don't allow issues in new code.
|
|
||||||
# Default is false.
|
|
||||||
new: false
|
|
||||||
|
|
||||||
# Show only new issues created after git revision `REV`
|
|
||||||
#new-from-rev: REV
|
|
||||||
|
|
||||||
# Show only new issues created in git patch with set file path.
|
|
||||||
#new-from-patch: path/to/patch/file
|
|
|
@ -1,24 +0,0 @@
|
||||||
language: go
|
|
||||||
|
|
||||||
go:
|
|
||||||
- "1.13"
|
|
||||||
- "1.14"
|
|
||||||
- tip
|
|
||||||
|
|
||||||
env:
|
|
||||||
- GO111MODULE=on
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- go get github.com/axw/gocov/gocov
|
|
||||||
- go get github.com/mattn/goveralls
|
|
||||||
- go get golang.org/x/tools/cmd/cover
|
|
||||||
- go get golang.org/x/tools/cmd/goimports
|
|
||||||
- wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh
|
|
||||||
|
|
||||||
script:
|
|
||||||
- test -z "$(goimports -d ./ 2>&1)"
|
|
||||||
- ./bin/golangci-lint run
|
|
||||||
- go test -v -race ./...
|
|
||||||
|
|
||||||
after_success:
|
|
||||||
- test "$TRAVIS_GO_VERSION" = "1.14" && goveralls -service=travis-ci
|
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2020 Djarvur
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
|
@ -1,75 +0,0 @@
|
||||||
= err113 image:https://godoc.org/github.com/Djarvur/go-err113?status.svg["GoDoc",link="http://godoc.org/github.com/Djarvur/go-err113"] image:https://travis-ci.org/Djarvur/go-err113.svg["Build Status",link="https://travis-ci.org/Djarvur/go-err113"] image:https://coveralls.io/repos/Djarvur/go-err113/badge.svg?branch=master&service=github["Coverage Status",link="https://coveralls.io/github/Djarvur/go-err113?branch=master"]
|
|
||||||
Daniel Podolsky
|
|
||||||
:toc:
|
|
||||||
|
|
||||||
Golang linter to check the errors handling expressions
|
|
||||||
|
|
||||||
== Details
|
|
||||||
|
|
||||||
Starting from Go 1.13 the standard `error` type behaviour was changed: one `error` could be derived from another with `fmt.Errorf()` method using `%w` format specifier.
|
|
||||||
|
|
||||||
So the errors hierarchy could be built for flexible and responsible errors processing.
|
|
||||||
|
|
||||||
And to make this possible at least two simple rules should be followed:
|
|
||||||
|
|
||||||
1. `error` values should not be compared directly but with `errors.Is()` method.
|
|
||||||
1. `error` should not be created dynamically from scratch but by the wrapping the static (package-level) error.
|
|
||||||
|
|
||||||
This linter is checking the code for these 2 rules compliance.
|
|
||||||
|
|
||||||
=== Reports
|
|
||||||
|
|
||||||
So, `err113` reports every `==` and `!=` comparison for exact `error` type variables except comparison to `nil` and `io.EOF`.
|
|
||||||
|
|
||||||
Also, any call of `errors.New()` and `fmt.Errorf()` methods are reported except the calls used to initialise package-level variables and the `fmt.Errorf()` calls wrapping the other errors.
|
|
||||||
|
|
||||||
Note: non-standard packages, like `github.com/pkg/errors` are ignored completely.
|
|
||||||
|
|
||||||
== Install
|
|
||||||
|
|
||||||
```
|
|
||||||
go get -u github.com/Djarvur/go-err113/cmd/err113
|
|
||||||
```
|
|
||||||
|
|
||||||
== Usage
|
|
||||||
|
|
||||||
Defined by link:https://pkg.go.dev/golang.org/x/tools/go/analysis/singlechecker[singlechecker] package.
|
|
||||||
|
|
||||||
```
|
|
||||||
err113: checks the error handling rules according to the Go 1.13 new error type
|
|
||||||
|
|
||||||
Usage: err113 [-flag] [package]
|
|
||||||
|
|
||||||
|
|
||||||
Flags:
|
|
||||||
-V print version and exit
|
|
||||||
-all
|
|
||||||
no effect (deprecated)
|
|
||||||
-c int
|
|
||||||
display offending line with this many lines of context (default -1)
|
|
||||||
-cpuprofile string
|
|
||||||
write CPU profile to this file
|
|
||||||
-debug string
|
|
||||||
debug flags, any subset of "fpstv"
|
|
||||||
-fix
|
|
||||||
apply all suggested fixes
|
|
||||||
-flags
|
|
||||||
print analyzer flags in JSON
|
|
||||||
-json
|
|
||||||
emit JSON output
|
|
||||||
-memprofile string
|
|
||||||
write memory profile to this file
|
|
||||||
-source
|
|
||||||
no effect (deprecated)
|
|
||||||
-tags string
|
|
||||||
no effect (deprecated)
|
|
||||||
-trace string
|
|
||||||
write trace log to this file
|
|
||||||
-v no effect (deprecated)
|
|
||||||
```
|
|
||||||
|
|
||||||
== Thanks
|
|
||||||
|
|
||||||
To link:https://github.com/quasilyte[Iskander (Alex) Sharipov] for the really useful advices.
|
|
||||||
|
|
||||||
To link:https://github.com/jackwhelpton[Jack Whelpton] for the bugfix provided.
|
|
|
@ -1,123 +0,0 @@
|
||||||
package err113
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"go/ast"
|
|
||||||
"go/token"
|
|
||||||
"go/types"
|
|
||||||
|
|
||||||
"golang.org/x/tools/go/analysis"
|
|
||||||
)
|
|
||||||
|
|
||||||
func inspectComparision(pass *analysis.Pass, n ast.Node) bool { // nolint: unparam
|
|
||||||
// check whether the call expression matches time.Now().Sub()
|
|
||||||
be, ok := n.(*ast.BinaryExpr)
|
|
||||||
if !ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if it is a comparison operation
|
|
||||||
if be.Op != token.EQL && be.Op != token.NEQ {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !areBothErrors(be.X, be.Y, pass.TypesInfo) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
oldExpr := render(pass.Fset, be)
|
|
||||||
|
|
||||||
negate := ""
|
|
||||||
if be.Op == token.NEQ {
|
|
||||||
negate = "!"
|
|
||||||
}
|
|
||||||
|
|
||||||
newExpr := fmt.Sprintf("%s%s.Is(%s, %s)", negate, "errors", rawString(be.X), rawString(be.Y))
|
|
||||||
|
|
||||||
pass.Report(
|
|
||||||
analysis.Diagnostic{
|
|
||||||
Pos: be.Pos(),
|
|
||||||
Message: fmt.Sprintf("do not compare errors directly %q, use %q instead", oldExpr, newExpr),
|
|
||||||
SuggestedFixes: []analysis.SuggestedFix{
|
|
||||||
{
|
|
||||||
Message: fmt.Sprintf("should replace %q with %q", oldExpr, newExpr),
|
|
||||||
TextEdits: []analysis.TextEdit{
|
|
||||||
{
|
|
||||||
Pos: be.Pos(),
|
|
||||||
End: be.End(),
|
|
||||||
NewText: []byte(newExpr),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func isError(v ast.Expr, info *types.Info) bool {
|
|
||||||
if intf, ok := info.TypeOf(v).Underlying().(*types.Interface); ok {
|
|
||||||
return intf.NumMethods() == 1 && intf.Method(0).FullName() == "(error).Error"
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isEOF(ex ast.Expr, info *types.Info) bool {
|
|
||||||
se, ok := ex.(*ast.SelectorExpr)
|
|
||||||
if !ok || se.Sel.Name != "EOF" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if ep, ok := asImportedName(se.X, info); !ok || ep != "io" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func asImportedName(ex ast.Expr, info *types.Info) (string, bool) {
|
|
||||||
ei, ok := ex.(*ast.Ident)
|
|
||||||
if !ok {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
ep, ok := info.ObjectOf(ei).(*types.PkgName)
|
|
||||||
if !ok {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
return ep.Imported().Path(), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func areBothErrors(x, y ast.Expr, typesInfo *types.Info) bool {
|
|
||||||
// check that both left and right hand side are not nil
|
|
||||||
if typesInfo.Types[x].IsNil() || typesInfo.Types[y].IsNil() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// check that both left and right hand side are not io.EOF
|
|
||||||
if isEOF(x, typesInfo) || isEOF(y, typesInfo) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// check that both left and right hand side are errors
|
|
||||||
if !isError(x, typesInfo) && !isError(y, typesInfo) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func rawString(x ast.Expr) string {
|
|
||||||
switch t := x.(type) {
|
|
||||||
case *ast.Ident:
|
|
||||||
return t.Name
|
|
||||||
case *ast.SelectorExpr:
|
|
||||||
return fmt.Sprintf("%s.%s", rawString(t.X), t.Sel.Name)
|
|
||||||
case *ast.CallExpr:
|
|
||||||
return fmt.Sprintf("%s()", rawString(t.Fun))
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s", x)
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
package err113
|
|
||||||
|
|
||||||
import (
|
|
||||||
"go/ast"
|
|
||||||
"go/types"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/tools/go/analysis"
|
|
||||||
)
|
|
||||||
|
|
||||||
var methods2check = map[string]map[string]func(*ast.CallExpr, *types.Info) bool{ // nolint: gochecknoglobals
|
|
||||||
"errors": {"New": justTrue},
|
|
||||||
"fmt": {"Errorf": checkWrap},
|
|
||||||
}
|
|
||||||
|
|
||||||
func justTrue(*ast.CallExpr, *types.Info) bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkWrap(ce *ast.CallExpr, info *types.Info) bool {
|
|
||||||
return !(len(ce.Args) > 0 && strings.Contains(toString(ce.Args[0], info), `%w`))
|
|
||||||
}
|
|
||||||
|
|
||||||
func inspectDefinition(pass *analysis.Pass, tlds map[*ast.CallExpr]struct{}, n ast.Node) bool { //nolint: unparam
|
|
||||||
// check whether the call expression matches time.Now().Sub()
|
|
||||||
ce, ok := n.(*ast.CallExpr)
|
|
||||||
if !ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok = tlds[ce]; ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn, ok := ce.Fun.(*ast.SelectorExpr)
|
|
||||||
if !ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fxName, ok := asImportedName(fn.X, pass.TypesInfo)
|
|
||||||
if !ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
methods, ok := methods2check[fxName]
|
|
||||||
if !ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
checkFunc, ok := methods[fn.Sel.Name]
|
|
||||||
if !ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !checkFunc(ce, pass.TypesInfo) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
pass.Reportf(
|
|
||||||
ce.Pos(),
|
|
||||||
"do not define dynamic errors, use wrapped static errors instead: %q",
|
|
||||||
render(pass.Fset, ce),
|
|
||||||
)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func toString(ex ast.Expr, info *types.Info) string {
|
|
||||||
if tv, ok := info.Types[ex]; ok && tv.Value != nil {
|
|
||||||
return tv.Value.ExactString()
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
// Package err113 is a Golang linter to check the errors handling expressions
|
|
||||||
package err113
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"go/ast"
|
|
||||||
"go/printer"
|
|
||||||
"go/token"
|
|
||||||
|
|
||||||
"golang.org/x/tools/go/analysis"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewAnalyzer creates a new analysis.Analyzer instance tuned to run err113 checks.
|
|
||||||
func NewAnalyzer() *analysis.Analyzer {
|
|
||||||
return &analysis.Analyzer{
|
|
||||||
Name: "err113",
|
|
||||||
Doc: "checks the error handling rules according to the Go 1.13 new error type",
|
|
||||||
Run: run,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func run(pass *analysis.Pass) (interface{}, error) {
|
|
||||||
for _, file := range pass.Files {
|
|
||||||
tlds := enumerateFileDecls(file)
|
|
||||||
|
|
||||||
ast.Inspect(
|
|
||||||
file,
|
|
||||||
func(n ast.Node) bool {
|
|
||||||
return inspectComparision(pass, n) &&
|
|
||||||
inspectDefinition(pass, tlds, n)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// render returns the pretty-print of the given node.
|
|
||||||
func render(fset *token.FileSet, x interface{}) string {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
if err := printer.Fprint(&buf, fset, x); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func enumerateFileDecls(f *ast.File) map[*ast.CallExpr]struct{} {
|
|
||||||
res := make(map[*ast.CallExpr]struct{})
|
|
||||||
|
|
||||||
var ces []*ast.CallExpr // nolint: prealloc
|
|
||||||
|
|
||||||
for _, d := range f.Decls {
|
|
||||||
ces = append(ces, enumerateDeclVars(d)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ce := range ces {
|
|
||||||
res[ce] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func enumerateDeclVars(d ast.Decl) (res []*ast.CallExpr) {
|
|
||||||
td, ok := d.(*ast.GenDecl)
|
|
||||||
if !ok || td.Tok != token.VAR {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range td.Specs {
|
|
||||||
res = append(res, enumerateSpecValues(s)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func enumerateSpecValues(s ast.Spec) (res []*ast.CallExpr) {
|
|
||||||
vs, ok := s.(*ast.ValueSpec)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range vs.Values {
|
|
||||||
if ce, ok := v.(*ast.CallExpr); ok {
|
|
||||||
res = append(res, ce)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
language: go
|
|
||||||
|
|
||||||
go:
|
|
||||||
- 1.6.x
|
|
||||||
- 1.7.x
|
|
||||||
- 1.8.x
|
|
||||||
- 1.9.x
|
|
||||||
- 1.10.x
|
|
||||||
- 1.11.x
|
|
||||||
- 1.12.x
|
|
||||||
- tip
|
|
||||||
|
|
||||||
# Setting sudo access to false will let Travis CI use containers rather than
|
|
||||||
# VMs to run the tests. For more details see:
|
|
||||||
# - http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
|
||||||
# - http://docs.travis-ci.com/user/workers/standard-infrastructure/
|
|
||||||
sudo: false
|
|
||||||
|
|
||||||
script:
|
|
||||||
- make setup
|
|
||||||
- make test
|
|
||||||
|
|
||||||
notifications:
|
|
||||||
webhooks:
|
|
||||||
urls:
|
|
||||||
- https://webhooks.gitter.im/e/06e3328629952dabe3e0
|
|
||||||
on_success: change # options: [always|never|change] default: always
|
|
||||||
on_failure: always # options: [always|never|change] default: always
|
|
||||||
on_start: never # options: [always|never|change] default: always
|
|
|
@ -1,109 +0,0 @@
|
||||||
# 1.5.0 (2019-09-11)
|
|
||||||
|
|
||||||
## Added
|
|
||||||
|
|
||||||
- #103: Add basic fuzzing for `NewVersion()` (thanks @jesse-c)
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
|
|
||||||
- #82: Clarify wildcard meaning in range constraints and update tests for it (thanks @greysteil)
|
|
||||||
- #83: Clarify caret operator range for pre-1.0.0 dependencies (thanks @greysteil)
|
|
||||||
- #72: Adding docs comment pointing to vert for a cli
|
|
||||||
- #71: Update the docs on pre-release comparator handling
|
|
||||||
- #89: Test with new go versions (thanks @thedevsaddam)
|
|
||||||
- #87: Added $ to ValidPrerelease for better validation (thanks @jeremycarroll)
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
|
|
||||||
- #78: Fix unchecked error in example code (thanks @ravron)
|
|
||||||
- #70: Fix the handling of pre-releases and the 0.0.0 release edge case
|
|
||||||
- #97: Fixed copyright file for proper display on GitHub
|
|
||||||
- #107: Fix handling prerelease when sorting alphanum and num
|
|
||||||
- #109: Fixed where Validate sometimes returns wrong message on error
|
|
||||||
|
|
||||||
# 1.4.2 (2018-04-10)
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
- #72: Updated the docs to point to vert for a console appliaction
|
|
||||||
- #71: Update the docs on pre-release comparator handling
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
- #70: Fix the handling of pre-releases and the 0.0.0 release edge case
|
|
||||||
|
|
||||||
# 1.4.1 (2018-04-02)
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
- Fixed #64: Fix pre-release precedence issue (thanks @uudashr)
|
|
||||||
|
|
||||||
# 1.4.0 (2017-10-04)
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
- #61: Update NewVersion to parse ints with a 64bit int size (thanks @zknill)
|
|
||||||
|
|
||||||
# 1.3.1 (2017-07-10)
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
- Fixed #57: number comparisons in prerelease sometimes inaccurate
|
|
||||||
|
|
||||||
# 1.3.0 (2017-05-02)
|
|
||||||
|
|
||||||
## Added
|
|
||||||
- #45: Added json (un)marshaling support (thanks @mh-cbon)
|
|
||||||
- Stability marker. See https://masterminds.github.io/stability/
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
- #51: Fix handling of single digit tilde constraint (thanks @dgodd)
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
- #55: The godoc icon moved from png to svg
|
|
||||||
|
|
||||||
# 1.2.3 (2017-04-03)
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
- #46: Fixed 0.x.x and 0.0.x in constraints being treated as *
|
|
||||||
|
|
||||||
# Release 1.2.2 (2016-12-13)
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
- #34: Fixed issue where hyphen range was not working with pre-release parsing.
|
|
||||||
|
|
||||||
# Release 1.2.1 (2016-11-28)
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
- #24: Fixed edge case issue where constraint "> 0" does not handle "0.0.1-alpha"
|
|
||||||
properly.
|
|
||||||
|
|
||||||
# Release 1.2.0 (2016-11-04)
|
|
||||||
|
|
||||||
## Added
|
|
||||||
- #20: Added MustParse function for versions (thanks @adamreese)
|
|
||||||
- #15: Added increment methods on versions (thanks @mh-cbon)
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
- Issue #21: Per the SemVer spec (section 9) a pre-release is unstable and
|
|
||||||
might not satisfy the intended compatibility. The change here ignores pre-releases
|
|
||||||
on constraint checks (e.g., ~ or ^) when a pre-release is not part of the
|
|
||||||
constraint. For example, `^1.2.3` will ignore pre-releases while
|
|
||||||
`^1.2.3-alpha` will include them.
|
|
||||||
|
|
||||||
# Release 1.1.1 (2016-06-30)
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
- Issue #9: Speed up version comparison performance (thanks @sdboyer)
|
|
||||||
- Issue #8: Added benchmarks (thanks @sdboyer)
|
|
||||||
- Updated Go Report Card URL to new location
|
|
||||||
- Updated Readme to add code snippet formatting (thanks @mh-cbon)
|
|
||||||
- Updating tagging to v[SemVer] structure for compatibility with other tools.
|
|
||||||
|
|
||||||
# Release 1.1.0 (2016-03-11)
|
|
||||||
|
|
||||||
- Issue #2: Implemented validation to provide reasons a versions failed a
|
|
||||||
constraint.
|
|
||||||
|
|
||||||
# Release 1.0.1 (2015-12-31)
|
|
||||||
|
|
||||||
- Fixed #1: * constraint failing on valid versions.
|
|
||||||
|
|
||||||
# Release 1.0.0 (2015-10-20)
|
|
||||||
|
|
||||||
- Initial release
|
|
|
@ -1,19 +0,0 @@
|
||||||
Copyright (C) 2014-2019, Matt Butcher and Matt Farina
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
|
@ -1,36 +0,0 @@
|
||||||
.PHONY: setup
|
|
||||||
setup:
|
|
||||||
go get -u gopkg.in/alecthomas/gometalinter.v1
|
|
||||||
gometalinter.v1 --install
|
|
||||||
|
|
||||||
.PHONY: test
|
|
||||||
test: validate lint
|
|
||||||
@echo "==> Running tests"
|
|
||||||
go test -v
|
|
||||||
|
|
||||||
.PHONY: validate
|
|
||||||
validate:
|
|
||||||
@echo "==> Running static validations"
|
|
||||||
@gometalinter.v1 \
|
|
||||||
--disable-all \
|
|
||||||
--enable deadcode \
|
|
||||||
--severity deadcode:error \
|
|
||||||
--enable gofmt \
|
|
||||||
--enable gosimple \
|
|
||||||
--enable ineffassign \
|
|
||||||
--enable misspell \
|
|
||||||
--enable vet \
|
|
||||||
--tests \
|
|
||||||
--vendor \
|
|
||||||
--deadline 60s \
|
|
||||||
./... || exit_code=1
|
|
||||||
|
|
||||||
.PHONY: lint
|
|
||||||
lint:
|
|
||||||
@echo "==> Running linters"
|
|
||||||
@gometalinter.v1 \
|
|
||||||
--disable-all \
|
|
||||||
--enable golint \
|
|
||||||
--vendor \
|
|
||||||
--deadline 60s \
|
|
||||||
./... || :
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue