Skip to content

Commit

Permalink
feat: add bearer auth
Browse files Browse the repository at this point in the history
  • Loading branch information
Code42Cate committed Apr 3, 2024
1 parent c506465 commit 153477b
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 12 deletions.
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,13 @@ The exporter can be configured via a `config.yaml` file placed in `/etc/plausibl
When put into a config file, the variable names are `snake_cased`, when set via the environment, the variables must be `UPPER_SNAKE_CASED`.
All options can be set in the config file or environment variables, with environment variables taking precedence.

| Option | Required | Description | Default |
| -------------------- | -------- | ---------------------------------------------------------------- | -------------- |
| `plausible_host` || The hostname and protocol of your plausible server | - |
| `plausible_site_ids` || The IDs of the sites you want to fetch from plausible, as a list | - |
| `plausible_token` || A valid API token for your plausible server | - |
| `listen_address` || Which host and port to listen to | `0.0.0.0:8080` |
| Option | Required | Description | Default |
| -------------------- | -------- | ---------------------------------------------------------------------- | -------------- |
| `plausible_host` || The hostname and protocol of your plausible server | - |
| `plausible_site_ids` || The IDs of the sites you want to fetch from plausible, as a list | - |
| `plausible_token` || A valid API token for your plausible server | - |
| `listen_address` || Which host and port to listen to | `0.0.0.0:8080` |
| `bearer_auth_token` || Bearer token to authorize metrics route through `Authorization` header | - |

> **Note:** Config options that are lists must be comma-separated when passed as an environment variable, e.g. `PLAUSIBLE_SITE_IDS=riesinger.dev,nononsense.cooking`
Expand All @@ -76,6 +77,11 @@ In case you've configured multiple sites to be scraped, you can differentiate be
You can use the exported metrics just like you'd use any other metric scraped by Prometheus.
The `examples` folder contains a small [demo dashboard](./examples/grafana-dashboard.json). You can use this as a starting point for integrating the metrics into your own dashboards.

#### Authorization

If you want to restrict access to the metrics endpoint, you can set the `bearer_auth_token` (`BEARER_AUTH_TOKEN` environment variable) option.
This will require the client to send a `Authorization: Bearer <token>` header with the request.

## License

This project is MIT-licensed, see [LICENSE.md](./LICENSE.md)
11 changes: 7 additions & 4 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import (
)

var (
listenAddress string
plausibleHost *url.URL
token string
siteIDs []string
listenAddress string
bearerAuthToken string
plausibleHost *url.URL
token string
siteIDs []string
)

func readConfig() error {
Expand Down Expand Up @@ -50,5 +51,7 @@ func readConfig() error {
return fmt.Errorf("config: no plausible site IDs provided")
}

bearerAuthToken = viper.GetString("bearer_auth_token")

return nil
}
4 changes: 4 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ func main() {
}()

srv := server.New()
if bearerAuthToken != "" {
srv.SetBearerAuthToken(bearerAuthToken)
}

go func() {
if err := srv.ListenAndServe(listenAddress); err != nil && err != http.ErrServerClosed {
log.Fatalf("server: %v\n", err)
Expand Down
24 changes: 24 additions & 0 deletions server/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package server

import (
"net/http"
)

func BearerAuthMiddleware(next http.Handler, token string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

headerValue := r.Header.Get("Authorization")
if headerValue == "" {
http.Error(w, "Authorization header is required", http.StatusUnauthorized)
return
}

expectedToken := "Bearer " + token
if headerValue != expectedToken {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}

next.ServeHTTP(w, r)
})
}
65 changes: 65 additions & 0 deletions server/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package server_test

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/riesinger/plausible-exporter/server"
)

func TestValidToken(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})

testToken := "secret-token"

req, _ := http.NewRequest("GET", "/", nil)
req.Header.Set("Authorization", "Bearer "+testToken)
rr := httptest.NewRecorder()

handlerWithMiddleware := server.BearerAuthMiddleware(handler, testToken)
handlerWithMiddleware.ServeHTTP(rr, req)

if status := rr.Code; status != http.StatusOK {
t.Errorf("Handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
}

func TestInvalidToken(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("Handler should not be called with an invalid token")
})

testToken := "secret-token"

req, _ := http.NewRequest("GET", "/", nil)
req.Header.Set("Authorization", "Bearer wrong-token")
rr := httptest.NewRecorder()

handlerWithMiddleware := server.BearerAuthMiddleware(handler, testToken)
handlerWithMiddleware.ServeHTTP(rr, req)

if status := rr.Code; status != http.StatusUnauthorized {
t.Errorf("Handler returned wrong status code: got %v want %v", status, http.StatusUnauthorized)
}
}

func TestMissingToken(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("Handler should not be called without a token")
})

testToken := "secret-token"

req, _ := http.NewRequest("GET", "/", nil)
rr := httptest.NewRecorder()

handlerWithMiddleware := server.BearerAuthMiddleware(handler, testToken)
handlerWithMiddleware.ServeHTTP(rr, req)

if status := rr.Code; status != http.StatusUnauthorized {
t.Errorf("Handler returned wrong status code: got %v want %v", status, http.StatusUnauthorized)
}
}
15 changes: 13 additions & 2 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,27 @@ import (
)

type Server struct {
s http.Server
s http.Server
bearerAuthToken string
}

func New() *Server {
return &Server{}
}

func (srv *Server) SetBearerAuthToken(token string) {
srv.bearerAuthToken = token
}

func (srv *Server) ListenAndServe(listenAddress string) error {
mux := http.NewServeMux()
mux.Handle("/metrics", promhttp.Handler())

if srv.bearerAuthToken != "" {
mux.Handle("/metrics", BearerAuthMiddleware(promhttp.Handler(), srv.bearerAuthToken))
} else {
mux.Handle("/metrics", promhttp.Handler())
}

srv.s = http.Server{
Addr: listenAddress,
Handler: mux,
Expand Down

0 comments on commit 153477b

Please sign in to comment.