Skip to content

Commit

Permalink
API bearer impl. (#272)
Browse files Browse the repository at this point in the history
  • Loading branch information
semihalev authored Feb 28, 2024
1 parent c5b8793 commit eaaed2a
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 11 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ example.com. 0 CH HINFO "Host" "IPv6:[2001:500:8f::53]:53 rtt:147ms health:[GOO
example.com. 0 CH HINFO "Host" "IPv6:[2001:500:8d::53]:53 rtt:148ms health:[GOOD]"
```
## Configuration (v1.3.6)
## Configuration (v1.3.7)
| Key | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------- |
Expand All @@ -148,6 +148,7 @@ example.com. 0 CH HINFO "Host" "IPv6:[2001:500:8d::53]:53 rtt:148ms health:[GOO
| **fallbackservers** | Failover resolver IPv4 or IPv6 addresses with port (leave blank to disable) Example: "8.8.8.8:53" |
| **forwarderservers** | Forwarder resolver IPv4 or IPv6 addresses with port (leave blank to disable) Example: "8.8.8.8:53" |
| **api** | HTTP API server binding address (leave blank to disable) |
| **bearertoken** | API bearer token for authorization. If the token set, Authorization header should be send on API requests |
| **blocklists** | Remote blocklist address list (downloaded to the blocklist folder) |
| **blocklistdir** | \[DEPRECATED] Directory creation is automated in the working directory |
| **loglevel** | Log verbosity level (crit, error, warn, info, debug) |
Expand Down
6 changes: 4 additions & 2 deletions api/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# SDNS HTTP API
# HTTP API

You can manage all blocks with basic HTTP API functions.

## Authentication

WARNING: Currently, there is no authentication mechanism for API functions.
API bearer token can be set on sdns config. If the token set, Authorization header should be send on API requests.
### Example Header
`Authorization: Bearer my_very_long_token`

## Actions

Expand Down
66 changes: 60 additions & 6 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ import (

// API type
type API struct {
addr string
router *Router
blocklist *blocklist.BlockList
addr string
bearerToken string
router *Router
blocklist *blocklist.BlockList
}

var debugpprof bool
Expand All @@ -41,19 +42,53 @@ func New(cfg *config.Config) *API {
}

a := &API{
addr: cfg.API,
blocklist: bl,
router: NewRouter(),
addr: cfg.API,
blocklist: bl,
router: NewRouter(),
bearerToken: cfg.BearerToken,
}

return a
}

func (a *API) checkToken(ctx *Context) bool {
if a.bearerToken == "" {
return true
}

authHeader := ctx.Request.Header.Get("Authorization")
if authHeader == "" {
ctx.JSON(http.StatusUnauthorized, Json{"error": "unauthorized"})
return false
}

tokenSplit := strings.Split(authHeader, " ")
if len(tokenSplit) != 2 {
ctx.JSON(http.StatusUnauthorized, Json{"error": "unauthorized"})
return false
}

if tokenSplit[0] == "Bearer" && a.bearerToken == tokenSplit[1] {
return true
}

ctx.JSON(http.StatusUnauthorized, Json{"error": "unauthorized"})
return false
}

func (a *API) existsBlock(ctx *Context) {
if !a.checkToken(ctx) {
return
}

ctx.JSON(http.StatusOK, Json{"exists": a.blocklist.Exists(ctx.Param("key"))})
}

func (a *API) getBlock(ctx *Context) {
if !a.checkToken(ctx) {
return
}

if ok, _ := a.blocklist.Get(ctx.Param("key")); !ok {
ctx.JSON(http.StatusNotFound, Json{"error": ctx.Param("key") + " not found"})
} else {
Expand All @@ -62,18 +97,34 @@ func (a *API) getBlock(ctx *Context) {
}

func (a *API) removeBlock(ctx *Context) {
if !a.checkToken(ctx) {
return
}

ctx.JSON(http.StatusOK, Json{"success": a.blocklist.Remove(ctx.Param("key"))})
}

func (a *API) setBlock(ctx *Context) {
if !a.checkToken(ctx) {
return
}

ctx.JSON(http.StatusOK, Json{"success": a.blocklist.Set(ctx.Param("key"))})
}

func (a *API) metrics(ctx *Context) {
if !a.checkToken(ctx) {
return
}

promhttp.Handler().ServeHTTP(ctx.Writer, ctx.Request)
}

func (a *API) purge(ctx *Context) {
if !a.checkToken(ctx) {
return
}

qtype := strings.ToUpper(ctx.Param("qtype"))
qname := dns.Fqdn(ctx.Param("qname"))

Expand Down Expand Up @@ -135,6 +186,9 @@ func (a *API) Run(ctx context.Context) {
}()

log.Info("API server listening...", "addr", a.addr)
if a.bearerToken != "" {
log.Info("API authorization bearer token", "token", a.bearerToken)
}

go func() {
<-ctx.Done()
Expand Down
79 changes: 79 additions & 0 deletions api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
Expand All @@ -20,6 +21,83 @@ func Test_Run(t *testing.T) {
a.Run(context.Background())
}

func Test_Authorization(t *testing.T) {
routes := []struct {
Method string
ReqURL string
ExpectedStatus int
}{
{"GET", "/api/v1/block/set/test.com", http.StatusUnauthorized},
{"GET", "/api/v1/block/get/test.com", http.StatusUnauthorized},
{"GET", "/api/v1/block/exists/test.com", http.StatusUnauthorized},
{"GET", "/api/v1/block/remove/test.com", http.StatusUnauthorized},
{"GET", "/api/v1/purge/test.com/A", http.StatusUnauthorized},
{"GET", "/metrics", http.StatusUnauthorized},
}

bearerToken := "secret_token"

a := New(&config.Config{BearerToken: bearerToken})

block := a.router.Group("/api/v1/block")
{
block.GET("/exists/:key", a.existsBlock)
block.GET("/exists/:key", a.existsBlock)
block.GET("/get/:key", a.getBlock)
block.GET("/remove/:key", a.removeBlock)
block.GET("/set/:key", a.setBlock)
block.POST("/set/:key", a.setBlock)
}

a.router.GET("/api/v1/purge/:qname/:qtype", a.purge)
a.router.GET("/metrics", a.metrics)

w := httptest.NewRecorder()
request, err := http.NewRequest("GET", "/metrics", nil)
if err != nil {
t.Fatalf("couldn't create request: %v\n", err)
}

a.router.ServeHTTP(w, request)

if w.Code != http.StatusUnauthorized {
t.Fatalf("Authorization not expected status code: %d", w.Code)
}

w = httptest.NewRecorder()
request.Header.Set("Authorization", "sometoken")
a.router.ServeHTTP(w, request)

if w.Code != http.StatusUnauthorized {
t.Fatalf("Authorization not expected status code: %d", w.Code)
}

w = httptest.NewRecorder()
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", a.bearerToken))
a.router.ServeHTTP(w, request)

if w.Code != http.StatusOK {
t.Fatalf("Authorization not expected status code: %d", w.Code)
}

for _, r := range routes {
w := httptest.NewRecorder()
request, err := http.NewRequest(r.Method, r.ReqURL, nil)

if err != nil {
t.Fatalf("couldn't create request: %v\n", err)
}

request.Header.Set("Authorization", "Bearer some_token")

a.router.ServeHTTP(w, request)

if w.Code != r.ExpectedStatus {
t.Fatalf("%s uri not expected status code: %d", r.ReqURL, w.Code)
}
}
}

func Test_AllAPICalls(t *testing.T) {
log.Root().SetHandler(log.LvlFilterHandler(0, log.StdoutHandler))
debugpprof = true
Expand All @@ -28,6 +106,7 @@ func Test_AllAPICalls(t *testing.T) {
cfg.Nullroute = "0.0.0.0"
cfg.Nullroutev6 = "::0"
cfg.BlockListDir = filepath.Join(os.TempDir(), "sdns_temp")
cfg.BearerToken = "secret_token"

middleware.Register("blocklist", func(cfg *config.Config) middleware.Handler { return blocklist.New(cfg) })
middleware.Setup(cfg)
Expand Down
7 changes: 6 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
"github.com/semihalev/log"
)

const configver = "1.3.6"
const configver = "1.3.7"

// Config type
type Config struct {
Expand All @@ -41,6 +41,7 @@ type Config struct {
TLSCertificate string
TLSPrivateKey string
API string
BearerToken string
Nullroute string
Nullroutev6 string
Hostsfile string
Expand Down Expand Up @@ -187,6 +188,10 @@ forwarderservers = [
# Address to bind to for the HTTP API server, left blank for disabled.
api = "127.0.0.1:8080"
# API bearer token for authorization. If the token set, Authorization header should be send on API requests.
# Header: Authorization: Bearer %%bearertoken%%
# bearertoken = ""
# What kind of information should be logged, Log verbosity level [crit, error, warn, info, debug].
loglevel = "info"
Expand Down
2 changes: 1 addition & 1 deletion sdns.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"github.com/semihalev/sdns/server"
)

const version = "1.3.6"
const version = "1.3.7"

var (
flagcfgpath string
Expand Down

0 comments on commit eaaed2a

Please sign in to comment.