Andrey Meshkov da0cb6fd0e Sync v2.9.0
2024-10-14 17:44:24 +03:00

142 lines
3.8 KiB
Go

package websvc
import (
"io"
"net/http"
"net/http/httptest"
"os"
"github.com/AdguardTeam/AdGuardDNS/internal/agdhttp"
"github.com/AdguardTeam/AdGuardDNS/internal/metrics"
"github.com/AdguardTeam/AdGuardDNS/internal/optlog"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/httphdr"
"github.com/AdguardTeam/golibs/log"
"golang.org/x/sys/unix"
)
// HTTP Handlers
// type check
var _ http.Handler = (*Service)(nil)
// ServeHTTP implements the http.Handler interface for *Service.
func (svc *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) {
respHdr := w.Header()
respHdr.Set(httphdr.Server, agdhttp.UserAgent())
m, p, rAddr := r.Method, r.URL.Path, r.RemoteAddr
optlog.Debug3("websvc: starting req %s %s from %s", m, p, rAddr)
defer optlog.Debug3("websvc: finished req %s %s from %s", m, p, rAddr)
if svc == nil {
http.NotFound(w, r)
return
}
// TODO(a.garipov): Refactor the 404 and 500 handling and use
// [httputil.CodeRecorderResponseWriter] instead.
rec := httptest.NewRecorder()
svc.serveHTTP(rec, r)
action, body := svc.processRec(respHdr, rec)
w.WriteHeader(rec.Code)
_, err := w.Write(body)
if err != nil {
logErrorByType(err, "websvc: handler: %s: %s", action, err)
}
}
// serveHTTP processes the HTTP request.
func (svc *Service) serveHTTP(rec *httptest.ResponseRecorder, r *http.Request) {
switch r.URL.Path {
case "/dnscheck/test":
svc.dnsCheck.ServeHTTP(rec, r)
metrics.WebSvcDNSCheckTestRequestsTotal.Inc()
case "/robots.txt":
serveRobotsDisallow(rec.Header(), rec, "handler")
case "/":
if svc.rootRedirectURL == "" {
http.NotFound(rec, r)
} else {
http.Redirect(rec, r, svc.rootRedirectURL, http.StatusFound)
metrics.WebSvcRootRedirectRequestsTotal.Inc()
}
default:
svc.staticContent.ServeHTTP(rec, r)
if rec.Code != http.StatusNotFound {
metrics.WebSvcStaticContentRequestsTotal.Inc()
}
// Assume that most unknown content types are actually plain-text files.
if h := rec.Header(); h.Get(httphdr.ContentType) == agdhttp.HdrValApplicationOctetStream {
h.Set(httphdr.ContentType, agdhttp.HdrValTextPlain)
}
}
}
// processRec processes the response code in rec and returns the appropriate
// body and a description of the action for logging. It also sets the necessary
// headers in respHdr.
func (svc *Service) processRec(
respHdr http.Header,
rec *httptest.ResponseRecorder,
) (action string, body []byte) {
switch rec.Code {
case http.StatusNotFound:
action = "writing 404"
if len(svc.error404) != 0 {
body = svc.error404
respHdr.Set(httphdr.ContentType, agdhttp.HdrValTextHTML)
}
metrics.WebSvcError404RequestsTotal.Inc()
case http.StatusInternalServerError:
action = "writing 500"
if len(svc.error500) != 0 {
body = svc.error500
respHdr.Set(httphdr.ContentType, agdhttp.HdrValTextHTML)
}
metrics.WebSvcError500RequestsTotal.Inc()
default:
action = "writing response"
for k, v := range rec.Header() {
respHdr[k] = v
}
}
if body == nil {
body = rec.Body.Bytes()
}
return action, body
}
// serveRobotsDisallow writes predefined disallow-all response.
func serveRobotsDisallow(hdr http.Header, w http.ResponseWriter, name string) {
hdr.Set(httphdr.ContentType, agdhttp.HdrValTextPlain)
_, err := io.WriteString(w, agdhttp.RobotsDisallowAll)
if err != nil {
logErrorByType(err, "websvc: %s: writing response: %s", name, err)
}
metrics.WebSvcRobotsTxtRequestsTotal.Inc()
}
// logErrorByType writes err to the error log, unless err is a network error or
// a timeout error, in which case it is written to the debug log.
func logErrorByType(err error, format string, args ...any) {
// TODO(d.kolyshev): Consider adding more error types.
if errors.Is(err, os.ErrDeadlineExceeded) || errors.Is(err, unix.EPIPE) ||
errors.Is(err, unix.ECONNRESET) {
log.Debug(format, args...)
} else {
log.Error(format, args...)
}
}