2020-05-23 22:54:51 +02:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
2023-11-19 21:47:50 +01:00
|
|
|
"context"
|
2020-05-23 22:54:51 +02:00
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
|
|
|
"html/template"
|
2022-08-19 22:04:35 +02:00
|
|
|
"io"
|
2020-05-23 22:54:51 +02:00
|
|
|
"net"
|
|
|
|
"net/http"
|
2022-05-10 09:09:50 +02:00
|
|
|
"time"
|
2020-05-23 22:54:51 +02:00
|
|
|
|
2023-09-09 19:30:55 +02:00
|
|
|
"github.com/0xERR0R/blocky/resolver"
|
|
|
|
|
2021-08-25 22:06:34 +02:00
|
|
|
"github.com/0xERR0R/blocky/api"
|
|
|
|
"github.com/0xERR0R/blocky/config"
|
2023-09-09 19:30:55 +02:00
|
|
|
"github.com/0xERR0R/blocky/docs"
|
2021-08-25 22:06:34 +02:00
|
|
|
"github.com/0xERR0R/blocky/log"
|
2021-08-29 21:51:24 +02:00
|
|
|
"github.com/0xERR0R/blocky/model"
|
2021-08-25 22:06:34 +02:00
|
|
|
"github.com/0xERR0R/blocky/util"
|
|
|
|
"github.com/0xERR0R/blocky/web"
|
|
|
|
|
2021-12-24 22:56:41 +01:00
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"github.com/go-chi/chi/v5/middleware"
|
2020-05-23 22:54:51 +02:00
|
|
|
"github.com/go-chi/cors"
|
|
|
|
"github.com/miekg/dns"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2022-06-29 22:36:54 +02:00
|
|
|
dohMessageLimit = 512
|
|
|
|
contentTypeHeader = "content-type"
|
|
|
|
dnsContentType = "application/dns-message"
|
|
|
|
htmlContentType = "text/html; charset=UTF-8"
|
2023-09-09 19:30:55 +02:00
|
|
|
yamlContentType = "text/yaml"
|
2022-06-29 22:36:54 +02:00
|
|
|
corsMaxAge = 5 * time.Minute
|
2020-05-23 22:54:51 +02:00
|
|
|
)
|
|
|
|
|
2022-06-03 22:43:31 +02:00
|
|
|
func secureHeader(next http.Handler) http.Handler {
|
2022-06-02 16:42:23 +02:00
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.Header().Set("strict-transport-security", "max-age=63072000")
|
|
|
|
w.Header().Set("x-frame-options", "DENY")
|
|
|
|
w.Header().Set("x-content-type-options", "nosniff")
|
|
|
|
w.Header().Set("x-xss-protection", "1; mode=block")
|
|
|
|
next.ServeHTTP(w, r)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-09-09 19:30:55 +02:00
|
|
|
func (s *Server) createOpenAPIInterfaceImpl() (impl api.StrictServerInterface, err error) {
|
|
|
|
bControl, err := resolver.GetFromChainWithType[api.BlockingControl](s.queryResolver)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("no blocking API implementation found %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
refresher, err := resolver.GetFromChainWithType[api.ListRefresher](s.queryResolver)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("no refresh API implementation found %w", err)
|
|
|
|
}
|
2020-05-23 22:54:51 +02:00
|
|
|
|
2023-09-30 22:13:01 +02:00
|
|
|
cacheControl, err := resolver.GetFromChainWithType[api.CacheControl](s.queryResolver)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("no cache API implementation found %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return api.NewOpenAPIInterfaceImpl(bControl, s, refresher, cacheControl), nil
|
2023-09-09 19:30:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) registerAPIEndpoints(router *chi.Mux) error {
|
|
|
|
const pathDohQuery = "/dns-query"
|
|
|
|
|
|
|
|
openAPIImpl, err := s.createOpenAPIInterfaceImpl()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
api.RegisterOpenAPIEndpoints(router, openAPIImpl)
|
|
|
|
|
|
|
|
router.Get(pathDohQuery, s.dohGetRequestHandler)
|
|
|
|
router.Get(pathDohQuery+"/", s.dohGetRequestHandler)
|
|
|
|
router.Get(pathDohQuery+"/{clientID}", s.dohGetRequestHandler)
|
|
|
|
router.Post(pathDohQuery, s.dohPostRequestHandler)
|
|
|
|
router.Post(pathDohQuery+"/", s.dohPostRequestHandler)
|
|
|
|
router.Post(pathDohQuery+"/{clientID}", s.dohPostRequestHandler)
|
|
|
|
|
|
|
|
return nil
|
2020-05-23 22:54:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) dohGetRequestHandler(rw http.ResponseWriter, req *http.Request) {
|
|
|
|
dnsParam, ok := req.URL.Query()["dns"]
|
|
|
|
if !ok || len(dnsParam[0]) < 1 {
|
|
|
|
http.Error(rw, "dns param is missing", http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-04-13 22:51:53 +02:00
|
|
|
rawMsg, err := base64.RawURLEncoding.DecodeString(dnsParam[0])
|
2020-05-23 22:54:51 +02:00
|
|
|
if err != nil {
|
|
|
|
http.Error(rw, "wrong message format", http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(rawMsg) > dohMessageLimit {
|
|
|
|
http.Error(rw, "URI Too Long", http.StatusRequestURITooLong)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
s.processDohMessage(rawMsg, rw, req)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) dohPostRequestHandler(rw http.ResponseWriter, req *http.Request) {
|
|
|
|
contentType := req.Header.Get("Content-type")
|
|
|
|
if contentType != dnsContentType {
|
|
|
|
http.Error(rw, "unsupported content type", http.StatusUnsupportedMediaType)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-08-19 22:04:35 +02:00
|
|
|
rawMsg, err := io.ReadAll(req.Body)
|
2020-05-23 22:54:51 +02:00
|
|
|
if err != nil {
|
|
|
|
http.Error(rw, err.Error(), http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(rawMsg) > dohMessageLimit {
|
|
|
|
http.Error(rw, "Payload Too Large", http.StatusRequestEntityTooLarge)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
s.processDohMessage(rawMsg, rw, req)
|
|
|
|
}
|
|
|
|
|
2024-01-27 22:19:07 +01:00
|
|
|
func (s *Server) processDohMessage(rawMsg []byte, rw http.ResponseWriter, httpReq *http.Request) {
|
2020-05-23 22:54:51 +02:00
|
|
|
msg := new(dns.Msg)
|
2022-12-26 22:11:45 +01:00
|
|
|
if err := msg.Unpack(rawMsg); err != nil {
|
2020-05-23 22:54:51 +02:00
|
|
|
logger().Error("can't deserialize message: ", err)
|
|
|
|
http.Error(rw, err.Error(), http.StatusBadRequest)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-01-27 22:19:07 +01:00
|
|
|
ctx, dnsReq := newRequestFromHTTP(httpReq.Context(), httpReq, msg)
|
2024-01-28 01:36:32 +01:00
|
|
|
|
2024-01-27 22:19:07 +01:00
|
|
|
s.handleReq(ctx, dnsReq, httpMsgWriter{rw})
|
|
|
|
}
|
2021-10-13 22:47:14 +02:00
|
|
|
|
2024-01-27 22:19:07 +01:00
|
|
|
type httpMsgWriter struct {
|
|
|
|
rw http.ResponseWriter
|
|
|
|
}
|
2020-05-23 22:54:51 +02:00
|
|
|
|
2024-01-27 22:19:07 +01:00
|
|
|
func (r httpMsgWriter) WriteMsg(msg *dns.Msg) error {
|
|
|
|
b, err := msg.Pack()
|
2020-05-23 22:54:51 +02:00
|
|
|
if err != nil {
|
2024-01-27 22:19:07 +01:00
|
|
|
return err
|
2020-05-23 22:54:51 +02:00
|
|
|
}
|
|
|
|
|
2024-01-27 22:19:07 +01:00
|
|
|
r.rw.Header().Set("content-type", dnsContentType)
|
2022-05-10 09:09:50 +02:00
|
|
|
|
2024-01-27 22:19:07 +01:00
|
|
|
// https://www.rfc-editor.org/rfc/rfc8484#section-4.2.1
|
|
|
|
r.rw.WriteHeader(http.StatusOK)
|
2020-05-23 22:54:51 +02:00
|
|
|
|
2024-01-27 22:19:07 +01:00
|
|
|
_, err = r.rw.Write(b)
|
|
|
|
|
|
|
|
return err
|
2020-05-23 22:54:51 +02:00
|
|
|
}
|
|
|
|
|
2023-12-20 01:50:17 +01:00
|
|
|
func (s *Server) Query(
|
|
|
|
ctx context.Context, serverHost string, clientIP net.IP, question string, qType dns.Type,
|
|
|
|
) (*model.Response, error) {
|
2024-01-27 22:19:07 +01:00
|
|
|
msg := util.NewMsgWithQuestion(question, qType)
|
|
|
|
clientID := extractClientIDFromHost(serverHost)
|
|
|
|
|
|
|
|
ctx, req := newRequest(ctx, clientIP, clientID, model.RequestProtocolTCP, msg)
|
2022-05-10 09:09:50 +02:00
|
|
|
|
2024-01-27 22:19:07 +01:00
|
|
|
return s.resolve(ctx, req)
|
2020-05-23 22:54:51 +02:00
|
|
|
}
|
|
|
|
|
2022-06-02 16:42:23 +02:00
|
|
|
func createHTTPSRouter(cfg *config.Config) *chi.Mux {
|
|
|
|
router := chi.NewRouter()
|
|
|
|
|
2022-06-03 22:43:31 +02:00
|
|
|
configureSecureHeaderHandler(router)
|
2022-06-02 16:42:23 +02:00
|
|
|
|
2023-09-09 19:30:55 +02:00
|
|
|
registerHandlers(cfg, router)
|
2022-06-02 16:42:23 +02:00
|
|
|
|
|
|
|
return router
|
|
|
|
}
|
|
|
|
|
2023-09-09 19:30:55 +02:00
|
|
|
func createHTTPRouter(cfg *config.Config) *chi.Mux {
|
2020-05-23 22:54:51 +02:00
|
|
|
router := chi.NewRouter()
|
|
|
|
|
2023-09-09 19:30:55 +02:00
|
|
|
registerHandlers(cfg, router)
|
|
|
|
|
|
|
|
return router
|
|
|
|
}
|
|
|
|
|
|
|
|
func registerHandlers(cfg *config.Config, router *chi.Mux) {
|
2020-05-23 22:54:51 +02:00
|
|
|
configureCorsHandler(router)
|
|
|
|
|
|
|
|
configureDebugHandler(router)
|
|
|
|
|
2023-09-09 19:30:55 +02:00
|
|
|
configureDocsHandler(router)
|
|
|
|
|
|
|
|
configureStaticAssetsHandler(router)
|
|
|
|
|
2020-05-23 22:54:51 +02:00
|
|
|
configureRootHandler(cfg, router)
|
2023-09-09 19:30:55 +02:00
|
|
|
}
|
2020-05-23 22:54:51 +02:00
|
|
|
|
2023-09-09 19:30:55 +02:00
|
|
|
func configureDocsHandler(router *chi.Mux) {
|
|
|
|
router.Get("/docs/openapi.yaml", func(writer http.ResponseWriter, request *http.Request) {
|
|
|
|
writer.Header().Set(contentTypeHeader, yamlContentType)
|
|
|
|
_, err := writer.Write([]byte(docs.OpenAPI))
|
|
|
|
logAndResponseWithError(err, "can't write OpenAPI definition file: ", writer)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func configureStaticAssetsHandler(router *chi.Mux) {
|
|
|
|
assets, err := web.Assets()
|
|
|
|
util.FatalOnError("unable to load static asset files", err)
|
|
|
|
|
|
|
|
fs := http.FileServer(http.FS(assets))
|
|
|
|
router.Handle("/static/*", http.StripPrefix("/static/", fs))
|
2020-05-23 22:54:51 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func configureRootHandler(cfg *config.Config, router *chi.Mux) {
|
|
|
|
router.Get("/", func(writer http.ResponseWriter, request *http.Request) {
|
2022-06-29 22:36:54 +02:00
|
|
|
writer.Header().Set(contentTypeHeader, htmlContentType)
|
2024-04-12 22:44:50 +02:00
|
|
|
|
2020-05-23 22:54:51 +02:00
|
|
|
t := template.New("index")
|
2024-04-12 22:44:50 +02:00
|
|
|
|
2020-05-23 22:54:51 +02:00
|
|
|
_, _ = t.Parse(web.IndexTmpl)
|
|
|
|
|
|
|
|
type HandlerLink struct {
|
|
|
|
URL string
|
|
|
|
Title string
|
|
|
|
}
|
2021-05-05 22:38:22 +02:00
|
|
|
|
|
|
|
type PageData struct {
|
|
|
|
Links []HandlerLink
|
|
|
|
Version string
|
|
|
|
BuildTime string
|
|
|
|
}
|
2024-04-12 22:44:50 +02:00
|
|
|
|
2021-05-05 22:38:22 +02:00
|
|
|
pd := PageData{
|
|
|
|
Links: nil,
|
|
|
|
Version: util.Version,
|
|
|
|
BuildTime: util.BuildTime,
|
|
|
|
}
|
2024-04-12 22:44:50 +02:00
|
|
|
|
2021-05-05 22:38:22 +02:00
|
|
|
pd.Links = []HandlerLink{
|
2020-05-23 22:54:51 +02:00
|
|
|
{
|
2023-09-09 19:30:55 +02:00
|
|
|
URL: "/docs/openapi.yaml",
|
|
|
|
Title: "Rest API Documentation (OpenAPI)",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
URL: "/static/rapidoc.html",
|
|
|
|
Title: "Interactive Rest API Documentation (RapiDoc)",
|
2020-05-23 22:54:51 +02:00
|
|
|
},
|
|
|
|
{
|
|
|
|
URL: "/debug/",
|
|
|
|
Title: "Go Profiler",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if cfg.Prometheus.Enable {
|
2021-05-05 22:38:22 +02:00
|
|
|
pd.Links = append(pd.Links, HandlerLink{
|
2020-05-23 22:54:51 +02:00
|
|
|
URL: cfg.Prometheus.Path,
|
|
|
|
Title: "Prometheus endpoint",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-05-05 22:38:22 +02:00
|
|
|
err := t.Execute(writer, pd)
|
2021-10-13 22:47:14 +02:00
|
|
|
logAndResponseWithError(err, "can't write index template: ", writer)
|
2020-05-23 22:54:51 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-13 22:47:14 +02:00
|
|
|
func logAndResponseWithError(err error, message string, writer http.ResponseWriter) {
|
|
|
|
if err != nil {
|
2021-12-20 22:37:32 +01:00
|
|
|
log.Log().Error(message, log.EscapeInput(err.Error()))
|
2021-10-13 22:47:14 +02:00
|
|
|
http.Error(writer, err.Error(), http.StatusInternalServerError)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-03 22:43:31 +02:00
|
|
|
func configureSecureHeaderHandler(router *chi.Mux) {
|
|
|
|
router.Use(secureHeader)
|
2022-06-02 16:42:23 +02:00
|
|
|
}
|
|
|
|
|
2020-05-23 22:54:51 +02:00
|
|
|
func configureDebugHandler(router *chi.Mux) {
|
|
|
|
router.Mount("/debug", middleware.Profiler())
|
|
|
|
}
|
|
|
|
|
|
|
|
func configureCorsHandler(router *chi.Mux) {
|
|
|
|
crs := cors.New(cors.Options{
|
|
|
|
AllowedOrigins: []string{"*"},
|
|
|
|
AllowedMethods: []string{"GET", "POST"},
|
|
|
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
|
|
|
ExposedHeaders: []string{"Link"},
|
|
|
|
AllowCredentials: true,
|
2022-05-10 09:09:50 +02:00
|
|
|
MaxAge: int(corsMaxAge.Seconds()),
|
2020-05-23 22:54:51 +02:00
|
|
|
})
|
|
|
|
router.Use(crs.Handler)
|
|
|
|
}
|