Compare commits
12 Commits
v0.108.0-b
...
websvc-con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ade25d227 | ||
|
|
b448a3b5dc | ||
|
|
c20ca9b85e | ||
|
|
dffffec9d4 | ||
|
|
1989c91c07 | ||
|
|
dbfc8ae362 | ||
|
|
d74ba3cb9d | ||
|
|
abcbdbed29 | ||
|
|
8a65848da4 | ||
|
|
b018e150e7 | ||
|
|
dbdae5b4fc | ||
|
|
27bd8bc58b |
52
CHANGELOG.md
52
CHANGELOG.md
@@ -15,22 +15,6 @@ and this project adheres to
|
|||||||
## [v0.108.0] - TBA (APPROX.)
|
## [v0.108.0] - TBA (APPROX.)
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--
|
|
||||||
## [v0.107.16] - 2022-11-02 (APPROX.)
|
|
||||||
|
|
||||||
See also the [v0.107.16 GitHub milestone][ms-v0.107.15].
|
|
||||||
|
|
||||||
[ms-v0.107.16]: https://github.com/AdguardTeam/AdGuardHome/milestone/52?closed=1
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v0.107.15] - 2022-10-03
|
|
||||||
|
|
||||||
See also the [v0.107.15 GitHub milestone][ms-v0.107.15].
|
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- As an additional CSRF protection measure, AdGuard Home now ensures that
|
- As an additional CSRF protection measure, AdGuard Home now ensures that
|
||||||
@@ -38,34 +22,21 @@ See also the [v0.107.15 GitHub milestone][ms-v0.107.15].
|
|||||||
/control/stats_reset` requests) do not have a `Content-Type` header set on
|
/control/stats_reset` requests) do not have a `Content-Type` header set on
|
||||||
them ([#4970]).
|
them ([#4970]).
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
#### Experimental HTTP/3 Support
|
|
||||||
|
|
||||||
See [#3955] and the related issues for more details. These features are still
|
|
||||||
experimental and may break or change in the future.
|
|
||||||
|
|
||||||
- DNS-over-HTTP/3 DNS and web UI client request support. This feature must be
|
|
||||||
explicitly enabled by setting the new property `dns.serve_http3` in the
|
|
||||||
configuration file to `true`.
|
|
||||||
- DNS-over-HTTP upstreams can now upgrade to HTTP/3 if the new configuration
|
|
||||||
file property `use_http3_upstreams` is set to `true`.
|
|
||||||
- Upstreams with forced DNS-over-HTTP/3 and no fallback to prior HTTP versions
|
|
||||||
using the `h3://` scheme.
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- User-specific blocked services not applying correctly ([#4945], [#4982],
|
|
||||||
[#4983]).
|
|
||||||
- `only application/json is allowed` errors in various APIs ([#4970]).
|
- `only application/json is allowed` errors in various APIs ([#4970]).
|
||||||
|
|
||||||
[#3955]: https://github.com/AdguardTeam/AdGuardHome/issues/3955
|
|
||||||
[#4945]: https://github.com/AdguardTeam/AdGuardHome/issues/4945
|
|
||||||
[#4970]: https://github.com/AdguardTeam/AdGuardHome/issues/4970
|
[#4970]: https://github.com/AdguardTeam/AdGuardHome/issues/4970
|
||||||
[#4982]: https://github.com/AdguardTeam/AdGuardHome/issues/4982
|
|
||||||
[#4983]: https://github.com/AdguardTeam/AdGuardHome/issues/4983
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
## [v0.107.15] - 2022-10-26 (APPROX.)
|
||||||
|
|
||||||
|
See also the [v0.107.15 GitHub milestone][ms-v0.107.15].
|
||||||
|
|
||||||
[ms-v0.107.15]: https://github.com/AdguardTeam/AdGuardHome/milestone/51?closed=1
|
[ms-v0.107.15]: https://github.com/AdguardTeam/AdGuardHome/milestone/51?closed=1
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1325,12 +1296,11 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
|
|||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.16...HEAD
|
|
||||||
[v0.107.16]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.15...v0.107.15
|
|
||||||
-->
|
|
||||||
|
|
||||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.15...HEAD
|
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.15...HEAD
|
||||||
[v0.107.15]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.14...v0.107.15
|
[v0.107.15]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.14...v0.107.15
|
||||||
|
-->
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.14...HEAD
|
||||||
[v0.107.14]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...v0.107.14
|
[v0.107.14]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...v0.107.14
|
||||||
[v0.107.13]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.12...v0.107.13
|
[v0.107.13]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.12...v0.107.13
|
||||||
[v0.107.12]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.11...v0.107.12
|
[v0.107.12]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.11...v0.107.12
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -34,7 +34,7 @@ YARN_INSTALL_FLAGS = $(YARN_FLAGS) --network-timeout 120000 --silent\
|
|||||||
--ignore-engines --ignore-optional --ignore-platform\
|
--ignore-engines --ignore-optional --ignore-platform\
|
||||||
--ignore-scripts
|
--ignore-scripts
|
||||||
|
|
||||||
V1API = 0
|
NEXTAPI = 0
|
||||||
|
|
||||||
# Macros for the build-release target. If FRONTEND_PREBUILT is 0, the
|
# Macros for the build-release target. If FRONTEND_PREBUILT is 0, the
|
||||||
# default, the macro $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT)) expands
|
# default, the macro $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT)) expands
|
||||||
@@ -63,7 +63,7 @@ ENV = env\
|
|||||||
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
|
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
|
||||||
RACE='$(RACE)'\
|
RACE='$(RACE)'\
|
||||||
SIGN='$(SIGN)'\
|
SIGN='$(SIGN)'\
|
||||||
V1API='$(V1API)'\
|
NEXTAPI='$(NEXTAPI)'\
|
||||||
VERBOSE='$(VERBOSE)'\
|
VERBOSE='$(VERBOSE)'\
|
||||||
VERSION='$(VERSION)'\
|
VERSION='$(VERSION)'\
|
||||||
|
|
||||||
|
|||||||
16
go.mod
16
go.mod
@@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
|
|||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AdguardTeam/dnsproxy v0.45.2
|
github.com/AdguardTeam/dnsproxy v0.44.0
|
||||||
github.com/AdguardTeam/golibs v0.10.9
|
github.com/AdguardTeam/golibs v0.10.9
|
||||||
github.com/AdguardTeam/urlfilter v0.16.0
|
github.com/AdguardTeam/urlfilter v0.16.0
|
||||||
github.com/NYTimes/gziphandler v1.1.1
|
github.com/NYTimes/gziphandler v1.1.1
|
||||||
@@ -18,7 +18,7 @@ require (
|
|||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20220822114210-de18a9d48e84
|
github.com/insomniacslk/dhcp v0.0.0-20220822114210-de18a9d48e84
|
||||||
github.com/kardianos/service v1.2.1
|
github.com/kardianos/service v1.2.1
|
||||||
github.com/lucas-clemente/quic-go v0.29.1
|
github.com/lucas-clemente/quic-go v0.29.0
|
||||||
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118
|
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118
|
||||||
github.com/mdlayher/netlink v1.6.0
|
github.com/mdlayher/netlink v1.6.0
|
||||||
// TODO(a.garipov): This package is deprecated; find a new one or use
|
// TODO(a.garipov): This package is deprecated; find a new one or use
|
||||||
@@ -28,10 +28,10 @@ require (
|
|||||||
github.com/stretchr/testify v1.8.0
|
github.com/stretchr/testify v1.8.0
|
||||||
github.com/ti-mo/netfilter v0.4.0
|
github.com/ti-mo/netfilter v0.4.0
|
||||||
go.etcd.io/bbolt v1.3.6
|
go.etcd.io/bbolt v1.3.6
|
||||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be
|
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
|
||||||
golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9
|
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91
|
||||||
golang.org/x/net v0.0.0-20220927171203-f486391704dc
|
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c
|
||||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec
|
golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
howett.net/plist v1.0.0
|
howett.net/plist v1.0.0
|
||||||
@@ -43,12 +43,10 @@ require (
|
|||||||
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
|
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect
|
||||||
github.com/ameshkov/dnsstamps v1.0.3 // indirect
|
github.com/ameshkov/dnsstamps v1.0.3 // indirect
|
||||||
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect
|
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect
|
||||||
github.com/bluele/gcache v0.0.2 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
github.com/josharian/native v1.0.0 // indirect
|
github.com/josharian/native v1.0.0 // indirect
|
||||||
github.com/marten-seemann/qpack v0.2.1 // indirect
|
|
||||||
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
|
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
|
||||||
github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
|
github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
|
||||||
github.com/mdlayher/packet v1.0.0 // indirect
|
github.com/mdlayher/packet v1.0.0 // indirect
|
||||||
@@ -59,7 +57,7 @@ require (
|
|||||||
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/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect
|
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220922195421-2adab6b8c60e // indirect
|
golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9 // indirect
|
||||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect
|
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
golang.org/x/tools v0.1.12 // indirect
|
golang.org/x/tools v0.1.12 // indirect
|
||||||
|
|||||||
35
go.sum
35
go.sum
@@ -1,5 +1,5 @@
|
|||||||
github.com/AdguardTeam/dnsproxy v0.45.2 h1:K9BXkQAfAKjrzbWbczpA2IA1owLe/edv0nG0e2+Esko=
|
github.com/AdguardTeam/dnsproxy v0.44.0 h1:JzIxEXF4OyJq4wZVHeZkM1af4VfuwcgrUzjgdBGljsE=
|
||||||
github.com/AdguardTeam/dnsproxy v0.45.2/go.mod h1:h+0r4GDvHHY2Wu6r7oqva+O37h00KofYysfzy1TEXFE=
|
github.com/AdguardTeam/dnsproxy v0.44.0/go.mod h1:HsxYYW/bC8uo+4eX9pRW21hFD6gWZdrvcfBb1R6/AzU=
|
||||||
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
||||||
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
|
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
|
||||||
github.com/AdguardTeam/golibs v0.10.9 h1:F9oP2da0dQ9RQDM1lGR7LxUTfUWu8hEFOs4icwAkKM0=
|
github.com/AdguardTeam/golibs v0.10.9 h1:F9oP2da0dQ9RQDM1lGR7LxUTfUWu8hEFOs4icwAkKM0=
|
||||||
@@ -23,8 +23,6 @@ github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1O
|
|||||||
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
|
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
|
||||||
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM=
|
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM=
|
||||||
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=
|
github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=
|
||||||
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
|
|
||||||
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
|
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
@@ -90,10 +88,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lucas-clemente/quic-go v0.29.1 h1:Z+WMJ++qMLhvpFkRZA+jl3BTxUjm415YBmWanXB8zP0=
|
github.com/lucas-clemente/quic-go v0.29.0 h1:Vw0mGTfmWqGzh4jx/kMymsIkFK6rErFVmg+t9RLrnZE=
|
||||||
github.com/lucas-clemente/quic-go v0.29.1/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE=
|
github.com/lucas-clemente/quic-go v0.29.0/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE=
|
||||||
github.com/marten-seemann/qpack v0.2.1 h1:jvTsT/HpCn2UZJdP+UUB53FfUUgeOyG5K1ns0OJOGVs=
|
|
||||||
github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc=
|
|
||||||
github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM=
|
github.com/marten-seemann/qtls-go1-18 v0.1.2 h1:JH6jmzbduz0ITVQ7ShevK10Av5+jBEKAHMntXmIV7kM=
|
||||||
github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
|
github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4=
|
||||||
github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU=
|
github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU=
|
||||||
@@ -129,7 +125,6 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
|||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
|
||||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
@@ -173,16 +168,16 @@ go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
|||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
|
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
|
||||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9 h1:lNtcVz/3bOstm7Vebox+5m3nLh/BYWnhmc3AhXOW6oI=
|
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw=
|
||||||
golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220922195421-2adab6b8c60e h1:WhB000cGjOfbJiedMGvJkMTclI18VD69w27k+sceql8=
|
golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9 h1:VtCrPQXM5Wo9l7XN64SjBMczl48j8mkP+2e3OhYlz+0=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220922195421-2adab6b8c60e/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
@@ -194,7 +189,6 @@ golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||||||
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
|
||||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
@@ -206,8 +200,8 @@ golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||||||
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ=
|
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c h1:yKufUcDwucU5urd+50/Opbt4AYpqthk7wHpHok8f1lo=
|
||||||
golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -230,7 +224,6 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -254,8 +247,8 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
|
golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77 h1:C1tElbkWrsSkn3IRl1GCW/gETw1TywWIPgwZtXTZbYg=
|
||||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
|||||||
33
internal/aghchan/aghchan.go
Normal file
33
internal/aghchan/aghchan.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// Package aghchan contains channel utilities.
|
||||||
|
package aghchan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Receive returns an error if it cannot receive a value form c before timeout
|
||||||
|
// runs out.
|
||||||
|
func Receive[T any](c <-chan T, timeout time.Duration) (v T, ok bool, err error) {
|
||||||
|
var zero T
|
||||||
|
timeoutCh := time.After(timeout)
|
||||||
|
select {
|
||||||
|
case <-timeoutCh:
|
||||||
|
// TODO(a.garipov): Consider implementing [errors.Aser] for
|
||||||
|
// os.ErrTimeout.
|
||||||
|
return zero, false, fmt.Errorf("did not receive after %s", timeout)
|
||||||
|
case v, ok = <-c:
|
||||||
|
return v, ok, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustReceive panics if it cannot receive a value form c before timeout runs
|
||||||
|
// out.
|
||||||
|
func MustReceive[T any](c <-chan T, timeout time.Duration) (v T, ok bool) {
|
||||||
|
v, ok, err := Receive(c, timeout)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
@@ -10,9 +10,9 @@ import (
|
|||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghchan"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
|
||||||
"github.com/AdguardTeam/golibs/stringutil"
|
"github.com/AdguardTeam/golibs/stringutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/AdguardTeam/urlfilter"
|
"github.com/AdguardTeam/urlfilter"
|
||||||
@@ -163,15 +163,9 @@ func TestHostsContainer_refresh(t *testing.T) {
|
|||||||
checkRefresh := func(t *testing.T, want *HostsRecord) {
|
checkRefresh := func(t *testing.T, want *HostsRecord) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
var ok bool
|
upd, ok := aghchan.MustReceive(hc.Upd(), 1*time.Second)
|
||||||
var upd *netutil.IPMap
|
require.True(t, ok)
|
||||||
select {
|
require.NotNil(t, upd)
|
||||||
case upd, ok = <-hc.Upd():
|
|
||||||
require.True(t, ok)
|
|
||||||
require.NotNil(t, upd)
|
|
||||||
case <-time.After(1 * time.Second):
|
|
||||||
t.Fatal("did not receive after 1s")
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, 1, upd.Len())
|
assert.Equal(t, 1, upd.Len())
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package aghtest
|
package aghtest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
@@ -15,6 +16,8 @@ import (
|
|||||||
|
|
||||||
// Standard Library
|
// Standard Library
|
||||||
|
|
||||||
|
// Package fs
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ fs.FS = &FS{}
|
var _ fs.FS = &FS{}
|
||||||
|
|
||||||
@@ -58,6 +61,8 @@ func (fsys *StatFS) Stat(name string) (fs.FileInfo, error) {
|
|||||||
return fsys.OnStat(name)
|
return fsys.OnStat(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Package net
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ net.Listener = (*Listener)(nil)
|
var _ net.Listener = (*Listener)(nil)
|
||||||
|
|
||||||
@@ -83,32 +88,10 @@ func (l *Listener) Close() (err error) {
|
|||||||
return l.OnClose()
|
return l.OnClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Module dnsproxy
|
|
||||||
|
|
||||||
// type check
|
|
||||||
var _ upstream.Upstream = (*UpstreamMock)(nil)
|
|
||||||
|
|
||||||
// UpstreamMock is a mock [upstream.Upstream] implementation for tests.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Replace with all uses of Upstream with UpstreamMock and
|
|
||||||
// rename it to just Upstream.
|
|
||||||
type UpstreamMock struct {
|
|
||||||
OnAddress func() (addr string)
|
|
||||||
OnExchange func(req *dns.Msg) (resp *dns.Msg, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Address implements the [upstream.Upstream] interface for *UpstreamMock.
|
|
||||||
func (u *UpstreamMock) Address() (addr string) {
|
|
||||||
return u.OnAddress()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exchange implements the [upstream.Upstream] interface for *UpstreamMock.
|
|
||||||
func (u *UpstreamMock) Exchange(req *dns.Msg) (resp *dns.Msg, err error) {
|
|
||||||
return u.OnExchange(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Module AdGuardHome
|
// Module AdGuardHome
|
||||||
|
|
||||||
|
// Package aghos
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ aghos.FSWatcher = (*FSWatcher)(nil)
|
var _ aghos.FSWatcher = (*FSWatcher)(nil)
|
||||||
|
|
||||||
@@ -133,3 +116,57 @@ func (w *FSWatcher) Add(name string) (err error) {
|
|||||||
func (w *FSWatcher) Close() (err error) {
|
func (w *FSWatcher) Close() (err error) {
|
||||||
return w.OnClose()
|
return w.OnClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Package websvc
|
||||||
|
|
||||||
|
// ServiceWithConfig is a mock [websvc.ServiceWithConfig] implementation for
|
||||||
|
// tests.
|
||||||
|
type ServiceWithConfig[ConfigType any] struct {
|
||||||
|
OnStart func() (err error)
|
||||||
|
OnShutdown func(ctx context.Context) (err error)
|
||||||
|
OnConfig func() (c ConfigType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start implements the [websvc.ServiceWithConfig] interface for
|
||||||
|
// *ServiceWithConfig.
|
||||||
|
func (s *ServiceWithConfig[_]) Start() (err error) {
|
||||||
|
return s.OnStart()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown implements the [websvc.ServiceWithConfig] interface for
|
||||||
|
// *ServiceWithConfig.
|
||||||
|
func (s *ServiceWithConfig[_]) Shutdown(ctx context.Context) (err error) {
|
||||||
|
return s.OnShutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config implements the [websvc.ServiceWithConfig] interface for
|
||||||
|
// *ServiceWithConfig.
|
||||||
|
func (s *ServiceWithConfig[ConfigType]) Config() (c ConfigType) {
|
||||||
|
return s.OnConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module dnsproxy
|
||||||
|
|
||||||
|
// Package upstream
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ upstream.Upstream = (*UpstreamMock)(nil)
|
||||||
|
|
||||||
|
// UpstreamMock is a mock [upstream.Upstream] implementation for tests.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Replace with all uses of Upstream with UpstreamMock and
|
||||||
|
// rename it to just Upstream.
|
||||||
|
type UpstreamMock struct {
|
||||||
|
OnAddress func() (addr string)
|
||||||
|
OnExchange func(req *dns.Msg) (resp *dns.Msg, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address implements the [upstream.Upstream] interface for *UpstreamMock.
|
||||||
|
func (u *UpstreamMock) Address() (addr string) {
|
||||||
|
return u.OnAddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exchange implements the [upstream.Upstream] interface for *UpstreamMock.
|
||||||
|
func (u *UpstreamMock) Exchange(req *dns.Msg) (resp *dns.Msg, err error) {
|
||||||
|
return u.OnExchange(req)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package aghtest_test
|
package aghtest_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ aghos.FSWatcher = (*aghtest.FSWatcher)(nil)
|
var _ websvc.ServiceWithConfig[struct{}] = (*aghtest.ServiceWithConfig[struct{}])(nil)
|
||||||
|
|||||||
@@ -183,7 +183,15 @@ func (s *Server) accessListJSON() (j accessListJSON) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleAccessList(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleAccessList(w http.ResponseWriter, r *http.Request) {
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, s.accessListJSON())
|
j := s.accessListJSON()
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
err := json.NewEncoder(w).Encode(j)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusInternalServerError, "encoding response: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateAccessSet checks the internal accessListJSON lists. To search for
|
// validateAccessSet checks the internal accessListJSON lists. To search for
|
||||||
|
|||||||
@@ -201,10 +201,6 @@ type ServerConfig struct {
|
|||||||
// Register an HTTP handler
|
// Register an HTTP handler
|
||||||
HTTPRegister aghhttp.RegisterFunc
|
HTTPRegister aghhttp.RegisterFunc
|
||||||
|
|
||||||
// LocalPTRResolvers is a slice of addresses to be used as upstreams for
|
|
||||||
// resolving PTR queries for local addresses.
|
|
||||||
LocalPTRResolvers []string
|
|
||||||
|
|
||||||
// ResolveClients signals if the RDNS should resolve clients' addresses.
|
// ResolveClients signals if the RDNS should resolve clients' addresses.
|
||||||
ResolveClients bool
|
ResolveClients bool
|
||||||
|
|
||||||
@@ -212,12 +208,9 @@ type ServerConfig struct {
|
|||||||
// locally-served networks should be resolved via private PTR resolvers.
|
// locally-served networks should be resolved via private PTR resolvers.
|
||||||
UsePrivateRDNS bool
|
UsePrivateRDNS bool
|
||||||
|
|
||||||
// ServeHTTP3 defines if HTTP/3 is be allowed for incoming requests.
|
// LocalPTRResolvers is a slice of addresses to be used as upstreams for
|
||||||
ServeHTTP3 bool
|
// resolving PTR queries for local addresses.
|
||||||
|
LocalPTRResolvers []string
|
||||||
// UseHTTP3Upstreams defines if HTTP/3 is be allowed for DNS-over-HTTPS
|
|
||||||
// upstreams.
|
|
||||||
UseHTTP3Upstreams bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if any of ServerConfig values are zero, then default values from below are used
|
// if any of ServerConfig values are zero, then default values from below are used
|
||||||
@@ -233,7 +226,6 @@ func (s *Server) createProxyConfig() (conf proxy.Config, err error) {
|
|||||||
conf = proxy.Config{
|
conf = proxy.Config{
|
||||||
UDPListenAddr: srvConf.UDPListenAddrs,
|
UDPListenAddr: srvConf.UDPListenAddrs,
|
||||||
TCPListenAddr: srvConf.TCPListenAddrs,
|
TCPListenAddr: srvConf.TCPListenAddrs,
|
||||||
HTTP3: srvConf.ServeHTTP3,
|
|
||||||
Ratelimit: int(srvConf.Ratelimit),
|
Ratelimit: int(srvConf.Ratelimit),
|
||||||
RatelimitWhitelist: srvConf.RatelimitWhitelist,
|
RatelimitWhitelist: srvConf.RatelimitWhitelist,
|
||||||
RefuseAny: srvConf.RefuseAny,
|
RefuseAny: srvConf.RefuseAny,
|
||||||
@@ -332,20 +324,6 @@ func (s *Server) initDefaultSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpstreamHTTPVersions returns the HTTP versions for upstream configuration
|
|
||||||
// depending on configuration.
|
|
||||||
func UpstreamHTTPVersions(http3 bool) (v []upstream.HTTPVersion) {
|
|
||||||
if !http3 {
|
|
||||||
return upstream.DefaultHTTPVersions
|
|
||||||
}
|
|
||||||
|
|
||||||
return []upstream.HTTPVersion{
|
|
||||||
upstream.HTTPVersion3,
|
|
||||||
upstream.HTTPVersion2,
|
|
||||||
upstream.HTTPVersion11,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepareUpstreamSettings - prepares upstream DNS server settings
|
// prepareUpstreamSettings - prepares upstream DNS server settings
|
||||||
func (s *Server) prepareUpstreamSettings() error {
|
func (s *Server) prepareUpstreamSettings() error {
|
||||||
// We're setting a customized set of RootCAs
|
// We're setting a customized set of RootCAs
|
||||||
@@ -375,14 +353,12 @@ func (s *Server) prepareUpstreamSettings() error {
|
|||||||
upstreams = s.conf.UpstreamDNS
|
upstreams = s.conf.UpstreamDNS
|
||||||
}
|
}
|
||||||
|
|
||||||
httpVersions := UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams)
|
|
||||||
upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty)
|
upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty)
|
||||||
upstreamConfig, err := proxy.ParseUpstreamsConfig(
|
upstreamConfig, err := proxy.ParseUpstreamsConfig(
|
||||||
upstreams,
|
upstreams,
|
||||||
&upstream.Options{
|
&upstream.Options{
|
||||||
Bootstrap: s.conf.BootstrapDNS,
|
Bootstrap: s.conf.BootstrapDNS,
|
||||||
Timeout: s.conf.UpstreamTimeout,
|
Timeout: s.conf.UpstreamTimeout,
|
||||||
HTTPVersions: httpVersions,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -395,9 +371,8 @@ func (s *Server) prepareUpstreamSettings() error {
|
|||||||
uc, err = proxy.ParseUpstreamsConfig(
|
uc, err = proxy.ParseUpstreamsConfig(
|
||||||
defaultDNS,
|
defaultDNS,
|
||||||
&upstream.Options{
|
&upstream.Options{
|
||||||
Bootstrap: s.conf.BootstrapDNS,
|
Bootstrap: s.conf.BootstrapDNS,
|
||||||
Timeout: s.conf.UpstreamTimeout,
|
Timeout: s.conf.UpstreamTimeout,
|
||||||
HTTPVersions: httpVersions,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ func (s *Server) checkHostRules(host string, rrtype uint16, setts *filtering.Set
|
|||||||
}
|
}
|
||||||
|
|
||||||
// filterDNSResponse checks each resource record of the response's answer
|
// filterDNSResponse checks each resource record of the response's answer
|
||||||
// section from pctx and returns a non-nil res if at least one of canonical
|
// section from pctx and returns a non-nil res if at least one of canonnical
|
||||||
// names or IP addresses in it matches the filtering rules.
|
// names or IP addresses in it matches the filtering rules.
|
||||||
func (s *Server) filterDNSResponse(
|
func (s *Server) filterDNSResponse(
|
||||||
pctx *proxy.DNSContext,
|
pctx *proxy.DNSContext,
|
||||||
|
|||||||
@@ -112,7 +112,13 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
DefautLocalPTRUpstreams: defLocalPTRUps,
|
DefautLocalPTRUpstreams: defLocalPTRUps,
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
if err = json.NewEncoder(w).Encode(resp); err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusInternalServerError, "json.Encoder: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (req *jsonDNSConfig) checkBlockingMode() (err error) {
|
func (req *jsonDNSConfig) checkBlockingMode() (err error) {
|
||||||
@@ -343,10 +349,7 @@ func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err erro
|
|||||||
|
|
||||||
conf, err = proxy.ParseUpstreamsConfig(
|
conf, err = proxy.ParseUpstreamsConfig(
|
||||||
upstreams,
|
upstreams,
|
||||||
&upstream.Options{
|
&upstream.Options{Bootstrap: []string{}, Timeout: DefaultTimeout},
|
||||||
Bootstrap: []string{},
|
|
||||||
Timeout: DefaultTimeout,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -409,15 +412,7 @@ func ValidateUpstreamsPrivate(upstreams []string, privateNets netutil.SubnetSet)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var protocols = []string{
|
var protocols = []string{"udp://", "tcp://", "tls://", "https://", "sdns://", "quic://"}
|
||||||
"h3://",
|
|
||||||
"https://",
|
|
||||||
"quic://",
|
|
||||||
"sdns://",
|
|
||||||
"tcp://",
|
|
||||||
"tls://",
|
|
||||||
"udp://",
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateUpstream returns an error if u alongside with domains is not a valid
|
// validateUpstream returns an error if u alongside with domains is not a valid
|
||||||
// upstream configuration. useDefault is true if the upstream is
|
// upstream configuration. useDefault is true if the upstream is
|
||||||
@@ -664,7 +659,24 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
|
|||||||
result[host] = "OK"
|
result[host] = "OK"
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, result)
|
jsonVal, err := json.Marshal(result)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(
|
||||||
|
r,
|
||||||
|
w,
|
||||||
|
http.StatusInternalServerError,
|
||||||
|
"Unable to marshal status json: %s",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, err = w.Write(jsonVal)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusInternalServerError, "Couldn't write body: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleDoH is the DNS-over-HTTPs handler.
|
// handleDoH is the DNS-over-HTTPs handler.
|
||||||
@@ -680,13 +692,11 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (s *Server) handleDoH(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleDoH(w http.ResponseWriter, r *http.Request) {
|
||||||
if !s.conf.TLSAllowUnencryptedDoH && r.TLS == nil {
|
if !s.conf.TLSAllowUnencryptedDoH && r.TLS == nil {
|
||||||
aghhttp.Error(r, w, http.StatusNotFound, "Not Found")
|
aghhttp.Error(r, w, http.StatusNotFound, "Not Found")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.IsRunning() {
|
if !s.IsRunning() {
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "dns server is not running")
|
aghhttp.Error(r, w, http.StatusInternalServerError, "dns server is not running")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
@@ -117,8 +116,7 @@ func TestDNSForwardHTTP_handleGetConfig(t *testing.T) {
|
|||||||
s.conf = tc.conf()
|
s.conf = tc.conf()
|
||||||
s.handleGetConfig(w, nil)
|
s.handleGetConfig(w, nil)
|
||||||
|
|
||||||
cType := w.Header().Get(aghhttp.HdrNameContentType)
|
assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
|
||||||
assert.Equal(t, aghhttp.HdrValApplicationJSON, cType)
|
|
||||||
assert.JSONEq(t, string(caseWant), w.Body.String())
|
assert.JSONEq(t, string(caseWant), w.Body.String())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -456,9 +456,8 @@ func (clients *clientsContainer) findUpstreams(
|
|||||||
conf, err = proxy.ParseUpstreamsConfig(
|
conf, err = proxy.ParseUpstreamsConfig(
|
||||||
upstreams,
|
upstreams,
|
||||||
&upstream.Options{
|
&upstream.Options{
|
||||||
Bootstrap: config.DNS.BootstrapDNS,
|
Bootstrap: config.DNS.BootstrapDNS,
|
||||||
Timeout: config.DNS.UpstreamTimeout.Duration,
|
Timeout: config.DNS.UpstreamTimeout.Duration,
|
||||||
HTTPVersions: dnsforward.UpstreamHTTPVersions(config.DNS.UseHTTP3Upstreams),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -166,19 +166,6 @@ type dnsConfig struct {
|
|||||||
// LocalPTRResolvers is the slice of addresses to be used as upstreams
|
// LocalPTRResolvers is the slice of addresses to be used as upstreams
|
||||||
// for PTR queries for locally-served networks.
|
// for PTR queries for locally-served networks.
|
||||||
LocalPTRResolvers []string `yaml:"local_ptr_upstreams"`
|
LocalPTRResolvers []string `yaml:"local_ptr_upstreams"`
|
||||||
|
|
||||||
// ServeHTTP3 defines if HTTP/3 is be allowed for incoming requests.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Add to the UI when HTTP/3 support is no longer
|
|
||||||
// experimental.
|
|
||||||
ServeHTTP3 bool `yaml:"serve_http3"`
|
|
||||||
|
|
||||||
// UseHTTP3Upstreams defines if HTTP/3 is be allowed for DNS-over-HTTPS
|
|
||||||
// upstreams.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Add to the UI when HTTP/3 support is no longer
|
|
||||||
// experimental.
|
|
||||||
UseHTTP3Upstreams bool `yaml:"use_http3_upstreams"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type tlsConfigSettings struct {
|
type tlsConfigSettings struct {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package home
|
package home
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -154,12 +155,19 @@ type profileJSON struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleGetProfile(w http.ResponseWriter, r *http.Request) {
|
func handleGetProfile(w http.ResponseWriter, r *http.Request) {
|
||||||
|
pj := profileJSON{}
|
||||||
u := Context.auth.getCurrentUser(r)
|
u := Context.auth.getCurrentUser(r)
|
||||||
resp := &profileJSON{
|
|
||||||
Name: u.Name,
|
pj.Name = u.Name
|
||||||
|
|
||||||
|
data, err := json.Marshal(pj)
|
||||||
|
if err != nil {
|
||||||
|
aghhttp.Error(r, w, http.StatusInternalServerError, "json.Marshal: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
_, _ = w.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------
|
// ------------------------
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/lucas-clemente/quic-go/http3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// getAddrsResponse is the response for /install/get_addresses endpoint.
|
// getAddrsResponse is the response for /install/get_addresses endpoint.
|
||||||
@@ -329,7 +328,6 @@ func copyInstallSettings(dst, src *configuration) {
|
|||||||
// shutdownTimeout is the timeout for shutting HTTP server down operation.
|
// shutdownTimeout is the timeout for shutting HTTP server down operation.
|
||||||
const shutdownTimeout = 5 * time.Second
|
const shutdownTimeout = 5 * time.Second
|
||||||
|
|
||||||
// shutdownSrv shuts srv down and prints error messages to the log.
|
|
||||||
func shutdownSrv(ctx context.Context, srv *http.Server) {
|
func shutdownSrv(ctx context.Context, srv *http.Server) {
|
||||||
defer log.OnPanic("")
|
defer log.OnPanic("")
|
||||||
|
|
||||||
@@ -338,38 +336,13 @@ func shutdownSrv(ctx context.Context, srv *http.Server) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err := srv.Shutdown(ctx)
|
err := srv.Shutdown(ctx)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
return
|
const msgFmt = "shutting down http server %q: %s"
|
||||||
}
|
if errors.Is(err, context.Canceled) {
|
||||||
|
log.Debug(msgFmt, srv.Addr, err)
|
||||||
const msgFmt = "shutting down http server %q: %s"
|
} else {
|
||||||
if errors.Is(err, context.Canceled) {
|
log.Error(msgFmt, srv.Addr, err)
|
||||||
log.Debug(msgFmt, srv.Addr, err)
|
}
|
||||||
} else {
|
|
||||||
log.Error(msgFmt, srv.Addr, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// shutdownSrv3 shuts srv down and prints error messages to the log.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Think of a good way to merge with [shutdownSrv].
|
|
||||||
func shutdownSrv3(srv *http3.Server) {
|
|
||||||
defer log.OnPanic("")
|
|
||||||
|
|
||||||
if srv == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := srv.Close()
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const msgFmt = "shutting down http/3 server %q: %s"
|
|
||||||
if errors.Is(err, context.Canceled) {
|
|
||||||
log.Debug(msgFmt, srv.Addr, err)
|
|
||||||
} else {
|
|
||||||
log.Error(msgFmt, srv.Addr, err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,11 +545,16 @@ func (web *Web) handleInstallCheckConfigBeta(w http.ResponseWriter, r *http.Requ
|
|||||||
|
|
||||||
err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData)
|
err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "encoding check_config: %s", err)
|
aghhttp.Error(
|
||||||
|
r,
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"Failed to encode 'check_config' JSON data: %s",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
body := nonBetaReqBody.String()
|
body := nonBetaReqBody.String()
|
||||||
r.Body = io.NopCloser(strings.NewReader(body))
|
r.Body = io.NopCloser(strings.NewReader(body))
|
||||||
r.ContentLength = int64(len(body))
|
r.ContentLength = int64(len(body))
|
||||||
@@ -644,7 +622,13 @@ func (web *Web) handleInstallConfigureBeta(w http.ResponseWriter, r *http.Reques
|
|||||||
|
|
||||||
err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData)
|
err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "encoding configure: %s", err)
|
aghhttp.Error(
|
||||||
|
r,
|
||||||
|
w,
|
||||||
|
http.StatusBadRequest,
|
||||||
|
"Failed to encode 'check_config' JSON data: %s",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,13 +246,10 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
|
|||||||
newConf.FilterHandler = applyAdditionalFiltering
|
newConf.FilterHandler = applyAdditionalFiltering
|
||||||
newConf.GetCustomUpstreamByClient = Context.clients.findUpstreams
|
newConf.GetCustomUpstreamByClient = Context.clients.findUpstreams
|
||||||
|
|
||||||
newConf.LocalPTRResolvers = dnsConf.LocalPTRResolvers
|
|
||||||
newConf.UpstreamTimeout = dnsConf.UpstreamTimeout.Duration
|
|
||||||
|
|
||||||
newConf.ResolveClients = config.Clients.Sources.RDNS
|
newConf.ResolveClients = config.Clients.Sources.RDNS
|
||||||
newConf.UsePrivateRDNS = dnsConf.UsePrivateRDNS
|
newConf.UsePrivateRDNS = dnsConf.UsePrivateRDNS
|
||||||
newConf.ServeHTTP3 = dnsConf.ServeHTTP3
|
newConf.LocalPTRResolvers = dnsConf.LocalPTRResolvers
|
||||||
newConf.UseHTTP3Upstreams = dnsConf.UseHTTP3Upstreams
|
newConf.UpstreamTimeout = dnsConf.UpstreamTimeout.Duration
|
||||||
|
|
||||||
return newConf, nil
|
return newConf, nil
|
||||||
}
|
}
|
||||||
@@ -361,13 +358,7 @@ func applyAdditionalFiltering(clientIP net.IP, clientID string, setts *filtering
|
|||||||
log.Debug("%s: using settings for client %q (%s; %q)", pref, c.Name, clientIP, clientID)
|
log.Debug("%s: using settings for client %q (%s; %q)", pref, c.Name, clientIP, clientID)
|
||||||
|
|
||||||
if c.UseOwnBlockedServices {
|
if c.UseOwnBlockedServices {
|
||||||
// TODO(e.burkov): Get rid of this crutch.
|
Context.filters.ApplyBlockedServices(setts, c.BlockedServices)
|
||||||
svcs := c.BlockedServices
|
|
||||||
if svcs == nil {
|
|
||||||
svcs = []string{}
|
|
||||||
}
|
|
||||||
Context.filters.ApplyBlockedServices(setts, svcs)
|
|
||||||
log.Debug("%s: services for client %q set: %s", pref, c.Name, svcs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setts.ClientName = c.Name
|
setts.ClientName = c.Name
|
||||||
|
|||||||
@@ -381,11 +381,9 @@ func initWeb(args options, clientBuildFS fs.FS) (web *Web, err error) {
|
|||||||
|
|
||||||
clientFS: clientFS,
|
clientFS: clientFS,
|
||||||
clientBetaFS: clientBetaFS,
|
clientBetaFS: clientBetaFS,
|
||||||
|
|
||||||
serveHTTP3: config.DNS.ServeHTTP3,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
web = newWeb(&webConf)
|
web = CreateWeb(&webConf)
|
||||||
if web == nil {
|
if web == nil {
|
||||||
return nil, fmt.Errorf("initializing web: %w", err)
|
return nil, fmt.Errorf("initializing web: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,7 +266,7 @@ func (t *TLSMod) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !webCheckPortAvailable(setts.PortHTTPS) {
|
if !WebCheckPortAvailable(setts.PortHTTPS) {
|
||||||
aghhttp.Error(
|
aghhttp.Error(
|
||||||
r,
|
r,
|
||||||
w,
|
w,
|
||||||
@@ -356,7 +356,7 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO(e.burkov): Investigate and perhaps check other ports.
|
// TODO(e.burkov): Investigate and perhaps check other ports.
|
||||||
if !webCheckPortAvailable(data.PortHTTPS) {
|
if !WebCheckPortAvailable(data.PortHTTPS) {
|
||||||
aghhttp.Error(
|
aghhttp.Error(
|
||||||
r,
|
r,
|
||||||
w,
|
w,
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ import (
|
|||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
"github.com/NYTimes/gziphandler"
|
"github.com/NYTimes/gziphandler"
|
||||||
"github.com/lucas-clemente/quic-go"
|
|
||||||
"github.com/lucas-clemente/quic-go/http3"
|
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
"golang.org/x/net/http2/h2c"
|
"golang.org/x/net/http2/h2c"
|
||||||
)
|
)
|
||||||
@@ -55,56 +53,40 @@ type webConfig struct {
|
|||||||
WriteTimeout time.Duration
|
WriteTimeout time.Duration
|
||||||
|
|
||||||
firstRun bool
|
firstRun bool
|
||||||
|
|
||||||
serveHTTP3 bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// httpsServer contains the data for the HTTPS server.
|
// HTTPSServer - HTTPS Server
|
||||||
type httpsServer struct {
|
type HTTPSServer struct {
|
||||||
// server is the pre-HTTP/3 HTTPS server.
|
server *http.Server
|
||||||
server *http.Server
|
cond *sync.Cond
|
||||||
// server3 is the HTTP/3 HTTPS server. If it is not nil,
|
condLock sync.Mutex
|
||||||
// [httpsServer.server] must also be non-nil.
|
shutdown bool // if TRUE, don't restart the server
|
||||||
server3 *http3.Server
|
enabled bool
|
||||||
|
cert tls.Certificate
|
||||||
// TODO(a.garipov): Why is there a *sync.Cond here? Remove.
|
|
||||||
cond *sync.Cond
|
|
||||||
condLock sync.Mutex
|
|
||||||
cert tls.Certificate
|
|
||||||
inShutdown bool
|
|
||||||
enabled bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Web is the web UI and API server.
|
// Web - module object
|
||||||
type Web struct {
|
type Web struct {
|
||||||
conf *webConfig
|
conf *webConfig
|
||||||
|
forceHTTPS bool
|
||||||
// TODO(a.garipov): Refactor all these servers.
|
httpServer *http.Server // HTTP module
|
||||||
httpServer *http.Server
|
httpsServer HTTPSServer // HTTPS module
|
||||||
|
|
||||||
// httpServerBeta is a server for new client.
|
|
||||||
httpServerBeta *http.Server
|
|
||||||
|
|
||||||
// handlerBeta is the handler for new client.
|
// handlerBeta is the handler for new client.
|
||||||
handlerBeta http.Handler
|
handlerBeta http.Handler
|
||||||
|
|
||||||
// installerBeta is the pre-install handler for new client.
|
// installerBeta is the pre-install handler for new client.
|
||||||
installerBeta http.Handler
|
installerBeta http.Handler
|
||||||
|
|
||||||
// httpsServer is the server that handles HTTPS traffic. If it is not nil,
|
// httpServerBeta is a server for new client.
|
||||||
// [Web.http3Server] must also not be nil.
|
httpServerBeta *http.Server
|
||||||
httpsServer httpsServer
|
|
||||||
|
|
||||||
forceHTTPS bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// newWeb creates a new instance of the web UI and API server.
|
// CreateWeb - create module
|
||||||
func newWeb(conf *webConfig) (w *Web) {
|
func CreateWeb(conf *webConfig) *Web {
|
||||||
log.Info("web: initializing")
|
log.Info("Initialize web module")
|
||||||
|
|
||||||
w = &Web{
|
w := Web{}
|
||||||
conf: conf,
|
w.conf = conf
|
||||||
}
|
|
||||||
|
|
||||||
clientFS := http.FileServer(http.FS(conf.clientFS))
|
clientFS := http.FileServer(http.FS(conf.clientFS))
|
||||||
betaClientFS := http.FileServer(http.FS(conf.clientBetaFS))
|
betaClientFS := http.FileServer(http.FS(conf.clientBetaFS))
|
||||||
@@ -126,15 +108,12 @@ func newWeb(conf *webConfig) (w *Web) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.httpsServer.cond = sync.NewCond(&w.httpsServer.condLock)
|
w.httpsServer.cond = sync.NewCond(&w.httpsServer.condLock)
|
||||||
|
return &w
|
||||||
return w
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// webCheckPortAvailable checks if port, which is considered an HTTPS port, is
|
// WebCheckPortAvailable - check if port is available
|
||||||
// available, unless the HTTPS server isn't active.
|
// BUT: if we are already using this port, no need
|
||||||
//
|
func WebCheckPortAvailable(port int) bool {
|
||||||
// TODO(a.garipov): Adapt for HTTP/3.
|
|
||||||
func webCheckPortAvailable(port int) (ok bool) {
|
|
||||||
return Context.web.httpsServer.server != nil ||
|
return Context.web.httpsServer.server != nil ||
|
||||||
aghnet.CheckPort("tcp", config.BindHost, port) == nil
|
aghnet.CheckPort("tcp", config.BindHost, port) == nil
|
||||||
}
|
}
|
||||||
@@ -142,7 +121,7 @@ func webCheckPortAvailable(port int) (ok bool) {
|
|||||||
// TLSConfigChanged updates the TLS configuration and restarts the HTTPS server
|
// TLSConfigChanged updates the TLS configuration and restarts the HTTPS server
|
||||||
// if necessary.
|
// if necessary.
|
||||||
func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) {
|
func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) {
|
||||||
log.Debug("web: applying new tls configuration")
|
log.Debug("Web: applying new TLS configuration")
|
||||||
web.conf.PortHTTPS = tlsConf.PortHTTPS
|
web.conf.PortHTTPS = tlsConf.PortHTTPS
|
||||||
web.forceHTTPS = (tlsConf.ForceHTTPS && tlsConf.Enabled && tlsConf.PortHTTPS != 0)
|
web.forceHTTPS = (tlsConf.ForceHTTPS && tlsConf.Enabled && tlsConf.PortHTTPS != 0)
|
||||||
|
|
||||||
@@ -164,8 +143,6 @@ func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings)
|
|||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
ctx, cancel = context.WithTimeout(ctx, shutdownTimeout)
|
ctx, cancel = context.WithTimeout(ctx, shutdownTimeout)
|
||||||
shutdownSrv(ctx, web.httpsServer.server)
|
shutdownSrv(ctx, web.httpsServer.server)
|
||||||
shutdownSrv3(web.httpsServer.server3)
|
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +160,7 @@ func (web *Web) Start() {
|
|||||||
go web.tlsServerLoop()
|
go web.tlsServerLoop()
|
||||||
|
|
||||||
// this loop is used as an ability to change listening host and/or port
|
// this loop is used as an ability to change listening host and/or port
|
||||||
for !web.httpsServer.inShutdown {
|
for !web.httpsServer.shutdown {
|
||||||
printHTTPAddresses(aghhttp.SchemeHTTP)
|
printHTTPAddresses(aghhttp.SchemeHTTP)
|
||||||
errs := make(chan error, 2)
|
errs := make(chan error, 2)
|
||||||
|
|
||||||
@@ -254,7 +231,7 @@ func (web *Web) Close(ctx context.Context) {
|
|||||||
log.Info("stopping http server...")
|
log.Info("stopping http server...")
|
||||||
|
|
||||||
web.httpsServer.cond.L.Lock()
|
web.httpsServer.cond.L.Lock()
|
||||||
web.httpsServer.inShutdown = true
|
web.httpsServer.shutdown = true
|
||||||
web.httpsServer.cond.L.Unlock()
|
web.httpsServer.cond.L.Unlock()
|
||||||
|
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
@@ -262,7 +239,6 @@ func (web *Web) Close(ctx context.Context) {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
shutdownSrv(ctx, web.httpsServer.server)
|
shutdownSrv(ctx, web.httpsServer.server)
|
||||||
shutdownSrv3(web.httpsServer.server3)
|
|
||||||
shutdownSrv(ctx, web.httpServer)
|
shutdownSrv(ctx, web.httpServer)
|
||||||
shutdownSrv(ctx, web.httpServerBeta)
|
shutdownSrv(ctx, web.httpServerBeta)
|
||||||
|
|
||||||
@@ -272,7 +248,7 @@ func (web *Web) Close(ctx context.Context) {
|
|||||||
func (web *Web) tlsServerLoop() {
|
func (web *Web) tlsServerLoop() {
|
||||||
for {
|
for {
|
||||||
web.httpsServer.cond.L.Lock()
|
web.httpsServer.cond.L.Lock()
|
||||||
if web.httpsServer.inShutdown {
|
if web.httpsServer.shutdown {
|
||||||
web.httpsServer.cond.L.Unlock()
|
web.httpsServer.cond.L.Unlock()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -280,7 +256,7 @@ func (web *Web) tlsServerLoop() {
|
|||||||
// this mechanism doesn't let us through until all conditions are met
|
// this mechanism doesn't let us through until all conditions are met
|
||||||
for !web.httpsServer.enabled { // sleep until necessary data is supplied
|
for !web.httpsServer.enabled { // sleep until necessary data is supplied
|
||||||
web.httpsServer.cond.Wait()
|
web.httpsServer.cond.Wait()
|
||||||
if web.httpsServer.inShutdown {
|
if web.httpsServer.shutdown {
|
||||||
web.httpsServer.cond.L.Unlock()
|
web.httpsServer.cond.L.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -288,10 +264,11 @@ func (web *Web) tlsServerLoop() {
|
|||||||
|
|
||||||
web.httpsServer.cond.L.Unlock()
|
web.httpsServer.cond.L.Unlock()
|
||||||
|
|
||||||
addr := netutil.JoinHostPort(web.conf.BindHost.String(), web.conf.PortHTTPS)
|
// prepare HTTPS server
|
||||||
|
address := netutil.JoinHostPort(web.conf.BindHost.String(), web.conf.PortHTTPS)
|
||||||
web.httpsServer.server = &http.Server{
|
web.httpsServer.server = &http.Server{
|
||||||
ErrorLog: log.StdLog("web: https", log.DEBUG),
|
ErrorLog: log.StdLog("web: https", log.DEBUG),
|
||||||
Addr: addr,
|
Addr: address,
|
||||||
TLSConfig: &tls.Config{
|
TLSConfig: &tls.Config{
|
||||||
Certificates: []tls.Certificate{web.httpsServer.cert},
|
Certificates: []tls.Certificate{web.httpsServer.cert},
|
||||||
RootCAs: Context.tlsRoots,
|
RootCAs: Context.tlsRoots,
|
||||||
@@ -305,40 +282,10 @@ func (web *Web) tlsServerLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
printHTTPAddresses(aghhttp.SchemeHTTPS)
|
printHTTPAddresses(aghhttp.SchemeHTTPS)
|
||||||
|
|
||||||
if web.conf.serveHTTP3 {
|
|
||||||
go web.mustStartHTTP3(addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("web: starting https server")
|
|
||||||
err := web.httpsServer.server.ListenAndServeTLS("", "")
|
err := web.httpsServer.server.ListenAndServeTLS("", "")
|
||||||
if !errors.Is(err, http.ErrServerClosed) {
|
if err != http.ErrServerClosed {
|
||||||
cleanupAlways()
|
cleanupAlways()
|
||||||
log.Fatalf("web: https: %s", err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (web *Web) mustStartHTTP3(address string) {
|
|
||||||
defer log.OnPanic("web: http3")
|
|
||||||
|
|
||||||
web.httpsServer.server3 = &http3.Server{
|
|
||||||
// TODO(a.garipov): See if there is a way to use the error log as
|
|
||||||
// well as timeouts here.
|
|
||||||
Addr: address,
|
|
||||||
TLSConfig: &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{web.httpsServer.cert},
|
|
||||||
RootCAs: Context.tlsRoots,
|
|
||||||
CipherSuites: aghtls.SaferCipherSuites(),
|
|
||||||
MinVersion: tls.VersionTLS12,
|
|
||||||
},
|
|
||||||
Handler: withMiddlewares(Context.mux, limitRequestBody),
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("web: starting http/3 server")
|
|
||||||
err := web.httpsServer.server3.ListenAndServe()
|
|
||||||
if !errors.Is(err, quic.ErrServerClosed) {
|
|
||||||
cleanupAlways()
|
|
||||||
log.Fatalf("web: http3: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,29 +11,32 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/websvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main is the entry point of application.
|
// Main is the entry point of application.
|
||||||
func Main(clientBuildFS fs.FS) {
|
func Main(clientBuildFS fs.FS) {
|
||||||
// # Initial Configuration
|
// Initial Configuration
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
rand.Seed(start.UnixNano())
|
rand.Seed(start.UnixNano())
|
||||||
|
|
||||||
// TODO(a.garipov): Set up logging.
|
// TODO(a.garipov): Set up logging.
|
||||||
|
|
||||||
// # Web Service
|
// Web Service
|
||||||
|
|
||||||
// TODO(a.garipov): Use in the Web service.
|
// TODO(a.garipov): Use in the Web service.
|
||||||
_ = clientBuildFS
|
_ = clientBuildFS
|
||||||
|
|
||||||
// TODO(a.garipov): Make configurable.
|
// TODO(a.garipov): Make configurable.
|
||||||
web := websvc.New(&websvc.Config{
|
web := websvc.New(&websvc.Config{
|
||||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:3001")},
|
// TODO(a.garipov): Use an actual implementation.
|
||||||
Start: start,
|
ConfigManager: nil,
|
||||||
Timeout: 60 * time.Second,
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:3001")},
|
||||||
|
Start: start,
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
ForceHTTPS: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
err := web.Start()
|
err := web.Start()
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/agh"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -9,9 +9,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/agh"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
// TODO(a.garipov): Add a “dnsproxy proxy” package to shield us from changes
|
// TODO(a.garipov): Add a “dnsproxy proxy” package to shield us from changes
|
||||||
// and replacement of module dnsproxy.
|
// and replacement of module dnsproxy.
|
||||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||||
@@ -47,6 +48,14 @@ type Config struct {
|
|||||||
// Service is the AdGuard Home DNS service. A nil *Service is a valid
|
// Service is the AdGuard Home DNS service. A nil *Service is a valid
|
||||||
// [agh.Service] that does nothing.
|
// [agh.Service] that does nothing.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
// running is an atomic boolean value. Keep it the first value in the
|
||||||
|
// struct to ensure atomic alignment. 0 means that the service is not
|
||||||
|
// running, 1 means that it is running.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Use [atomic.Bool] in Go 1.19 or get rid of it
|
||||||
|
// completely.
|
||||||
|
running uint64
|
||||||
|
|
||||||
proxy *proxy.Proxy
|
proxy *proxy.Proxy
|
||||||
bootstraps []string
|
bootstraps []string
|
||||||
upstreams []string
|
upstreams []string
|
||||||
@@ -160,6 +169,17 @@ func (svc *Service) Start() (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// TODO(a.garipov): [proxy.Proxy.Start] doesn't actually have any way to
|
||||||
|
// tell when all servers are actually up, so at best this is merely an
|
||||||
|
// assumption.
|
||||||
|
if err != nil {
|
||||||
|
atomic.StoreUint64(&svc.running, 0)
|
||||||
|
} else {
|
||||||
|
atomic.StoreUint64(&svc.running, 1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return svc.proxy.Start()
|
return svc.proxy.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,13 +193,27 @@ func (svc *Service) Shutdown(ctx context.Context) (err error) {
|
|||||||
return svc.proxy.Stop()
|
return svc.proxy.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config returns the current configuration of the web service.
|
// Config returns the current configuration of the web service. Config must not
|
||||||
|
// be called simultaneously with Start. If svc was initialized with ":0"
|
||||||
|
// addresses, addrs will not return the actual bound ports until Start is
|
||||||
|
// finished.
|
||||||
func (svc *Service) Config() (c *Config) {
|
func (svc *Service) Config() (c *Config) {
|
||||||
// TODO(a.garipov): Do we need to get the TCP addresses separately?
|
// TODO(a.garipov): Do we need to get the TCP addresses separately?
|
||||||
udpAddrs := svc.proxy.Addrs(proxy.ProtoUDP)
|
|
||||||
addrs := make([]netip.AddrPort, len(udpAddrs))
|
var addrs []netip.AddrPort
|
||||||
for i, a := range udpAddrs {
|
if atomic.LoadUint64(&svc.running) == 1 {
|
||||||
addrs[i] = a.(*net.UDPAddr).AddrPort()
|
udpAddrs := svc.proxy.Addrs(proxy.ProtoUDP)
|
||||||
|
addrs = make([]netip.AddrPort, len(udpAddrs))
|
||||||
|
for i, a := range udpAddrs {
|
||||||
|
addrs[i] = a.(*net.UDPAddr).AddrPort()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
conf := svc.proxy.Config
|
||||||
|
udpAddrs := conf.UDPListenAddr
|
||||||
|
addrs = make([]netip.AddrPort, len(udpAddrs))
|
||||||
|
for i, a := range udpAddrs {
|
||||||
|
addrs[i] = a.AddrPort()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c = &Config{
|
c = &Config{
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/dnssvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
84
internal/next/websvc/dns.go
Normal file
84
internal/next/websvc/dns.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNS Settings Handlers
|
||||||
|
|
||||||
|
// ReqPatchSettingsDNS describes the request to the PATCH /api/v1/settings/dns
|
||||||
|
// HTTP API.
|
||||||
|
type ReqPatchSettingsDNS struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
|
||||||
|
Addresses []netip.AddrPort `json:"addresses"`
|
||||||
|
BootstrapServers []string `json:"bootstrap_servers"`
|
||||||
|
UpstreamServers []string `json:"upstream_servers"`
|
||||||
|
UpstreamTimeout JSONDuration `json:"upstream_timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPAPIDNSSettings are the DNS settings as used by the HTTP API. See the
|
||||||
|
// DnsSettings object in the OpenAPI specification.
|
||||||
|
type HTTPAPIDNSSettings struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
|
||||||
|
Addresses []netip.AddrPort `json:"addresses"`
|
||||||
|
BootstrapServers []string `json:"bootstrap_servers"`
|
||||||
|
UpstreamServers []string `json:"upstream_servers"`
|
||||||
|
UpstreamTimeout JSONDuration `json:"upstream_timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePatchSettingsDNS is the handler for the PATCH /api/v1/settings/dns HTTP
|
||||||
|
// API.
|
||||||
|
func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
req := &ReqPatchSettingsDNS{
|
||||||
|
Addresses: []netip.AddrPort{},
|
||||||
|
BootstrapServers: []string{},
|
||||||
|
UpstreamServers: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Validate nulls and proper JSON patch.
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newConf := &dnssvc.Config{
|
||||||
|
Addresses: req.Addresses,
|
||||||
|
BootstrapServers: req.BootstrapServers,
|
||||||
|
UpstreamServers: req.UpstreamServers,
|
||||||
|
UpstreamTimeout: time.Duration(req.UpstreamTimeout),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
err = svc.confMgr.UpdateDNS(ctx, newConf)
|
||||||
|
if err != nil {
|
||||||
|
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newSvc := svc.confMgr.DNS()
|
||||||
|
err = newSvc.Start()
|
||||||
|
if err != nil {
|
||||||
|
writeJSONErrorResponse(w, r, fmt.Errorf("starting new service: %w", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSONOKResponse(w, r, &HTTPAPIDNSSettings{
|
||||||
|
Addresses: newConf.Addresses,
|
||||||
|
BootstrapServers: newConf.BootstrapServers,
|
||||||
|
UpstreamServers: newConf.UpstreamServers,
|
||||||
|
UpstreamTimeout: JSONDuration(newConf.UpstreamTimeout),
|
||||||
|
})
|
||||||
|
}
|
||||||
68
internal/next/websvc/dns_test.go
Normal file
68
internal/next/websvc/dns_test.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package websvc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_HandlePatchSettingsDNS(t *testing.T) {
|
||||||
|
wantDNS := &websvc.HTTPAPIDNSSettings{
|
||||||
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:53")},
|
||||||
|
BootstrapServers: []string{"1.0.0.1"},
|
||||||
|
UpstreamServers: []string{"1.1.1.1"},
|
||||||
|
UpstreamTimeout: websvc.JSONDuration(2 * time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Use [atomic.Bool] in Go 1.19.
|
||||||
|
var numStarted uint64
|
||||||
|
confMgr := newConfigManager()
|
||||||
|
confMgr.onDNS = func() (s websvc.ServiceWithConfig[*dnssvc.Config]) {
|
||||||
|
return &aghtest.ServiceWithConfig[*dnssvc.Config]{
|
||||||
|
OnStart: func() (err error) {
|
||||||
|
atomic.AddUint64(&numStarted, 1)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
OnShutdown: func(_ context.Context) (err error) { panic("not implemented") },
|
||||||
|
OnConfig: func() (c *dnssvc.Config) { panic("not implemented") },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
confMgr.onUpdateDNS = func(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, addr := newTestServer(t, confMgr)
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: addr.String(),
|
||||||
|
Path: websvc.PathV1SettingsDNS,
|
||||||
|
}
|
||||||
|
|
||||||
|
req := jobj{
|
||||||
|
"addresses": wantDNS.Addresses,
|
||||||
|
"bootstrap_servers": wantDNS.BootstrapServers,
|
||||||
|
"upstream_servers": wantDNS.UpstreamServers,
|
||||||
|
"upstream_timeout": wantDNS.UpstreamTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody := httpPatch(t, u, req, http.StatusOK)
|
||||||
|
resp := &websvc.HTTPAPIDNSSettings{}
|
||||||
|
err := json.Unmarshal(respBody, resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, uint64(1), numStarted)
|
||||||
|
assert.Equal(t, wantDNS, resp)
|
||||||
|
assert.Equal(t, wantDNS, resp)
|
||||||
|
}
|
||||||
109
internal/next/websvc/http.go
Normal file
109
internal/next/websvc/http.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTP Settings Handlers
|
||||||
|
|
||||||
|
// ReqPatchSettingsHTTP describes the request to the PATCH /api/v1/settings/http
|
||||||
|
// HTTP API.
|
||||||
|
type ReqPatchSettingsHTTP struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Add wait time.
|
||||||
|
|
||||||
|
Addresses []netip.AddrPort `json:"addresses"`
|
||||||
|
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
||||||
|
Timeout JSONDuration `json:"timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPAPIHTTPSettings are the HTTP settings as used by the HTTP API. See the
|
||||||
|
// HttpSettings object in the OpenAPI specification.
|
||||||
|
type HTTPAPIHTTPSettings struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
|
||||||
|
Addresses []netip.AddrPort `json:"addresses"`
|
||||||
|
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
||||||
|
Timeout JSONDuration `json:"timeout"`
|
||||||
|
ForceHTTPS bool `json:"force_https"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePatchSettingsHTTP is the handler for the PATCH /api/v1/settings/http
|
||||||
|
// HTTP API.
|
||||||
|
func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
req := &ReqPatchSettingsHTTP{}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Validate nulls and proper JSON patch.
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newConf := &Config{
|
||||||
|
ConfigManager: svc.confMgr,
|
||||||
|
TLS: svc.tls,
|
||||||
|
Addresses: req.Addresses,
|
||||||
|
SecureAddresses: req.SecureAddresses,
|
||||||
|
Timeout: time.Duration(req.Timeout),
|
||||||
|
ForceHTTPS: svc.forceHTTPS,
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSONOKResponse(w, r, &HTTPAPIHTTPSettings{
|
||||||
|
Addresses: newConf.Addresses,
|
||||||
|
SecureAddresses: newConf.SecureAddresses,
|
||||||
|
Timeout: JSONDuration(newConf.Timeout),
|
||||||
|
ForceHTTPS: newConf.ForceHTTPS,
|
||||||
|
})
|
||||||
|
|
||||||
|
cancelUpd := func() {}
|
||||||
|
updCtx := context.Background()
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
|
updCtx, cancelUpd = context.WithDeadline(updCtx, deadline)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch the new HTTP service in a separate goroutine to let this handler
|
||||||
|
// finish and thus, this server to shutdown.
|
||||||
|
go func() {
|
||||||
|
defer cancelUpd()
|
||||||
|
|
||||||
|
updErr := svc.confMgr.UpdateWeb(updCtx, newConf)
|
||||||
|
if updErr != nil {
|
||||||
|
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", updErr))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Consider better ways to do this.
|
||||||
|
const maxUpdDur = 10 * time.Second
|
||||||
|
updStart := time.Now()
|
||||||
|
var newSvc ServiceWithConfig[*Config]
|
||||||
|
for newSvc = svc.confMgr.Web(); newSvc == svc; {
|
||||||
|
if time.Since(updStart) >= maxUpdDur {
|
||||||
|
log.Error("websvc: failed to update svc after %s", maxUpdDur)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("websvc: waiting for new websvc to be configured")
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
updErr = newSvc.Start()
|
||||||
|
if updErr != nil {
|
||||||
|
log.Error("websvc: new svc failed to start with error: %s", updErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
62
internal/next/websvc/http_test.go
Normal file
62
internal/next/websvc/http_test.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package websvc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_HandlePatchSettingsHTTP(t *testing.T) {
|
||||||
|
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
||||||
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:80")},
|
||||||
|
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:443")},
|
||||||
|
Timeout: websvc.JSONDuration(10 * time.Second),
|
||||||
|
ForceHTTPS: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
confMgr := newConfigManager()
|
||||||
|
confMgr.onWeb = func() (s websvc.ServiceWithConfig[*websvc.Config]) {
|
||||||
|
return websvc.New(&websvc.Config{
|
||||||
|
TLS: &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{{}},
|
||||||
|
},
|
||||||
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
|
||||||
|
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
ForceHTTPS: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
confMgr.onUpdateWeb = func(ctx context.Context, c *websvc.Config) (err error) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, addr := newTestServer(t, confMgr)
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: addr.String(),
|
||||||
|
Path: websvc.PathV1SettingsHTTP,
|
||||||
|
}
|
||||||
|
|
||||||
|
req := jobj{
|
||||||
|
"addresses": wantWeb.Addresses,
|
||||||
|
"secure_addresses": wantWeb.SecureAddresses,
|
||||||
|
"timeout": wantWeb.Timeout,
|
||||||
|
"force_https": wantWeb.ForceHTTPS,
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody := httpPatch(t, u, req, http.StatusOK)
|
||||||
|
resp := &websvc.HTTPAPIHTTPSettings{}
|
||||||
|
err := json.Unmarshal(respBody, resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, wantWeb, resp)
|
||||||
|
}
|
||||||
143
internal/next/websvc/json.go
Normal file
143
internal/next/websvc/json.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSON Utilities
|
||||||
|
|
||||||
|
// nsecPerMsec is the number of nanoseconds in a millisecond.
|
||||||
|
const nsecPerMsec = float64(time.Millisecond / time.Nanosecond)
|
||||||
|
|
||||||
|
// JSONDuration is a time.Duration that can be decoded from JSON and encoded
|
||||||
|
// into JSON according to our API conventions.
|
||||||
|
type JSONDuration time.Duration
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ json.Marshaler = JSONDuration(0)
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface for JSONDuration. err is
|
||||||
|
// always nil.
|
||||||
|
func (d JSONDuration) MarshalJSON() (b []byte, err error) {
|
||||||
|
msec := float64(time.Duration(d)) / nsecPerMsec
|
||||||
|
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ json.Unmarshaler = (*JSONDuration)(nil)
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Marshaler interface for *JSONDuration.
|
||||||
|
func (d *JSONDuration) UnmarshalJSON(b []byte) (err error) {
|
||||||
|
if d == nil {
|
||||||
|
return fmt.Errorf("json duration is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
msec, err := strconv.ParseFloat(string(b), 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing json time: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*d = JSONDuration(int64(msec * nsecPerMsec))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONTime is a time.Time that can be decoded from JSON and encoded into JSON
|
||||||
|
// according to our API conventions.
|
||||||
|
type JSONTime time.Time
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ json.Marshaler = JSONTime{}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface for JSONTime. err is
|
||||||
|
// always nil.
|
||||||
|
func (t JSONTime) MarshalJSON() (b []byte, err error) {
|
||||||
|
msec := float64(time.Time(t).UnixNano()) / nsecPerMsec
|
||||||
|
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ json.Unmarshaler = (*JSONTime)(nil)
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Marshaler interface for *JSONTime.
|
||||||
|
func (t *JSONTime) UnmarshalJSON(b []byte) (err error) {
|
||||||
|
if t == nil {
|
||||||
|
return fmt.Errorf("json time is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
msec, err := strconv.ParseFloat(string(b), 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing json time: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*t = JSONTime(time.Unix(0, int64(msec*nsecPerMsec)).UTC())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeJSONOKResponse writes headers with the code 200 OK, encodes v into w,
|
||||||
|
// and logs any errors it encounters. r is used to get additional information
|
||||||
|
// from the request.
|
||||||
|
func writeJSONOKResponse(w http.ResponseWriter, r *http.Request, v any) {
|
||||||
|
writeJSONResponse(w, r, v, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeJSONResponse writes headers with code, encodes v into w, and logs any
|
||||||
|
// errors it encounters. r is used to get additional information from the
|
||||||
|
// request.
|
||||||
|
func writeJSONResponse(w http.ResponseWriter, r *http.Request, v any, code int) {
|
||||||
|
// TODO(a.garipov): Put some of these to a middleware.
|
||||||
|
h := w.Header()
|
||||||
|
h.Set(aghhttp.HdrNameContentType, aghhttp.HdrValApplicationJSON)
|
||||||
|
h.Set(aghhttp.HdrNameServer, aghhttp.UserAgent())
|
||||||
|
|
||||||
|
w.WriteHeader(code)
|
||||||
|
|
||||||
|
err := json.NewEncoder(w).Encode(v)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("websvc: writing resp to %s %s: %s", r.Method, r.URL.Path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorCode is the error code as used by the HTTP API. See the ErrorCode
|
||||||
|
// definition in the OpenAPI specification.
|
||||||
|
type ErrorCode string
|
||||||
|
|
||||||
|
// ErrorCode constants.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Expand and document codes.
|
||||||
|
const (
|
||||||
|
// ErrorCodeTMP000 is the temporary error code used for all errors.
|
||||||
|
ErrorCodeTMP000 = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPAPIErrorResp is the error response as used by the HTTP API. See the
|
||||||
|
// BadRequestResp, InternalServerErrorResp, and similar objects in the OpenAPI
|
||||||
|
// specification.
|
||||||
|
type HTTPAPIErrorResp struct {
|
||||||
|
Code ErrorCode `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeJSONErrorResponse encodes err as a JSON error into w, and logs any
|
||||||
|
// errors it encounters. r is used to get additional information from the
|
||||||
|
// request.
|
||||||
|
func writeJSONErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
log.Error("websvc: %s %s: %s", r.Method, r.URL.Path, err)
|
||||||
|
|
||||||
|
writeJSONResponse(w, r, &HTTPAPIErrorResp{
|
||||||
|
Code: ErrorCodeTMP000,
|
||||||
|
Msg: err.Error(),
|
||||||
|
}, http.StatusUnprocessableEntity)
|
||||||
|
}
|
||||||
114
internal/next/websvc/json_test.go
Normal file
114
internal/next/websvc/json_test.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package websvc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testJSONTime is the JSON time for tests.
|
||||||
|
var testJSONTime = websvc.JSONTime(time.Unix(1_234_567_890, 123_456_000).UTC())
|
||||||
|
|
||||||
|
// testJSONTimeStr is the string with the JSON encoding of testJSONTime.
|
||||||
|
const testJSONTimeStr = "1234567890123.456"
|
||||||
|
|
||||||
|
func TestJSONTime_MarshalJSON(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
wantErrMsg string
|
||||||
|
in websvc.JSONTime
|
||||||
|
want []byte
|
||||||
|
}{{
|
||||||
|
name: "unix_zero",
|
||||||
|
wantErrMsg: "",
|
||||||
|
in: websvc.JSONTime(time.Unix(0, 0)),
|
||||||
|
want: []byte("0"),
|
||||||
|
}, {
|
||||||
|
name: "empty",
|
||||||
|
wantErrMsg: "",
|
||||||
|
in: websvc.JSONTime{},
|
||||||
|
want: []byte("-6795364578871.345"),
|
||||||
|
}, {
|
||||||
|
name: "time",
|
||||||
|
wantErrMsg: "",
|
||||||
|
in: testJSONTime,
|
||||||
|
want: []byte(testJSONTimeStr),
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got, err := tc.in.MarshalJSON()
|
||||||
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("json", func(t *testing.T) {
|
||||||
|
in := &struct {
|
||||||
|
A websvc.JSONTime
|
||||||
|
}{
|
||||||
|
A: testJSONTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := json.Marshal(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, []byte(`{"A":`+testJSONTimeStr+`}`), got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONTime_UnmarshalJSON(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
wantErrMsg string
|
||||||
|
want websvc.JSONTime
|
||||||
|
data []byte
|
||||||
|
}{{
|
||||||
|
name: "time",
|
||||||
|
wantErrMsg: "",
|
||||||
|
want: testJSONTime,
|
||||||
|
data: []byte(testJSONTimeStr),
|
||||||
|
}, {
|
||||||
|
name: "bad",
|
||||||
|
wantErrMsg: `parsing json time: strconv.ParseFloat: parsing "{}": ` +
|
||||||
|
`invalid syntax`,
|
||||||
|
want: websvc.JSONTime{},
|
||||||
|
data: []byte(`{}`),
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var got websvc.JSONTime
|
||||||
|
err := got.UnmarshalJSON(tc.data)
|
||||||
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("nil", func(t *testing.T) {
|
||||||
|
err := (*websvc.JSONTime)(nil).UnmarshalJSON([]byte("0"))
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
msg := err.Error()
|
||||||
|
assert.Equal(t, "json time is nil", msg)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("json", func(t *testing.T) {
|
||||||
|
want := testJSONTime
|
||||||
|
var got struct {
|
||||||
|
A websvc.JSONTime
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(`{"A":`+testJSONTimeStr+`}`), &got)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, want, got.A)
|
||||||
|
})
|
||||||
|
}
|
||||||
11
internal/next/websvc/path.go
Normal file
11
internal/next/websvc/path.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
// Path constants
|
||||||
|
const (
|
||||||
|
PathHealthCheck = "/health-check"
|
||||||
|
|
||||||
|
PathV1SettingsAll = "/api/v1/settings/all"
|
||||||
|
PathV1SettingsDNS = "/api/v1/settings/dns"
|
||||||
|
PathV1SettingsHTTP = "/api/v1/settings/http"
|
||||||
|
PathV1SystemInfo = "/api/v1/system/info"
|
||||||
|
)
|
||||||
42
internal/next/websvc/settings.go
Normal file
42
internal/next/websvc/settings.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// All Settings Handlers
|
||||||
|
|
||||||
|
// RespGetV1SettingsAll describes the response of the GET /api/v1/settings/all
|
||||||
|
// HTTP API.
|
||||||
|
type RespGetV1SettingsAll struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
|
||||||
|
DNS *HTTPAPIDNSSettings `json:"dns"`
|
||||||
|
HTTP *HTTPAPIHTTPSettings `json:"http"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetSettingsAll is the handler for the GET /api/v1/settings/all HTTP
|
||||||
|
// API.
|
||||||
|
func (svc *Service) handleGetSettingsAll(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dnsSvc := svc.confMgr.DNS()
|
||||||
|
dnsConf := dnsSvc.Config()
|
||||||
|
|
||||||
|
webSvc := svc.confMgr.Web()
|
||||||
|
httpConf := webSvc.Config()
|
||||||
|
|
||||||
|
// TODO(a.garipov): Add all currently supported parameters.
|
||||||
|
writeJSONOKResponse(w, r, &RespGetV1SettingsAll{
|
||||||
|
DNS: &HTTPAPIDNSSettings{
|
||||||
|
Addresses: dnsConf.Addresses,
|
||||||
|
BootstrapServers: dnsConf.BootstrapServers,
|
||||||
|
UpstreamServers: dnsConf.UpstreamServers,
|
||||||
|
UpstreamTimeout: JSONDuration(dnsConf.UpstreamTimeout),
|
||||||
|
},
|
||||||
|
HTTP: &HTTPAPIHTTPSettings{
|
||||||
|
Addresses: httpConf.Addresses,
|
||||||
|
SecureAddresses: httpConf.SecureAddresses,
|
||||||
|
Timeout: JSONDuration(httpConf.Timeout),
|
||||||
|
ForceHTTPS: httpConf.ForceHTTPS,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
74
internal/next/websvc/settings_test.go
Normal file
74
internal/next/websvc/settings_test.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package websvc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_HandleGetSettingsAll(t *testing.T) {
|
||||||
|
// TODO(a.garipov): Add all currently supported parameters.
|
||||||
|
|
||||||
|
wantDNS := &websvc.HTTPAPIDNSSettings{
|
||||||
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:53")},
|
||||||
|
BootstrapServers: []string{"94.140.14.140", "94.140.14.141"},
|
||||||
|
UpstreamServers: []string{"94.140.14.14", "1.1.1.1"},
|
||||||
|
UpstreamTimeout: websvc.JSONDuration(1 * time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
||||||
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
|
||||||
|
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
|
||||||
|
Timeout: websvc.JSONDuration(5 * time.Second),
|
||||||
|
ForceHTTPS: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
confMgr := newConfigManager()
|
||||||
|
confMgr.onDNS = func() (s websvc.ServiceWithConfig[*dnssvc.Config]) {
|
||||||
|
c, err := dnssvc.New(&dnssvc.Config{
|
||||||
|
Addresses: wantDNS.Addresses,
|
||||||
|
UpstreamServers: wantDNS.UpstreamServers,
|
||||||
|
BootstrapServers: wantDNS.BootstrapServers,
|
||||||
|
UpstreamTimeout: time.Duration(wantDNS.UpstreamTimeout),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
confMgr.onWeb = func() (s websvc.ServiceWithConfig[*websvc.Config]) {
|
||||||
|
return websvc.New(&websvc.Config{
|
||||||
|
TLS: &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{{}},
|
||||||
|
},
|
||||||
|
Addresses: wantWeb.Addresses,
|
||||||
|
SecureAddresses: wantWeb.SecureAddresses,
|
||||||
|
Timeout: time.Duration(wantWeb.Timeout),
|
||||||
|
ForceHTTPS: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, addr := newTestServer(t, confMgr)
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: addr.String(),
|
||||||
|
Path: websvc.PathV1SettingsAll,
|
||||||
|
}
|
||||||
|
|
||||||
|
body := httpGet(t, u, http.StatusOK)
|
||||||
|
resp := &websvc.RespGetV1SettingsAll{}
|
||||||
|
err := json.Unmarshal(body, resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, wantDNS, resp.DNS)
|
||||||
|
assert.Equal(t, wantWeb, resp.HTTP)
|
||||||
|
}
|
||||||
@@ -16,20 +16,20 @@ type RespGetV1SystemInfo struct {
|
|||||||
Channel string `json:"channel"`
|
Channel string `json:"channel"`
|
||||||
OS string `json:"os"`
|
OS string `json:"os"`
|
||||||
NewVersion string `json:"new_version,omitempty"`
|
NewVersion string `json:"new_version,omitempty"`
|
||||||
Start jsonTime `json:"start"`
|
Start JSONTime `json:"start"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetV1SystemInfo is the handler for the GET /api/v1/system/info HTTP
|
// handleGetV1SystemInfo is the handler for the GET /api/v1/system/info HTTP
|
||||||
// API.
|
// API.
|
||||||
func (svc *Service) handleGetV1SystemInfo(w http.ResponseWriter, r *http.Request) {
|
func (svc *Service) handleGetV1SystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
writeJSONResponse(w, r, &RespGetV1SystemInfo{
|
writeJSONOKResponse(w, r, &RespGetV1SystemInfo{
|
||||||
Arch: runtime.GOARCH,
|
Arch: runtime.GOARCH,
|
||||||
Channel: version.Channel(),
|
Channel: version.Channel(),
|
||||||
OS: runtime.GOOS,
|
OS: runtime.GOOS,
|
||||||
// TODO(a.garipov): Fill this when we have an updater.
|
// TODO(a.garipov): Fill this when we have an updater.
|
||||||
NewVersion: "",
|
NewVersion: "",
|
||||||
Start: jsonTime(svc.start),
|
Start: JSONTime(svc.start),
|
||||||
Version: version.Version(),
|
Version: version.Version(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -8,16 +8,17 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/websvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestService_handleGetV1SystemInfo(t *testing.T) {
|
func TestService_handleGetV1SystemInfo(t *testing.T) {
|
||||||
_, addr := newTestServer(t)
|
confMgr := newConfigManager()
|
||||||
|
_, addr := newTestServer(t, confMgr)
|
||||||
u := &url.URL{
|
u := &url.URL{
|
||||||
Scheme: "http",
|
Scheme: "http",
|
||||||
Host: addr,
|
Host: addr.String(),
|
||||||
Path: websvc.PathV1SystemInfo,
|
Path: websvc.PathV1SystemInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
31
internal/next/websvc/waitlistener.go
Normal file
31
internal/next/websvc/waitlistener.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wait Listener
|
||||||
|
|
||||||
|
// waitListener is a wrapper around a listener that also calls wg.Done() on the
|
||||||
|
// first call to Accept. It is useful in situations where it is important to
|
||||||
|
// catch the precise moment of the first call to Accept, for example when
|
||||||
|
// starting an HTTP server.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Move to aghnet?
|
||||||
|
type waitListener struct {
|
||||||
|
net.Listener
|
||||||
|
|
||||||
|
firstAcceptWG *sync.WaitGroup
|
||||||
|
firstAcceptOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ net.Listener = (*waitListener)(nil)
|
||||||
|
|
||||||
|
// Accept implements the [net.Listener] interface for *waitListener.
|
||||||
|
func (l *waitListener) Accept() (conn net.Conn, err error) {
|
||||||
|
l.firstAcceptOnce.Do(l.firstAcceptWG.Done)
|
||||||
|
|
||||||
|
return l.Listener.Accept()
|
||||||
|
}
|
||||||
46
internal/next/websvc/waitlistener_internal_test.go
Normal file
46
internal/next/websvc/waitlistener_internal_test.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghchan"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWaitListener_Accept(t *testing.T) {
|
||||||
|
// TODO(a.garipov): use atomic.Bool in Go 1.19.
|
||||||
|
var numAcceptCalls uint32
|
||||||
|
var l net.Listener = &aghtest.Listener{
|
||||||
|
OnAccept: func() (conn net.Conn, err error) {
|
||||||
|
atomic.AddUint32(&numAcceptCalls, 1)
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
OnAddr: func() (addr net.Addr) { panic("not implemented") },
|
||||||
|
OnClose: func() (err error) { panic("not implemented") },
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go aghchan.MustReceive(done, testTimeout)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var wrapper net.Listener = &waitListener{
|
||||||
|
Listener: l,
|
||||||
|
firstAcceptWG: wg,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = wrapper.Accept()
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(done)
|
||||||
|
|
||||||
|
assert.Equal(t, uint32(1), atomic.LoadUint32(&numAcceptCalls))
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
// Package websvc contains the AdGuard Home web service.
|
// Package websvc contains the AdGuard Home HTTP API service.
|
||||||
|
//
|
||||||
|
// NOTE: Packages other than cmd must not import this package, as it imports
|
||||||
|
// most other packages.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Add tests.
|
// TODO(a.garipov): Add tests.
|
||||||
package websvc
|
package websvc
|
||||||
@@ -14,18 +17,46 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/agh"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
httptreemux "github.com/dimfeld/httptreemux/v5"
|
httptreemux "github.com/dimfeld/httptreemux/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ServiceWithConfig is an extension of the [agh.Service] interface for services
|
||||||
|
// that can return their configuration.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Consider removing this generic interface if we figure out
|
||||||
|
// how to make it testable in a better way.
|
||||||
|
type ServiceWithConfig[ConfigType any] interface {
|
||||||
|
agh.Service
|
||||||
|
|
||||||
|
Config() (c ConfigType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigManager is the configuration manager interface.
|
||||||
|
type ConfigManager interface {
|
||||||
|
DNS() (svc ServiceWithConfig[*dnssvc.Config])
|
||||||
|
Web() (svc ServiceWithConfig[*Config])
|
||||||
|
|
||||||
|
UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error)
|
||||||
|
UpdateWeb(ctx context.Context, c *Config) (err error)
|
||||||
|
}
|
||||||
|
|
||||||
// Config is the AdGuard Home web service configuration structure.
|
// Config is the AdGuard Home web service configuration structure.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
// ConfigManager is used to show information about services as well as
|
||||||
|
// dynamically reconfigure them.
|
||||||
|
ConfigManager ConfigManager
|
||||||
|
|
||||||
// TLS is the optional TLS configuration. If TLS is not nil,
|
// TLS is the optional TLS configuration. If TLS is not nil,
|
||||||
// SecureAddresses must not be empty.
|
// SecureAddresses must not be empty.
|
||||||
TLS *tls.Config
|
TLS *tls.Config
|
||||||
|
|
||||||
|
// Start is the time of start of AdGuard Home.
|
||||||
|
Start time.Time
|
||||||
|
|
||||||
// Addresses are the addresses on which to serve the plain HTTP API.
|
// Addresses are the addresses on which to serve the plain HTTP API.
|
||||||
Addresses []netip.AddrPort
|
Addresses []netip.AddrPort
|
||||||
|
|
||||||
@@ -33,40 +64,48 @@ type Config struct {
|
|||||||
// SecureAddresses is not empty, TLS must not be nil.
|
// SecureAddresses is not empty, TLS must not be nil.
|
||||||
SecureAddresses []netip.AddrPort
|
SecureAddresses []netip.AddrPort
|
||||||
|
|
||||||
// Start is the time of start of AdGuard Home.
|
|
||||||
Start time.Time
|
|
||||||
|
|
||||||
// Timeout is the timeout for all server operations.
|
// Timeout is the timeout for all server operations.
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
|
|
||||||
|
// ForceHTTPS tells if all requests to Addresses should be redirected to a
|
||||||
|
// secure address instead.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Use; define rules, which address to redirect to.
|
||||||
|
ForceHTTPS bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service is the AdGuard Home web service. A nil *Service is a valid
|
// Service is the AdGuard Home web service. A nil *Service is a valid
|
||||||
// [agh.Service] that does nothing.
|
// [agh.Service] that does nothing.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
tls *tls.Config
|
confMgr ConfigManager
|
||||||
servers []*http.Server
|
tls *tls.Config
|
||||||
start time.Time
|
start time.Time
|
||||||
timeout time.Duration
|
servers []*http.Server
|
||||||
|
timeout time.Duration
|
||||||
|
forceHTTPS bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new properly initialized *Service. If c is nil, svc is a nil
|
// New returns a new properly initialized *Service. If c is nil, svc is a nil
|
||||||
// *Service that does nothing.
|
// *Service that does nothing. The fields of c must not be modified after
|
||||||
|
// calling New.
|
||||||
func New(c *Config) (svc *Service) {
|
func New(c *Config) (svc *Service) {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
svc = &Service{
|
svc = &Service{
|
||||||
tls: c.TLS,
|
confMgr: c.ConfigManager,
|
||||||
start: c.Start,
|
tls: c.TLS,
|
||||||
timeout: c.Timeout,
|
start: c.Start,
|
||||||
|
timeout: c.Timeout,
|
||||||
|
forceHTTPS: c.ForceHTTPS,
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := newMux(svc)
|
mux := newMux(svc)
|
||||||
|
|
||||||
for _, a := range c.Addresses {
|
for _, a := range c.Addresses {
|
||||||
addr := a.String()
|
addr := a.String()
|
||||||
errLog := log.StdLog("websvc: http: "+addr, log.ERROR)
|
errLog := log.StdLog("websvc: plain http: "+addr, log.ERROR)
|
||||||
svc.servers = append(svc.servers, &http.Server{
|
svc.servers = append(svc.servers, &http.Server{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
@@ -111,6 +150,21 @@ func newMux(svc *Service) (mux *httptreemux.ContextMux) {
|
|||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: PathHealthCheck,
|
path: PathHealthCheck,
|
||||||
isJSON: false,
|
isJSON: false,
|
||||||
|
}, {
|
||||||
|
handler: svc.handleGetSettingsAll,
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: PathV1SettingsAll,
|
||||||
|
isJSON: true,
|
||||||
|
}, {
|
||||||
|
handler: svc.handlePatchSettingsDNS,
|
||||||
|
method: http.MethodPatch,
|
||||||
|
path: PathV1SettingsDNS,
|
||||||
|
isJSON: true,
|
||||||
|
}, {
|
||||||
|
handler: svc.handlePatchSettingsHTTP,
|
||||||
|
method: http.MethodPatch,
|
||||||
|
path: PathV1SettingsHTTP,
|
||||||
|
isJSON: true,
|
||||||
}, {
|
}, {
|
||||||
handler: svc.handleGetV1SystemInfo,
|
handler: svc.handleGetV1SystemInfo,
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
@@ -119,29 +173,41 @@ func newMux(svc *Service) (mux *httptreemux.ContextMux) {
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
for _, r := range routes {
|
for _, r := range routes {
|
||||||
var h http.HandlerFunc
|
|
||||||
if r.isJSON {
|
if r.isJSON {
|
||||||
// TODO(a.garipov): Consider using httptreemux's MiddlewareFunc.
|
mux.Handle(r.method, r.path, jsonMw(r.handler))
|
||||||
h = jsonMw(r.handler)
|
|
||||||
} else {
|
} else {
|
||||||
h = r.handler
|
mux.Handle(r.method, r.path, r.handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
mux.Handle(r.method, r.path, h)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
// Addrs returns all addresses on which this server serves the HTTP API. Addrs
|
// addrs returns all addresses on which this server serves the HTTP API. addrs
|
||||||
// must not be called until Start returns.
|
// must not be called simultaneously with Start. If svc was initialized with
|
||||||
func (svc *Service) Addrs() (addrs []string) {
|
// ":0" addresses, addrs will not return the actual bound ports until Start is
|
||||||
addrs = make([]string, 0, len(svc.servers))
|
// finished.
|
||||||
|
func (svc *Service) addrs() (addrs, secureAddrs []netip.AddrPort) {
|
||||||
for _, srv := range svc.servers {
|
for _, srv := range svc.servers {
|
||||||
addrs = append(addrs, srv.Addr)
|
addrPort, err := netip.ParseAddrPort(srv.Addr)
|
||||||
|
if err != nil {
|
||||||
|
// Technically shouldn't happen, since all servers must have a valid
|
||||||
|
// address.
|
||||||
|
panic(fmt.Errorf("websvc: server %q: bad address: %w", srv.Addr, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// srv.Serve will set TLSConfig to an almost empty value, so, instead of
|
||||||
|
// relying only on the nilness of TLSConfig, check the length of the
|
||||||
|
// certificates field as well.
|
||||||
|
if srv.TLSConfig == nil || len(srv.TLSConfig.Certificates) == 0 {
|
||||||
|
addrs = append(addrs, addrPort)
|
||||||
|
} else {
|
||||||
|
secureAddrs = append(secureAddrs, addrPort)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return addrs
|
return addrs, secureAddrs
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetHealthCheck is the handler for the GET /health-check HTTP API.
|
// handleGetHealthCheck is the handler for the GET /health-check HTTP API.
|
||||||
@@ -149,9 +215,6 @@ func (svc *Service) handleGetHealthCheck(w http.ResponseWriter, _ *http.Request)
|
|||||||
_, _ = io.WriteString(w, "OK")
|
_, _ = io.WriteString(w, "OK")
|
||||||
}
|
}
|
||||||
|
|
||||||
// unit is a convenient alias for struct{}.
|
|
||||||
type unit = struct{}
|
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ agh.Service = (*Service)(nil)
|
var _ agh.Service = (*Service)(nil)
|
||||||
|
|
||||||
@@ -163,11 +226,9 @@ func (svc *Service) Start() (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
srvs := svc.servers
|
|
||||||
|
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
wg.Add(len(srvs))
|
wg.Add(len(svc.servers))
|
||||||
for _, srv := range srvs {
|
for _, srv := range svc.servers {
|
||||||
go serve(srv, wg)
|
go serve(srv, wg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,11 +242,14 @@ func serve(srv *http.Server, wg *sync.WaitGroup) {
|
|||||||
addr := srv.Addr
|
addr := srv.Addr
|
||||||
defer log.OnPanic(addr)
|
defer log.OnPanic(addr)
|
||||||
|
|
||||||
|
var proto string
|
||||||
var l net.Listener
|
var l net.Listener
|
||||||
var err error
|
var err error
|
||||||
if srv.TLSConfig == nil {
|
if srv.TLSConfig == nil {
|
||||||
|
proto = "http"
|
||||||
l, err = net.Listen("tcp", addr)
|
l, err = net.Listen("tcp", addr)
|
||||||
} else {
|
} else {
|
||||||
|
proto = "https"
|
||||||
l, err = tls.Listen("tcp", addr, srv.TLSConfig)
|
l, err = tls.Listen("tcp", addr, srv.TLSConfig)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -196,8 +260,12 @@ func serve(srv *http.Server, wg *sync.WaitGroup) {
|
|||||||
// would mean that a random available port was automatically chosen.
|
// would mean that a random available port was automatically chosen.
|
||||||
srv.Addr = l.Addr().String()
|
srv.Addr = l.Addr().String()
|
||||||
|
|
||||||
log.Info("websvc: starting srv http://%s", srv.Addr)
|
log.Info("websvc: starting srv %s://%s", proto, srv.Addr)
|
||||||
wg.Done()
|
|
||||||
|
l = &waitListener{
|
||||||
|
Listener: l,
|
||||||
|
firstAcceptWG: wg,
|
||||||
|
}
|
||||||
|
|
||||||
err = srv.Serve(l)
|
err = srv.Serve(l)
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
@@ -221,8 +289,28 @@ func (svc *Service) Shutdown(ctx context.Context) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return errors.List("shutting down")
|
return errors.List("shutting down", errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Config returns the current configuration of the web service. Config must not
|
||||||
|
// be called simultaneously with Start. If svc was initialized with ":0"
|
||||||
|
// addresses, addrs will not return the actual bound ports until Start is
|
||||||
|
// finished.
|
||||||
|
func (svc *Service) Config() (c *Config) {
|
||||||
|
c = &Config{
|
||||||
|
ConfigManager: svc.confMgr,
|
||||||
|
TLS: svc.tls,
|
||||||
|
// Leave Addresses and SecureAddresses empty and get the actual
|
||||||
|
// addresses that include the :0 ones later.
|
||||||
|
Start: svc.start,
|
||||||
|
Timeout: svc.timeout,
|
||||||
|
ForceHTTPS: svc.forceHTTPS,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Addresses, c.SecureAddresses = svc.addrs()
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
6
internal/next/websvc/websvc_internal_test.go
Normal file
6
internal/next/websvc/websvc_internal_test.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// testTimeout is the common timeout for tests.
|
||||||
|
const testTimeout = 1 * time.Second
|
||||||
187
internal/next/websvc/websvc_test.go
Normal file
187
internal/next/websvc/websvc_test.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
package websvc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
aghtest.DiscardLogOutput(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testTimeout is the common timeout for tests.
|
||||||
|
const testTimeout = 1 * time.Second
|
||||||
|
|
||||||
|
// testStart is the server start value for tests.
|
||||||
|
var testStart = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ websvc.ConfigManager = (*configManager)(nil)
|
||||||
|
|
||||||
|
// configManager is a [websvc.ConfigManager] for tests.
|
||||||
|
type configManager struct {
|
||||||
|
onDNS func() (svc websvc.ServiceWithConfig[*dnssvc.Config])
|
||||||
|
onWeb func() (svc websvc.ServiceWithConfig[*websvc.Config])
|
||||||
|
|
||||||
|
onUpdateDNS func(ctx context.Context, c *dnssvc.Config) (err error)
|
||||||
|
onUpdateWeb func(ctx context.Context, c *websvc.Config) (err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS implements the [websvc.ConfigManager] interface for *configManager.
|
||||||
|
func (m *configManager) DNS() (svc websvc.ServiceWithConfig[*dnssvc.Config]) {
|
||||||
|
return m.onDNS()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web implements the [websvc.ConfigManager] interface for *configManager.
|
||||||
|
func (m *configManager) Web() (svc websvc.ServiceWithConfig[*websvc.Config]) {
|
||||||
|
return m.onWeb()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDNS implements the [websvc.ConfigManager] interface for *configManager.
|
||||||
|
func (m *configManager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||||
|
return m.onUpdateDNS(ctx, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateWeb implements the [websvc.ConfigManager] interface for *configManager.
|
||||||
|
func (m *configManager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
||||||
|
return m.onUpdateWeb(ctx, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newConfigManager returns a *configManager all methods of which panic.
|
||||||
|
func newConfigManager() (m *configManager) {
|
||||||
|
return &configManager{
|
||||||
|
onDNS: func() (svc websvc.ServiceWithConfig[*dnssvc.Config]) { panic("not implemented") },
|
||||||
|
onWeb: func() (svc websvc.ServiceWithConfig[*websvc.Config]) { panic("not implemented") },
|
||||||
|
onUpdateDNS: func(_ context.Context, _ *dnssvc.Config) (err error) {
|
||||||
|
panic("not implemented")
|
||||||
|
},
|
||||||
|
onUpdateWeb: func(_ context.Context, _ *websvc.Config) (err error) {
|
||||||
|
panic("not implemented")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTestServer creates and starts a new web service instance as well as its
|
||||||
|
// sole address. It also registers a cleanup procedure, which shuts the
|
||||||
|
// instance down.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Use svc or remove it.
|
||||||
|
func newTestServer(
|
||||||
|
t testing.TB,
|
||||||
|
confMgr websvc.ConfigManager,
|
||||||
|
) (svc *websvc.Service, addr netip.AddrPort) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
c := &websvc.Config{
|
||||||
|
ConfigManager: confMgr,
|
||||||
|
TLS: nil,
|
||||||
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:0")},
|
||||||
|
SecureAddresses: nil,
|
||||||
|
Timeout: testTimeout,
|
||||||
|
Start: testStart,
|
||||||
|
ForceHTTPS: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
svc = websvc.New(c)
|
||||||
|
|
||||||
|
err := svc.Start()
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
err = svc.Shutdown(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
c = svc.Config()
|
||||||
|
require.NotNil(t, c)
|
||||||
|
require.Len(t, c.Addresses, 1)
|
||||||
|
|
||||||
|
return svc, c.Addresses[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// jobj is a utility alias for JSON objects.
|
||||||
|
type jobj map[string]any
|
||||||
|
|
||||||
|
// httpGet is a helper that performs an HTTP GET request and returns the body of
|
||||||
|
// the response as well as checks that the status code is correct.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Add helpers for other methods.
|
||||||
|
func httpGet(t testing.TB, u *url.URL, wantCode int) (body []byte) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||||
|
require.NoErrorf(t, err, "creating req")
|
||||||
|
|
||||||
|
httpCli := &http.Client{
|
||||||
|
Timeout: testTimeout,
|
||||||
|
}
|
||||||
|
resp, err := httpCli.Do(req)
|
||||||
|
require.NoErrorf(t, err, "performing req")
|
||||||
|
require.Equal(t, wantCode, resp.StatusCode)
|
||||||
|
|
||||||
|
testutil.CleanupAndRequireSuccess(t, resp.Body.Close)
|
||||||
|
|
||||||
|
body, err = io.ReadAll(resp.Body)
|
||||||
|
require.NoErrorf(t, err, "reading body")
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// httpPatch is a helper that performs an HTTP PATCH request with JSON-encoded
|
||||||
|
// reqBody as the request body and returns the body of the response as well as
|
||||||
|
// checks that the status code is correct.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Add helpers for other methods.
|
||||||
|
func httpPatch(t testing.TB, u *url.URL, reqBody any, wantCode int) (body []byte) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
b, err := json.Marshal(reqBody)
|
||||||
|
require.NoErrorf(t, err, "marshaling reqBody")
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPatch, u.String(), bytes.NewReader(b))
|
||||||
|
require.NoErrorf(t, err, "creating req")
|
||||||
|
|
||||||
|
httpCli := &http.Client{
|
||||||
|
Timeout: testTimeout,
|
||||||
|
}
|
||||||
|
resp, err := httpCli.Do(req)
|
||||||
|
require.NoErrorf(t, err, "performing req")
|
||||||
|
require.Equal(t, wantCode, resp.StatusCode)
|
||||||
|
|
||||||
|
testutil.CleanupAndRequireSuccess(t, resp.Body.Close)
|
||||||
|
|
||||||
|
body, err = io.ReadAll(resp.Body)
|
||||||
|
require.NoErrorf(t, err, "reading body")
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_Start_getHealthCheck(t *testing.T) {
|
||||||
|
confMgr := newConfigManager()
|
||||||
|
_, addr := newTestServer(t, confMgr)
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: addr.String(),
|
||||||
|
Path: websvc.PathHealthCheck,
|
||||||
|
}
|
||||||
|
|
||||||
|
body := httpGet(t, u, http.StatusOK)
|
||||||
|
|
||||||
|
assert.Equal(t, []byte("OK"), body)
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package websvc
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// JSON Utilities
|
|
||||||
|
|
||||||
// jsonTime is a time.Time that can be decoded from JSON and encoded into JSON
|
|
||||||
// according to our API conventions.
|
|
||||||
type jsonTime time.Time
|
|
||||||
|
|
||||||
// type check
|
|
||||||
var _ json.Marshaler = jsonTime{}
|
|
||||||
|
|
||||||
// nsecPerMsec is the number of nanoseconds in a millisecond.
|
|
||||||
const nsecPerMsec = float64(time.Millisecond / time.Nanosecond)
|
|
||||||
|
|
||||||
// MarshalJSON implements the json.Marshaler interface for jsonTime. err is
|
|
||||||
// always nil.
|
|
||||||
func (t jsonTime) MarshalJSON() (b []byte, err error) {
|
|
||||||
msec := float64(time.Time(t).UnixNano()) / nsecPerMsec
|
|
||||||
b = strconv.AppendFloat(nil, msec, 'f', 3, 64)
|
|
||||||
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// type check
|
|
||||||
var _ json.Unmarshaler = (*jsonTime)(nil)
|
|
||||||
|
|
||||||
// UnmarshalJSON implements the json.Marshaler interface for *jsonTime.
|
|
||||||
func (t *jsonTime) UnmarshalJSON(b []byte) (err error) {
|
|
||||||
if t == nil {
|
|
||||||
return fmt.Errorf("json time is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
msec, err := strconv.ParseFloat(string(b), 64)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("parsing json time: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
*t = jsonTime(time.Unix(0, int64(msec*nsecPerMsec)).UTC())
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeJSONResponse encodes v into w and logs any errors it encounters. r is
|
|
||||||
// used to get additional information from the request.
|
|
||||||
func writeJSONResponse(w io.Writer, r *http.Request, v any) {
|
|
||||||
err := json.NewEncoder(w).Encode(v)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("websvc: writing resp to %s %s: %s", r.Method, r.URL.Path, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package websvc
|
|
||||||
|
|
||||||
// Path constants
|
|
||||||
const (
|
|
||||||
PathHealthCheck = "/health-check"
|
|
||||||
|
|
||||||
PathV1SystemInfo = "/api/v1/system/info"
|
|
||||||
)
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package websvc_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
|
||||||
"net/url"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/websvc"
|
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
const testTimeout = 1 * time.Second
|
|
||||||
|
|
||||||
// testStart is the server start value for tests.
|
|
||||||
var testStart = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
||||||
|
|
||||||
// newTestServer creates and starts a new web service instance as well as its
|
|
||||||
// sole address. It also registers a cleanup procedure, which shuts the
|
|
||||||
// instance down.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Use svc or remove it.
|
|
||||||
func newTestServer(t testing.TB) (svc *websvc.Service, addr string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
c := &websvc.Config{
|
|
||||||
TLS: nil,
|
|
||||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:0")},
|
|
||||||
SecureAddresses: nil,
|
|
||||||
Timeout: testTimeout,
|
|
||||||
Start: testStart,
|
|
||||||
}
|
|
||||||
|
|
||||||
svc = websvc.New(c)
|
|
||||||
|
|
||||||
err := svc.Start()
|
|
||||||
require.NoError(t, err)
|
|
||||||
t.Cleanup(func() {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
|
||||||
t.Cleanup(cancel)
|
|
||||||
|
|
||||||
err = svc.Shutdown(ctx)
|
|
||||||
require.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
addrs := svc.Addrs()
|
|
||||||
require.Len(t, addrs, 1)
|
|
||||||
|
|
||||||
return svc, addrs[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// httpGet is a helper that performs an HTTP GET request and returns the body of
|
|
||||||
// the response as well as checks that the status code is correct.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Add helpers for other methods.
|
|
||||||
func httpGet(t testing.TB, u *url.URL, wantCode int) (body []byte) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
|
||||||
require.NoErrorf(t, err, "creating req")
|
|
||||||
|
|
||||||
httpCli := &http.Client{
|
|
||||||
Timeout: testTimeout,
|
|
||||||
}
|
|
||||||
resp, err := httpCli.Do(req)
|
|
||||||
require.NoErrorf(t, err, "performing req")
|
|
||||||
require.Equal(t, wantCode, resp.StatusCode)
|
|
||||||
|
|
||||||
testutil.CleanupAndRequireSuccess(t, resp.Body.Close)
|
|
||||||
|
|
||||||
body, err = io.ReadAll(resp.Body)
|
|
||||||
require.NoErrorf(t, err, "reading body")
|
|
||||||
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestService_Start_getHealthCheck(t *testing.T) {
|
|
||||||
_, addr := newTestServer(t)
|
|
||||||
u := &url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: addr,
|
|
||||||
Path: websvc.PathHealthCheck,
|
|
||||||
}
|
|
||||||
|
|
||||||
body := httpGet(t, u, http.StatusOK)
|
|
||||||
|
|
||||||
assert.Equal(t, []byte("OK"), body)
|
|
||||||
}
|
|
||||||
@@ -63,14 +63,6 @@ func Version() (v string) {
|
|||||||
return version
|
return version
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constants defining the format of module information string.
|
|
||||||
const (
|
|
||||||
modInfoAtSep = "@"
|
|
||||||
modInfoDevSep = " "
|
|
||||||
modInfoSumLeft = " (sum: "
|
|
||||||
modInfoSumRight = ")"
|
|
||||||
)
|
|
||||||
|
|
||||||
// fmtModule returns formatted information about module. The result looks like:
|
// fmtModule returns formatted information about module. The result looks like:
|
||||||
//
|
//
|
||||||
// github.com/Username/module@v1.2.3 (sum: someHASHSUM=)
|
// github.com/Username/module@v1.2.3 (sum: someHASHSUM=)
|
||||||
@@ -87,14 +79,16 @@ func fmtModule(m *debug.Module) (formatted string) {
|
|||||||
|
|
||||||
stringutil.WriteToBuilder(b, m.Path)
|
stringutil.WriteToBuilder(b, m.Path)
|
||||||
if ver := m.Version; ver != "" {
|
if ver := m.Version; ver != "" {
|
||||||
sep := modInfoAtSep
|
sep := "@"
|
||||||
if ver == "(devel)" {
|
if ver == "(devel)" {
|
||||||
sep = modInfoDevSep
|
sep = " "
|
||||||
}
|
}
|
||||||
|
|
||||||
stringutil.WriteToBuilder(b, sep, ver)
|
stringutil.WriteToBuilder(b, sep, ver)
|
||||||
}
|
}
|
||||||
|
|
||||||
if sum := m.Sum; sum != "" {
|
if sum := m.Sum; sum != "" {
|
||||||
stringutil.WriteToBuilder(b, modInfoSumLeft, sum, modInfoSumRight)
|
stringutil.WriteToBuilder(b, "(sum: ", sum, ")")
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.String()
|
return b.String()
|
||||||
|
|||||||
4
main.go
4
main.go
@@ -1,5 +1,5 @@
|
|||||||
//go:build !v1
|
//go:build !next
|
||||||
// +build !v1
|
// +build !next
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
//go:build v1
|
//go:build next
|
||||||
// +build v1
|
// +build next
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/cmd"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/cmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Embed the prebuilt client here since we strive to keep .go files inside the
|
// Embed the prebuilt client here since we strive to keep .go files inside the
|
||||||
@@ -2289,7 +2289,7 @@
|
|||||||
'upstream_servers':
|
'upstream_servers':
|
||||||
- '1.1.1.1'
|
- '1.1.1.1'
|
||||||
- '8.8.8.8'
|
- '8.8.8.8'
|
||||||
'upstream_timeout': '1s'
|
'upstream_timeout': 1000
|
||||||
'required':
|
'required':
|
||||||
- 'addresses'
|
- 'addresses'
|
||||||
- 'blocking_mode'
|
- 'blocking_mode'
|
||||||
@@ -2397,8 +2397,9 @@
|
|||||||
'type': 'array'
|
'type': 'array'
|
||||||
'upstream_timeout':
|
'upstream_timeout':
|
||||||
'description': >
|
'description': >
|
||||||
Upstream request timeout, as a human readable duration.
|
Upstream request timeout, in milliseconds.
|
||||||
'type': 'string'
|
'format': 'double'
|
||||||
|
'type': 'number'
|
||||||
'type': 'object'
|
'type': 'object'
|
||||||
|
|
||||||
'DnsType':
|
'DnsType':
|
||||||
@@ -3505,14 +3506,16 @@
|
|||||||
'addresses':
|
'addresses':
|
||||||
- '127.0.0.1:80'
|
- '127.0.0.1:80'
|
||||||
- '192.168.1.1:80'
|
- '192.168.1.1:80'
|
||||||
|
'force_https': true
|
||||||
'secure_addresses':
|
'secure_addresses':
|
||||||
- '127.0.0.1:443'
|
- '127.0.0.1:443'
|
||||||
- '192.168.1.1:443'
|
- '192.168.1.1:443'
|
||||||
'force_https': true
|
'timeout': 10000
|
||||||
'required':
|
'required':
|
||||||
- 'addresses'
|
- 'addresses'
|
||||||
- 'secure_addresses'
|
|
||||||
- 'force_https'
|
- 'force_https'
|
||||||
|
- 'secure_addresses'
|
||||||
|
- 'timeout'
|
||||||
|
|
||||||
'HttpSettingsPatch':
|
'HttpSettingsPatch':
|
||||||
'description': >
|
'description': >
|
||||||
@@ -3539,6 +3542,11 @@
|
|||||||
'items':
|
'items':
|
||||||
'type': 'string'
|
'type': 'string'
|
||||||
'type': 'array'
|
'type': 'array'
|
||||||
|
'timeout':
|
||||||
|
'description': >
|
||||||
|
HTTP request timeout, in milliseconds.
|
||||||
|
'format': 'double'
|
||||||
|
'type': 'number'
|
||||||
'type': 'object'
|
'type': 'object'
|
||||||
|
|
||||||
'InternalServerErrorResp':
|
'InternalServerErrorResp':
|
||||||
|
|||||||
@@ -136,11 +136,11 @@ underscores() {
|
|||||||
-e '_freebsd.go'\
|
-e '_freebsd.go'\
|
||||||
-e '_linux.go'\
|
-e '_linux.go'\
|
||||||
-e '_little.go'\
|
-e '_little.go'\
|
||||||
|
-e '_next.go'\
|
||||||
-e '_openbsd.go'\
|
-e '_openbsd.go'\
|
||||||
-e '_others.go'\
|
-e '_others.go'\
|
||||||
-e '_test.go'\
|
-e '_test.go'\
|
||||||
-e '_unix.go'\
|
-e '_unix.go'\
|
||||||
-e '_v1.go'\
|
|
||||||
-e '_windows.go' \
|
-e '_windows.go' \
|
||||||
-v\
|
-v\
|
||||||
| sed -e 's/./\t\0/'
|
| sed -e 's/./\t\0/'
|
||||||
@@ -229,7 +229,7 @@ gocyclo --over 13 ./internal/filtering/
|
|||||||
# Apply stricter standards to new or somewhat refactored code.
|
# Apply stricter standards to new or somewhat refactored code.
|
||||||
gocyclo --over 10 ./internal/aghio/ ./internal/aghnet/ ./internal/aghos/\
|
gocyclo --over 10 ./internal/aghio/ ./internal/aghnet/ ./internal/aghos/\
|
||||||
./internal/aghtest/ ./internal/dnsforward/ ./internal/stats/\
|
./internal/aghtest/ ./internal/dnsforward/ ./internal/stats/\
|
||||||
./internal/tools/ ./internal/updater/ ./internal/v1/ ./internal/version/\
|
./internal/tools/ ./internal/updater/ ./internal/next/ ./internal/version/\
|
||||||
./main.go
|
./main.go
|
||||||
|
|
||||||
ineffassign ./...
|
ineffassign ./...
|
||||||
|
|||||||
Reference in New Issue
Block a user