diff --git a/CHANGELOG.md b/CHANGELOG.md index 492ba690..71b93339 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,12 +18,54 @@ and this project adheres to + + + +## [v0.107.15] - 2022-10-03 See also the [v0.107.15 GitHub milestone][ms-v0.107.15]. +### Security + +- As an additional CSRF protection measure, AdGuard Home now ensures that + requests that change its state but have no body (such as `POST + /control/stats_reset` requests) do not have a `Content-Type` header set on + 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 + +- User-specific blocked services not applying correctly ([#4945], [#4982], + [#4983]). +- `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 +[#4982]: https://github.com/AdguardTeam/AdGuardHome/issues/4982 +[#4983]: https://github.com/AdguardTeam/AdGuardHome/issues/4983 + [ms-v0.107.15]: https://github.com/AdguardTeam/AdGuardHome/milestone/51?closed=1 ---> @@ -63,8 +105,8 @@ bodies are documented in `openapi/openapi.yaml` and `openapi/CHANGELOG.md`. #### Stricter Content-Type Checks (BREAKING API CHANGE) -All JSON APIs now check if the request actually has the `application/json` -content-type. +All JSON APIs that expect a body now check if the request actually has +`Content-Type` set to `application/json`. #### Other Security Changes @@ -1283,11 +1325,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2]. -[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.14...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.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.12]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.11...v0.107.12 diff --git a/client/src/api/Api.js b/client/src/api/Api.js index 113c2c00..036f9050 100644 --- a/client/src/api/Api.js +++ b/client/src/api/Api.js @@ -10,11 +10,17 @@ class Api { async makeRequest(path, method = 'POST', config) { const url = `${this.baseUrl}/${path}`; + const axiosConfig = config || {}; + if (method !== 'GET' && axiosConfig.data) { + axiosConfig.headers = axiosConfig.headers || {}; + axiosConfig.headers['Content-Type'] = axiosConfig.headers['Content-Type'] || 'application/json'; + } + try { const response = await axios({ url, method, - ...config, + ...axiosConfig, }); return response.data; } catch (error) { @@ -55,7 +61,6 @@ class Api { const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS; const config = { data: servers, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, config); } @@ -64,7 +69,6 @@ class Api { const { path, method } = this.GLOBAL_VERSION; const config = { data, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, config); } @@ -100,7 +104,6 @@ class Api { const { path, method } = this.FILTERING_REFRESH; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); @@ -110,7 +113,6 @@ class Api { const { path, method } = this.FILTERING_ADD_FILTER; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); @@ -120,7 +122,6 @@ class Api { const { path, method } = this.FILTERING_REMOVE_FILTER; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); @@ -130,7 +131,6 @@ class Api { const { path, method } = this.FILTERING_SET_RULES; const parameters = { data: rules, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -139,7 +139,6 @@ class Api { const { path, method } = this.FILTERING_CONFIG; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -148,7 +147,6 @@ class Api { const { path, method } = this.FILTERING_SET_URL; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -239,7 +237,6 @@ class Api { const { path, method } = this.CHANGE_LANGUAGE; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -275,7 +272,6 @@ class Api { const { path, method } = this.DHCP_SET_CONFIG; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -284,7 +280,6 @@ class Api { const { path, method } = this.DHCP_FIND_ACTIVE; const parameters = { data: req, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -293,7 +288,6 @@ class Api { const { path, method } = this.DHCP_ADD_STATIC_LEASE; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -302,7 +296,6 @@ class Api { const { path, method } = this.DHCP_REMOVE_STATIC_LEASE; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -333,7 +326,6 @@ class Api { const { path, method } = this.INSTALL_CONFIGURE; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -342,7 +334,6 @@ class Api { const { path, method } = this.INSTALL_CHECK_CONFIG; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -363,7 +354,6 @@ class Api { const { path, method } = this.TLS_CONFIG; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -372,7 +362,6 @@ class Api { const { path, method } = this.TLS_VALIDATE; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -397,7 +386,6 @@ class Api { const { path, method } = this.ADD_CLIENT; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -406,7 +394,6 @@ class Api { const { path, method } = this.DELETE_CLIENT; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -415,7 +402,6 @@ class Api { const { path, method } = this.UPDATE_CLIENT; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -440,7 +426,6 @@ class Api { const { path, method } = this.ACCESS_SET; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -461,7 +446,6 @@ class Api { const { path, method } = this.REWRITE_ADD; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -470,7 +454,6 @@ class Api { const { path, method } = this.REWRITE_DELETE; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -496,7 +479,6 @@ class Api { const { path, method } = this.BLOCKED_SERVICES_SET; const parameters = { data: config, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, parameters); } @@ -524,7 +506,6 @@ class Api { const { path, method } = this.STATS_CONFIG; const config = { data, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, config); } @@ -560,7 +541,6 @@ class Api { const { path, method } = this.QUERY_LOG_CONFIG; const config = { data, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, config); } @@ -577,7 +557,6 @@ class Api { const { path, method } = this.LOGIN; const config = { data, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, config); } @@ -604,7 +583,6 @@ class Api { const { path, method } = this.SET_DNS_CONFIG; const config = { data, - headers: { 'Content-Type': 'application/json' }, }; return this.makeRequest(path, method, config); } diff --git a/go.mod b/go.mod index bc090305..b7acd047 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome go 1.18 require ( - github.com/AdguardTeam/dnsproxy v0.44.0 + github.com/AdguardTeam/dnsproxy v0.45.2 github.com/AdguardTeam/golibs v0.10.9 github.com/AdguardTeam/urlfilter v0.16.0 github.com/NYTimes/gziphandler v1.1.1 @@ -18,7 +18,7 @@ require ( github.com/google/uuid v1.3.0 github.com/insomniacslk/dhcp v0.0.0-20220822114210-de18a9d48e84 github.com/kardianos/service v1.2.1 - github.com/lucas-clemente/quic-go v0.29.0 + github.com/lucas-clemente/quic-go v0.29.1 github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 github.com/mdlayher/netlink v1.6.0 // 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/ti-mo/netfilter v0.4.0 go.etcd.io/bbolt v1.3.6 - golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 - golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 - golang.org/x/net v0.0.0-20220906165146-f3363e06e74c - golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77 + golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be + golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9 + golang.org/x/net v0.0.0-20220927171203-f486391704dc + golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec gopkg.in/natefinch/lumberjack.v2 v2.0.0 gopkg.in/yaml.v3 v3.0.1 howett.net/plist v1.0.0 @@ -43,10 +43,12 @@ require ( github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect github.com/ameshkov/dnsstamps v1.0.3 // 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/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/golang/mock v1.6.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-19 v0.1.0 // indirect github.com/mdlayher/packet v1.0.0 // indirect @@ -57,7 +59,7 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect - golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9 // indirect + golang.org/x/mod v0.6.0-dev.0.20220922195421-2adab6b8c60e // indirect golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/tools v0.1.12 // indirect diff --git a/go.sum b/go.sum index a2a4197c..458cb14d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/AdguardTeam/dnsproxy v0.44.0 h1:JzIxEXF4OyJq4wZVHeZkM1af4VfuwcgrUzjgdBGljsE= -github.com/AdguardTeam/dnsproxy v0.44.0/go.mod h1:HsxYYW/bC8uo+4eX9pRW21hFD6gWZdrvcfBb1R6/AzU= +github.com/AdguardTeam/dnsproxy v0.45.2 h1:K9BXkQAfAKjrzbWbczpA2IA1owLe/edv0nG0e2+Esko= +github.com/AdguardTeam/dnsproxy v0.45.2/go.mod h1:h+0r4GDvHHY2Wu6r7oqva+O37h00KofYysfzy1TEXFE= 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.9 h1:F9oP2da0dQ9RQDM1lGR7LxUTfUWu8hEFOs4icwAkKM0= @@ -23,6 +23,8 @@ github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1O 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/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/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= @@ -88,8 +90,10 @@ 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.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lucas-clemente/quic-go v0.29.0 h1:Vw0mGTfmWqGzh4jx/kMymsIkFK6rErFVmg+t9RLrnZE= -github.com/lucas-clemente/quic-go v0.29.0/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE= +github.com/lucas-clemente/quic-go v0.29.1 h1:Z+WMJ++qMLhvpFkRZA+jl3BTxUjm415YBmWanXB8zP0= +github.com/lucas-clemente/quic-go v0.29.1/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/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= github.com/marten-seemann/qtls-go1-19 v0.1.0 h1:rLFKD/9mp/uq1SYGYuVZhm83wkmU95pK5df3GufyYYU= @@ -125,6 +129,7 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 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.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/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= @@ -168,16 +173,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-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-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM= -golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw= -golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A= +golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/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-20220929160808-de9c53c655b9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= 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.3.0/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.20220818022119-ed83ed61efb9 h1:VtCrPQXM5Wo9l7XN64SjBMczl48j8mkP+2e3OhYlz+0= -golang.org/x/mod v0.6.0-dev.0.20220818022119-ed83ed61efb9/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0-dev.0.20220922195421-2adab6b8c60e h1:WhB000cGjOfbJiedMGvJkMTclI18VD69w27k+sceql8= +golang.org/x/mod v0.6.0-dev.0.20220922195421-2adab6b8c60e/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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -189,6 +194,7 @@ 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-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-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-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -200,8 +206,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-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-20220906165146-f3363e06e74c h1:yKufUcDwucU5urd+50/Opbt4AYpqthk7wHpHok8f1lo= -golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ= +golang.org/x/net v0.0.0-20220927171203-f486391704dc/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-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -224,6 +230,7 @@ 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-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-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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -247,8 +254,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-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-20220906135438-9e1f76180b77 h1:C1tElbkWrsSkn3IRl1GCW/gETw1TywWIPgwZtXTZbYg= -golang.org/x/sys v0.0.0-20220906135438-9e1f76180b77/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= +golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/internal/aghhttp/aghhttp.go b/internal/aghhttp/aghhttp.go index 8f786749..f03ebf7d 100644 --- a/internal/aghhttp/aghhttp.go +++ b/internal/aghhttp/aghhttp.go @@ -33,7 +33,7 @@ func OK(w http.ResponseWriter) { // Error writes formatted message to w and also logs it. func Error(r *http.Request, w http.ResponseWriter, code int, format string, args ...any) { text := fmt.Sprintf(format, args...) - log.Error("%s %s: %s", r.Method, r.URL, text) + log.Error("%s %s %s: %s", r.Method, r.Host, r.URL, text) http.Error(w, text, code) } diff --git a/internal/aghhttp/header.go b/internal/aghhttp/header.go index 1509a7e0..a0b79425 100644 --- a/internal/aghhttp/header.go +++ b/internal/aghhttp/header.go @@ -8,8 +8,8 @@ package aghhttp const ( HdrNameAcceptEncoding = "Accept-Encoding" HdrNameAccessControlAllowOrigin = "Access-Control-Allow-Origin" - HdrNameContentType = "Content-Type" HdrNameContentEncoding = "Content-Encoding" + HdrNameContentType = "Content-Type" HdrNameServer = "Server" HdrNameTrailer = "Trailer" HdrNameUserAgent = "User-Agent" diff --git a/internal/dnsforward/access.go b/internal/dnsforward/access.go index 33c9a978..23ec1137 100644 --- a/internal/dnsforward/access.go +++ b/internal/dnsforward/access.go @@ -183,15 +183,7 @@ func (s *Server) accessListJSON() (j accessListJSON) { } func (s *Server) handleAccessList(w http.ResponseWriter, r *http.Request) { - 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 - } + _ = aghhttp.WriteJSONResponse(w, r, s.accessListJSON()) } // validateAccessSet checks the internal accessListJSON lists. To search for diff --git a/internal/dnsforward/config.go b/internal/dnsforward/config.go index 747767c4..f8e51ff0 100644 --- a/internal/dnsforward/config.go +++ b/internal/dnsforward/config.go @@ -201,6 +201,10 @@ type ServerConfig struct { // Register an HTTP handler 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 bool @@ -208,9 +212,12 @@ type ServerConfig struct { // locally-served networks should be resolved via private PTR resolvers. UsePrivateRDNS bool - // LocalPTRResolvers is a slice of addresses to be used as upstreams for - // resolving PTR queries for local addresses. - LocalPTRResolvers []string + // ServeHTTP3 defines if HTTP/3 is be allowed for incoming requests. + ServeHTTP3 bool + + // 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 @@ -226,6 +233,7 @@ func (s *Server) createProxyConfig() (conf proxy.Config, err error) { conf = proxy.Config{ UDPListenAddr: srvConf.UDPListenAddrs, TCPListenAddr: srvConf.TCPListenAddrs, + HTTP3: srvConf.ServeHTTP3, Ratelimit: int(srvConf.Ratelimit), RatelimitWhitelist: srvConf.RatelimitWhitelist, RefuseAny: srvConf.RefuseAny, @@ -324,6 +332,20 @@ 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 func (s *Server) prepareUpstreamSettings() error { // We're setting a customized set of RootCAs @@ -353,12 +375,14 @@ func (s *Server) prepareUpstreamSettings() error { upstreams = s.conf.UpstreamDNS } + httpVersions := UpstreamHTTPVersions(s.conf.UseHTTP3Upstreams) upstreams = stringutil.FilterOut(upstreams, IsCommentOrEmpty) upstreamConfig, err := proxy.ParseUpstreamsConfig( upstreams, &upstream.Options{ - Bootstrap: s.conf.BootstrapDNS, - Timeout: s.conf.UpstreamTimeout, + Bootstrap: s.conf.BootstrapDNS, + Timeout: s.conf.UpstreamTimeout, + HTTPVersions: httpVersions, }, ) if err != nil { @@ -371,8 +395,9 @@ func (s *Server) prepareUpstreamSettings() error { uc, err = proxy.ParseUpstreamsConfig( defaultDNS, &upstream.Options{ - Bootstrap: s.conf.BootstrapDNS, - Timeout: s.conf.UpstreamTimeout, + Bootstrap: s.conf.BootstrapDNS, + Timeout: s.conf.UpstreamTimeout, + HTTPVersions: httpVersions, }, ) if err != nil { diff --git a/internal/dnsforward/filter.go b/internal/dnsforward/filter.go index 7dc8514e..6f64d35d 100644 --- a/internal/dnsforward/filter.go +++ b/internal/dnsforward/filter.go @@ -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 -// section from pctx and returns a non-nil res if at least one of canonnical +// section from pctx and returns a non-nil res if at least one of canonical // names or IP addresses in it matches the filtering rules. func (s *Server) filterDNSResponse( pctx *proxy.DNSContext, diff --git a/internal/dnsforward/http.go b/internal/dnsforward/http.go index 5df74fe8..91e31b70 100644 --- a/internal/dnsforward/http.go +++ b/internal/dnsforward/http.go @@ -112,13 +112,7 @@ func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) { DefautLocalPTRUpstreams: defLocalPTRUps, } - 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 - } + _ = aghhttp.WriteJSONResponse(w, r, resp) } func (req *jsonDNSConfig) checkBlockingMode() (err error) { @@ -349,7 +343,10 @@ func newUpstreamConfig(upstreams []string) (conf *proxy.UpstreamConfig, err erro conf, err = proxy.ParseUpstreamsConfig( upstreams, - &upstream.Options{Bootstrap: []string{}, Timeout: DefaultTimeout}, + &upstream.Options{ + Bootstrap: []string{}, + Timeout: DefaultTimeout, + }, ) if err != nil { return nil, err @@ -412,7 +409,15 @@ func ValidateUpstreamsPrivate(upstreams []string, privateNets netutil.SubnetSet) return nil } -var protocols = []string{"udp://", "tcp://", "tls://", "https://", "sdns://", "quic://"} +var protocols = []string{ + "h3://", + "https://", + "quic://", + "sdns://", + "tcp://", + "tls://", + "udp://", +} // validateUpstream returns an error if u alongside with domains is not a valid // upstream configuration. useDefault is true if the upstream is @@ -659,24 +664,7 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) { result[host] = "OK" } - 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) - } + _ = aghhttp.WriteJSONResponse(w, r, result) } // handleDoH is the DNS-over-HTTPs handler. @@ -692,11 +680,13 @@ func (s *Server) handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) { func (s *Server) handleDoH(w http.ResponseWriter, r *http.Request) { if !s.conf.TLSAllowUnencryptedDoH && r.TLS == nil { aghhttp.Error(r, w, http.StatusNotFound, "Not Found") + return } if !s.IsRunning() { aghhttp.Error(r, w, http.StatusInternalServerError, "dns server is not running") + return } diff --git a/internal/dnsforward/http_test.go b/internal/dnsforward/http_test.go index 1ae39455..f591ac58 100644 --- a/internal/dnsforward/http_test.go +++ b/internal/dnsforward/http_test.go @@ -12,6 +12,7 @@ import ( "strings" "testing" + "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/AdGuardHome/internal/filtering" "github.com/AdguardTeam/golibs/netutil" @@ -116,7 +117,8 @@ func TestDNSForwardHTTP_handleGetConfig(t *testing.T) { s.conf = tc.conf() s.handleGetConfig(w, nil) - assert.Equal(t, "application/json", w.Header().Get("Content-Type")) + cType := w.Header().Get(aghhttp.HdrNameContentType) + assert.Equal(t, aghhttp.HdrValApplicationJSON, cType) assert.JSONEq(t, string(caseWant), w.Body.String()) }) } diff --git a/internal/home/clients.go b/internal/home/clients.go index 7396e8c6..631d27ba 100644 --- a/internal/home/clients.go +++ b/internal/home/clients.go @@ -456,8 +456,9 @@ func (clients *clientsContainer) findUpstreams( conf, err = proxy.ParseUpstreamsConfig( upstreams, &upstream.Options{ - Bootstrap: config.DNS.BootstrapDNS, - Timeout: config.DNS.UpstreamTimeout.Duration, + Bootstrap: config.DNS.BootstrapDNS, + Timeout: config.DNS.UpstreamTimeout.Duration, + HTTPVersions: dnsforward.UpstreamHTTPVersions(config.DNS.UseHTTP3Upstreams), }, ) if err != nil { diff --git a/internal/home/config.go b/internal/home/config.go index ed337c86..598baf81 100644 --- a/internal/home/config.go +++ b/internal/home/config.go @@ -166,6 +166,19 @@ type dnsConfig struct { // LocalPTRResolvers is the slice of addresses to be used as upstreams // for PTR queries for locally-served networks. 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 { diff --git a/internal/home/control.go b/internal/home/control.go index 881e957a..54d2efb1 100644 --- a/internal/home/control.go +++ b/internal/home/control.go @@ -1,13 +1,13 @@ package home import ( - "encoding/json" "fmt" "net" "net/http" "net/url" "runtime" "strings" + "time" "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" "github.com/AdguardTeam/AdGuardHome/internal/aghnet" @@ -97,16 +97,16 @@ func collectDNSAddresses() (addrs []string, err error) { // statusResponse is a response for /control/status endpoint. type statusResponse struct { + Version string `json:"version"` + Language string `json:"language"` DNSAddrs []string `json:"dns_addresses"` DNSPort int `json:"dns_port"` HTTPPort int `json:"http_port"` IsProtectionEnabled bool `json:"protection_enabled"` // TODO(e.burkov): Inspect if front-end doesn't requires this field as // openapi.yaml declares. - IsDHCPAvailable bool `json:"dhcp_available"` - IsRunning bool `json:"running"` - Version string `json:"version"` - Language string `json:"language"` + IsDHCPAvailable bool `json:"dhcp_available"` + IsRunning bool `json:"running"` } func handleStatus(w http.ResponseWriter, r *http.Request) { @@ -125,12 +125,12 @@ func handleStatus(w http.ResponseWriter, r *http.Request) { defer config.RUnlock() resp = statusResponse{ + Version: version.Version(), DNSAddrs: dnsAddrs, DNSPort: config.DNS.Port, HTTPPort: config.BindPort, - IsRunning: isRunning(), - Version: version.Version(), Language: config.Language, + IsRunning: isRunning(), } }() @@ -154,19 +154,12 @@ type profileJSON struct { } func handleGetProfile(w http.ResponseWriter, r *http.Request) { - pj := profileJSON{} u := Context.auth.getCurrentUser(r) - - pj.Name = u.Name - - data, err := json.Marshal(pj) - if err != nil { - aghhttp.Error(r, w, http.StatusInternalServerError, "json.Marshal: %s", err) - - return + resp := &profileJSON{ + Name: u.Name, } - _, _ = w.Write(data) + _ = aghhttp.WriteJSONResponse(w, r, resp) } // ------------------------ @@ -196,29 +189,26 @@ func httpRegister(method, url string, handler http.HandlerFunc) { Context.mux.Handle(url, postInstallHandler(optionalAuthHandler(gziphandler.GzipHandler(ensureHandler(method, handler))))) } -// ---------------------------------- -// helper functions for HTTP handlers -// ---------------------------------- -func ensure(method string, handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { +// ensure returns a wrapped handler that makes sure that the request has the +// correct method as well as additional method and header checks. +func ensure( + method string, + handler func(http.ResponseWriter, *http.Request), +) (wrapped func(http.ResponseWriter, *http.Request)) { return func(w http.ResponseWriter, r *http.Request) { - log.Debug("%s %v", r.Method, r.URL) + start := time.Now() + m, u := r.Method, r.URL + log.Debug("started %s %s %s", m, r.Host, u) + defer func() { log.Debug("finished %s %s %s in %s", m, r.Host, u, time.Since(start)) }() - if r.Method != method { - aghhttp.Error(r, w, http.StatusMethodNotAllowed, "only %s is allowed", method) + if m != method { + aghhttp.Error(r, w, http.StatusMethodNotAllowed, "only method %s is allowed", method) return } - if method == http.MethodPost || method == http.MethodPut || method == http.MethodDelete { - if r.Header.Get(aghhttp.HdrNameContentType) != aghhttp.HdrValApplicationJSON { - aghhttp.Error( - r, - w, - http.StatusUnsupportedMediaType, - "only %s is allowed", - aghhttp.HdrValApplicationJSON, - ) - + if modifiesData(m) { + if !ensureContentType(w, r) { return } @@ -230,6 +220,42 @@ func ensure(method string, handler func(http.ResponseWriter, *http.Request)) fun } } +// modifiesData returns true if m is an HTTP method that can modify data. +func modifiesData(m string) (ok bool) { + return m == http.MethodPost || m == http.MethodPut || m == http.MethodDelete +} + +// ensureContentType makes sure that the content type of a data-modifying +// request is set correctly. If it is not, ensureContentType writes a response +// to w, and ok is false. +func ensureContentType(w http.ResponseWriter, r *http.Request) (ok bool) { + const statusUnsup = http.StatusUnsupportedMediaType + + cType := r.Header.Get(aghhttp.HdrNameContentType) + if r.ContentLength == 0 { + if cType == "" { + return true + } + + // Assume that browsers always send a content type when submitting HTML + // forms and require no content type for requests with no body to make + // sure that the request comes from JavaScript. + aghhttp.Error(r, w, statusUnsup, "empty body with content-type %q not allowed", cType) + + return false + + } + + const wantCType = aghhttp.HdrValApplicationJSON + if cType == wantCType { + return true + } + + aghhttp.Error(r, w, statusUnsup, "only content-type %s is allowed", wantCType) + + return false +} + func ensurePOST(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { return ensure(http.MethodPost, handler) } diff --git a/internal/home/controlinstall.go b/internal/home/controlinstall.go index 78de64f7..7df8d320 100644 --- a/internal/home/controlinstall.go +++ b/internal/home/controlinstall.go @@ -21,6 +21,7 @@ import ( "github.com/AdguardTeam/AdGuardHome/internal/version" "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/log" + "github.com/lucas-clemente/quic-go/http3" ) // getAddrsResponse is the response for /install/get_addresses endpoint. @@ -328,6 +329,7 @@ func copyInstallSettings(dst, src *configuration) { // shutdownTimeout is the timeout for shutting HTTP server down operation. const shutdownTimeout = 5 * time.Second +// shutdownSrv shuts srv down and prints error messages to the log. func shutdownSrv(ctx context.Context, srv *http.Server) { defer log.OnPanic("") @@ -336,13 +338,38 @@ func shutdownSrv(ctx context.Context, srv *http.Server) { } err := srv.Shutdown(ctx) - if err != nil { - const msgFmt = "shutting down http server %q: %s" - if errors.Is(err, context.Canceled) { - log.Debug(msgFmt, srv.Addr, err) - } else { - log.Error(msgFmt, srv.Addr, err) - } + if err == nil { + return + } + + const msgFmt = "shutting down http server %q: %s" + if errors.Is(err, context.Canceled) { + 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) } } @@ -545,16 +572,11 @@ func (web *Web) handleInstallCheckConfigBeta(w http.ResponseWriter, r *http.Requ err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData) if err != nil { - aghhttp.Error( - r, - w, - http.StatusBadRequest, - "Failed to encode 'check_config' JSON data: %s", - err, - ) + aghhttp.Error(r, w, http.StatusBadRequest, "encoding check_config: %s", err) return } + body := nonBetaReqBody.String() r.Body = io.NopCloser(strings.NewReader(body)) r.ContentLength = int64(len(body)) @@ -622,13 +644,7 @@ func (web *Web) handleInstallConfigureBeta(w http.ResponseWriter, r *http.Reques err = json.NewEncoder(nonBetaReqBody).Encode(nonBetaReqData) if err != nil { - aghhttp.Error( - r, - w, - http.StatusBadRequest, - "Failed to encode 'check_config' JSON data: %s", - err, - ) + aghhttp.Error(r, w, http.StatusBadRequest, "encoding configure: %s", err) return } diff --git a/internal/home/dns.go b/internal/home/dns.go index 06c38bcc..da462876 100644 --- a/internal/home/dns.go +++ b/internal/home/dns.go @@ -246,11 +246,14 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) { newConf.FilterHandler = applyAdditionalFiltering newConf.GetCustomUpstreamByClient = Context.clients.findUpstreams - newConf.ResolveClients = config.Clients.Sources.RDNS - newConf.UsePrivateRDNS = dnsConf.UsePrivateRDNS newConf.LocalPTRResolvers = dnsConf.LocalPTRResolvers newConf.UpstreamTimeout = dnsConf.UpstreamTimeout.Duration + newConf.ResolveClients = config.Clients.Sources.RDNS + newConf.UsePrivateRDNS = dnsConf.UsePrivateRDNS + newConf.ServeHTTP3 = dnsConf.ServeHTTP3 + newConf.UseHTTP3Upstreams = dnsConf.UseHTTP3Upstreams + return newConf, nil } @@ -358,7 +361,13 @@ func applyAdditionalFiltering(clientIP net.IP, clientID string, setts *filtering log.Debug("%s: using settings for client %q (%s; %q)", pref, c.Name, clientIP, clientID) if c.UseOwnBlockedServices { - Context.filters.ApplyBlockedServices(setts, c.BlockedServices) + // TODO(e.burkov): Get rid of this crutch. + 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 diff --git a/internal/home/home.go b/internal/home/home.go index f917b6a5..42c44249 100644 --- a/internal/home/home.go +++ b/internal/home/home.go @@ -381,9 +381,11 @@ func initWeb(args options, clientBuildFS fs.FS) (web *Web, err error) { clientFS: clientFS, clientBetaFS: clientBetaFS, + + serveHTTP3: config.DNS.ServeHTTP3, } - web = CreateWeb(&webConf) + web = newWeb(&webConf) if web == nil { return nil, fmt.Errorf("initializing web: %w", err) } diff --git a/internal/home/tls.go b/internal/home/tls.go index 359a7a9d..a5089bd8 100644 --- a/internal/home/tls.go +++ b/internal/home/tls.go @@ -266,7 +266,7 @@ func (t *TLSMod) handleTLSValidate(w http.ResponseWriter, r *http.Request) { } } - if !WebCheckPortAvailable(setts.PortHTTPS) { + if !webCheckPortAvailable(setts.PortHTTPS) { aghhttp.Error( r, w, @@ -356,7 +356,7 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) { } // TODO(e.burkov): Investigate and perhaps check other ports. - if !WebCheckPortAvailable(data.PortHTTPS) { + if !webCheckPortAvailable(data.PortHTTPS) { aghhttp.Error( r, w, diff --git a/internal/home/web.go b/internal/home/web.go index 5a26de59..3e248d80 100644 --- a/internal/home/web.go +++ b/internal/home/web.go @@ -16,6 +16,8 @@ import ( "github.com/AdguardTeam/golibs/log" "github.com/AdguardTeam/golibs/netutil" "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/h2c" ) @@ -53,40 +55,56 @@ type webConfig struct { WriteTimeout time.Duration firstRun bool + + serveHTTP3 bool } -// HTTPSServer - HTTPS Server -type HTTPSServer struct { - server *http.Server - cond *sync.Cond - condLock sync.Mutex - shutdown bool // if TRUE, don't restart the server - enabled bool - cert tls.Certificate +// httpsServer contains the data for the HTTPS server. +type httpsServer struct { + // server is the pre-HTTP/3 HTTPS server. + server *http.Server + // server3 is the HTTP/3 HTTPS server. If it is not nil, + // [httpsServer.server] must also be non-nil. + server3 *http3.Server + + // 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 - module object +// Web is the web UI and API server. type Web struct { - conf *webConfig - forceHTTPS bool - httpServer *http.Server // HTTP module - httpsServer HTTPSServer // HTTPS module + conf *webConfig - // handlerBeta is the handler for new client. - handlerBeta http.Handler - // installerBeta is the pre-install handler for new client. - installerBeta http.Handler + // TODO(a.garipov): Refactor all these servers. + httpServer *http.Server // httpServerBeta is a server for new client. httpServerBeta *http.Server + + // handlerBeta is the handler for new client. + handlerBeta http.Handler + + // installerBeta is the pre-install handler for new client. + installerBeta http.Handler + + // httpsServer is the server that handles HTTPS traffic. If it is not nil, + // [Web.http3Server] must also not be nil. + httpsServer httpsServer + + forceHTTPS bool } -// CreateWeb - create module -func CreateWeb(conf *webConfig) *Web { - log.Info("Initialize web module") +// newWeb creates a new instance of the web UI and API server. +func newWeb(conf *webConfig) (w *Web) { + log.Info("web: initializing") - w := Web{} - w.conf = conf + w = &Web{ + conf: conf, + } clientFS := http.FileServer(http.FS(conf.clientFS)) betaClientFS := http.FileServer(http.FS(conf.clientBetaFS)) @@ -108,12 +126,15 @@ func CreateWeb(conf *webConfig) *Web { } w.httpsServer.cond = sync.NewCond(&w.httpsServer.condLock) - return &w + + return w } -// WebCheckPortAvailable - check if port is available -// BUT: if we are already using this port, no need -func WebCheckPortAvailable(port int) bool { +// webCheckPortAvailable checks if port, which is considered an HTTPS port, is +// available, unless the HTTPS server isn't active. +// +// TODO(a.garipov): Adapt for HTTP/3. +func webCheckPortAvailable(port int) (ok bool) { return Context.web.httpsServer.server != nil || aghnet.CheckPort("tcp", config.BindHost, port) == nil } @@ -121,7 +142,7 @@ func WebCheckPortAvailable(port int) bool { // TLSConfigChanged updates the TLS configuration and restarts the HTTPS server // if necessary. 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.forceHTTPS = (tlsConf.ForceHTTPS && tlsConf.Enabled && tlsConf.PortHTTPS != 0) @@ -143,6 +164,8 @@ func (web *Web) TLSConfigChanged(ctx context.Context, tlsConf tlsConfigSettings) var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, shutdownTimeout) shutdownSrv(ctx, web.httpsServer.server) + shutdownSrv3(web.httpsServer.server3) + cancel() } @@ -160,7 +183,7 @@ func (web *Web) Start() { go web.tlsServerLoop() // this loop is used as an ability to change listening host and/or port - for !web.httpsServer.shutdown { + for !web.httpsServer.inShutdown { printHTTPAddresses(aghhttp.SchemeHTTP) errs := make(chan error, 2) @@ -231,7 +254,7 @@ func (web *Web) Close(ctx context.Context) { log.Info("stopping http server...") web.httpsServer.cond.L.Lock() - web.httpsServer.shutdown = true + web.httpsServer.inShutdown = true web.httpsServer.cond.L.Unlock() var cancel context.CancelFunc @@ -239,6 +262,7 @@ func (web *Web) Close(ctx context.Context) { defer cancel() shutdownSrv(ctx, web.httpsServer.server) + shutdownSrv3(web.httpsServer.server3) shutdownSrv(ctx, web.httpServer) shutdownSrv(ctx, web.httpServerBeta) @@ -248,7 +272,7 @@ func (web *Web) Close(ctx context.Context) { func (web *Web) tlsServerLoop() { for { web.httpsServer.cond.L.Lock() - if web.httpsServer.shutdown { + if web.httpsServer.inShutdown { web.httpsServer.cond.L.Unlock() break } @@ -256,7 +280,7 @@ func (web *Web) tlsServerLoop() { // this mechanism doesn't let us through until all conditions are met for !web.httpsServer.enabled { // sleep until necessary data is supplied web.httpsServer.cond.Wait() - if web.httpsServer.shutdown { + if web.httpsServer.inShutdown { web.httpsServer.cond.L.Unlock() return } @@ -264,11 +288,10 @@ func (web *Web) tlsServerLoop() { web.httpsServer.cond.L.Unlock() - // prepare HTTPS server - address := netutil.JoinHostPort(web.conf.BindHost.String(), web.conf.PortHTTPS) + addr := netutil.JoinHostPort(web.conf.BindHost.String(), web.conf.PortHTTPS) web.httpsServer.server = &http.Server{ ErrorLog: log.StdLog("web: https", log.DEBUG), - Addr: address, + Addr: addr, TLSConfig: &tls.Config{ Certificates: []tls.Certificate{web.httpsServer.cert}, RootCAs: Context.tlsRoots, @@ -282,10 +305,40 @@ func (web *Web) tlsServerLoop() { } printHTTPAddresses(aghhttp.SchemeHTTPS) + + if web.conf.serveHTTP3 { + go web.mustStartHTTP3(addr) + } + + log.Debug("web: starting https server") err := web.httpsServer.server.ListenAndServeTLS("", "") - if err != http.ErrServerClosed { + if !errors.Is(err, http.ErrServerClosed) { cleanupAlways() - log.Fatal(err) + log.Fatalf("web: https: %s", 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) + } +} diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md index d9946115..d0a450ed 100644 --- a/openapi/CHANGELOG.md +++ b/openapi/CHANGELOG.md @@ -6,6 +6,28 @@ +## v0.107.15: `POST` Requests Without Bodies + +As an additional CSRF protection measure, AdGuard Home now ensures that requests +that change its state but have no body do not have a `Content-Type` header set +on them. + +This concerns the following APIs: + +* `POST /control/dhcp/reset_leases`; +* `POST /control/dhcp/reset`; +* `POST /control/parental/disable`; +* `POST /control/parental/enable`; +* `POST /control/querylog_clear`; +* `POST /control/safebrowsing/disable`; +* `POST /control/safebrowsing/enable`; +* `POST /control/safesearch/disable`; +* `POST /control/safesearch/enable`; +* `POST /control/stats_reset`; +* `POST /control/update`. + + + ## v0.107.14: BREAKING API CHANGES A Cross-Site Request Forgery (CSRF) vulnerability has been discovered. We have @@ -13,6 +35,9 @@ implemented several measures to prevent such vulnerabilities in the future, but some of these measures break backwards compatibility for the sake of better protection. +All JSON APIs that expect a body now check if the request actually has +`Content-Type` set to `application/json`. + All new formats for the request and response bodies are documented in `openapi.yaml`. diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index c6451fa7..195b0a5e 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -601,11 +601,10 @@ 'summary': 'Set user-defined filter rules' 'requestBody': 'content': - 'text/plain': + 'application/json': 'schema': - 'type': 'string' - 'example': '@@||yandex.ru^|' - 'description': 'All filtering rules, one line per rule' + '$ref': '#/components/schemas/SetRulesRequest' + 'description': 'Custom filtering rules.' 'responses': '200': 'description': 'OK.' @@ -1538,6 +1537,19 @@ 'properties': 'updated': 'type': 'integer' + 'SetRulesRequest': + 'description': 'Custom filtering rules setting request.' + 'example': + 'rules': + - '||example.com^' + - '# comment' + - '@@||www.example.com^' + 'properties': + 'rules': + 'items': + 'type': 'string' + 'type': 'array' + 'type': 'object' 'GetVersionRequest': 'type': 'object' 'description': '/version.json request data'