Skip to content

Commit

Permalink
faet(server): add authentication middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
qdm12 committed Sep 4, 2024
1 parent ff7cadb commit 7c177f1
Show file tree
Hide file tree
Showing 17 changed files with 616 additions and 6 deletions.
1 change: 1 addition & 0 deletions cmd/gluetun/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,7 @@ func _main(ctx context.Context, buildInfo models.BuildInformation,
"http server", goroutine.OptionTimeout(defaultShutdownTimeout))
httpServer, err := server.New(httpServerCtx, controlServerAddress, controlServerLogging,
logger.New(log.SetComponent("http server")),
allSettings.ControlServer.Auth,
buildInfo, vpnLooper, portForwardLooper, dnsLooper, updaterLooper, publicIPLooper,
storage, ipv6Supported)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/golang/mock v1.6.0
github.com/klauspost/compress v1.17.9
github.com/klauspost/pgzip v1.2.6
github.com/pelletier/go-toml/v2 v2.2.2
github.com/qdm12/dns/v2 v2.0.0-rc6
github.com/qdm12/gosettings v0.4.2
github.com/qdm12/goshutdown v0.3.0
Expand Down
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ github.com/breml/rootcerts v0.2.17 h1:0/M2BE2Apw0qEJCXDOkaiu7d5Sx5ObNfe1BkImJ4u1
github.com/breml/rootcerts v0.2.17/go.mod h1:S/PKh+4d1HUn4HQovEB8hPJZO6pUZYrIhmXBhsegfXw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
Expand Down Expand Up @@ -47,6 +48,8 @@ github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
Expand Down Expand Up @@ -75,6 +78,13 @@ github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
Expand Down Expand Up @@ -148,6 +158,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
Expand Down
31 changes: 31 additions & 0 deletions internal/configuration/settings/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"strconv"

"github.com/qdm12/gluetun/internal/server/middlewares/auth"
"github.com/qdm12/gosettings"
"github.com/qdm12/gosettings/reader"
"github.com/qdm12/gotree"
Expand All @@ -19,6 +20,15 @@ type ControlServer struct {
// Log can be true or false to enable logging on requests.
// It cannot be nil in the internal state.
Log *bool
// AuthFilePath is the path to the file containing the authentication
// configuration for the middleware.
// It cannot be empty in the internal state and defaults to
// /gluetun/auth/config.toml.
AuthFilePath string
// Auth contains settings for the authentication middleware.
// These are parsed from a configuration file specified by
// AuthFilePath.
Auth auth.Settings
}

func (c ControlServer) validate() (err error) {
Expand All @@ -39,6 +49,11 @@ func (c ControlServer) validate() (err error) {
ErrControlServerPrivilegedPort, port, uid)
}

err = c.Auth.Validate()
if err != nil {
return fmt.Errorf("validating authentication middleware: %w", err)
}

return nil
}

Expand All @@ -55,11 +70,15 @@ func (c *ControlServer) copy() (copied ControlServer) {
func (c *ControlServer) overrideWith(other ControlServer) {
c.Address = gosettings.OverrideWithPointer(c.Address, other.Address)
c.Log = gosettings.OverrideWithPointer(c.Log, other.Log)
c.AuthFilePath = gosettings.OverrideWithComparable(c.AuthFilePath, other.AuthFilePath)
c.Auth.OverrideWith(other.Auth)
}

func (c *ControlServer) setDefaults() {
c.Address = gosettings.DefaultPointer(c.Address, ":8000")
c.Log = gosettings.DefaultPointer(c.Log, true)
c.AuthFilePath = gosettings.DefaultComparable(c.AuthFilePath, "/gluetun/auth/config.toml")
c.Auth.SetDefaults()
}

func (c ControlServer) String() string {
Expand All @@ -70,6 +89,8 @@ func (c ControlServer) toLinesNode() (node *gotree.Node) {
node = gotree.New("Control server settings:")
node.Appendf("Listening address: %s", *c.Address)
node.Appendf("Logging: %s", gosettings.BoolToYesNo(c.Log))
node.Appendf("Authentication file path: %s", c.AuthFilePath)
node.AppendNode(c.Auth.ToLinesNode())
return node
}

Expand All @@ -78,6 +99,16 @@ func (c *ControlServer) read(r *reader.Reader) (err error) {
if err != nil {
return err
}

c.Address = r.Get("HTTP_CONTROL_SERVER_ADDRESS")

c.AuthFilePath = r.String("HTTP_CONTROL_SERVER_AUTH_CONFIG_FILEPATH")
if c.AuthFilePath != "" {
c.Auth, err = auth.Read(c.AuthFilePath) // TODO: move to internal/config/sources/files maybe?
if err != nil {
return fmt.Errorf("reading authentication middleware settings: %w", err)
}
}

return nil
}
6 changes: 5 additions & 1 deletion internal/configuration/settings/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ func Test_Settings_String(t *testing.T) {
| └── Enabled: no
├── Control server settings:
| ├── Listening address: :8000
| └── Logging: yes
| ├── Logging: yes
| ├── Authentication file path: /gluetun/auth/config.toml
| └── Authentication middleware settings:
| ├── Authentications defined: public
| └── Roles defined: public
├── Storage settings:
| └── Filepath: /gluetun/servers.json
├── OS Alpine settings:
Expand Down
15 changes: 12 additions & 3 deletions internal/server/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ package server

import (
"context"
"fmt"
"net/http"
"strings"

"github.com/qdm12/gluetun/internal/models"
"github.com/qdm12/gluetun/internal/server/middlewares/auth"
"github.com/qdm12/gluetun/internal/server/middlewares/log"
)

func newHandler(ctx context.Context, logger infoWarner, logging bool,
func newHandler(ctx context.Context, logger Logger, logging bool,
authSettings auth.Settings,
buildInfo models.BuildInformation,
vpnLooper VPNLooper,
pfGetter PortForwardedGetter,
Expand All @@ -18,7 +21,7 @@ func newHandler(ctx context.Context, logger infoWarner, logging bool,
publicIPLooper PublicIPLoop,
storage Storage,
ipv6Supported bool,
) (httpHandler http.Handler) {
) (httpHandler http.Handler, err error) {
handler := &handler{}

vpn := newVPNHandler(ctx, vpnLooper, storage, ipv6Supported, logger)
Expand All @@ -30,14 +33,20 @@ func newHandler(ctx context.Context, logger infoWarner, logging bool,
handler.v0 = newHandlerV0(ctx, logger, vpnLooper, dnsLooper, updaterLooper)
handler.v1 = newHandlerV1(logger, buildInfo, vpn, openvpn, dns, updater, publicip)

authMiddleware, err := auth.New(authSettings, logger)
if err != nil {
return nil, fmt.Errorf("creating auth middleware: %w", err)
}

middlewares := []func(http.Handler) http.Handler{
authMiddleware,
log.New(logger, logging),
}
httpHandler = handler
for _, middleware := range middlewares {
httpHandler = middleware(httpHandler)
}
return httpHandler
return httpHandler, nil
}

type handler struct {
Expand Down
2 changes: 2 additions & 0 deletions internal/server/logger.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package server

type Logger interface {
Debugf(format string, args ...any)
infoer
warner
Warnf(format string, args ...any)
errorer
}

Expand Down
30 changes: 30 additions & 0 deletions internal/server/middlewares/auth/configfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package auth

import (
"errors"
"fmt"
"os"

"github.com/pelletier/go-toml/v2"
)

// Read reads the toml file specified by the filepath given.
func Read(filepath string) (settings Settings, err error) {
file, err := os.Open(filepath)
if err != nil {
return settings, fmt.Errorf("opening file: %w", err)
}
decoder := toml.NewDecoder(file)
decoder.DisallowUnknownFields()
err = decoder.Decode(&settings)
if err != nil {
strictErr := new(toml.StrictMissingError)
ok := errors.As(err, &strictErr)
if ok {
return settings, fmt.Errorf("toml decoding file: %w:\n%s",
strictErr, strictErr.String())
}
return settings, fmt.Errorf("toml decoding file: %w", err)
}
return settings, nil
}
81 changes: 81 additions & 0 deletions internal/server/middlewares/auth/configfile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package auth

import (
"io/fs"
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// Read reads the toml file specified by the filepath given.
func Test_Read(t *testing.T) {
t.Parallel()

testCases := map[string]struct {
fileContent string
settings Settings
errMessage string
}{
"empty_file": {},
"unknown field": {
fileContent: `unknown = "what is this"`,
errMessage: `toml decoding file: strict mode: fields in the document are missing in the target struct:
1| unknown = "what is this"
| ~~~~~~~ missing field`,
},
"filled_settings": {
fileContent: `[[auths]]
name = "abc"
method = "none"
[[auths]]
name = "xyz"
method = "oauth2"
[[roles]]
name = "public"
auths = ["abc"]
[[roles.routes]]
Method = 'GET'
Path = '/v1/vpn/status'`,
settings: Settings{
Auths: []Auth{{
Name: "abc",
Method: MethodNone,
}, {
Name: "xyz",
Method: "oauth2",
}},
Roles: []Role{{
Name: "public",
Auths: []string{"abc"},
Routes: []Route{{Method: "GET", Path: "/v1/vpn/status"}},
}},
},
},
}

for name, testCase := range testCases {
testCase := testCase
t.Run(name, func(t *testing.T) {
t.Parallel()

tempDir := t.TempDir()
filepath := tempDir + "/config.toml"
const permissions fs.FileMode = 0600
err := os.WriteFile(filepath, []byte(testCase.fileContent), permissions)
require.NoError(t, err)

settings, err := Read(filepath)

assert.Equal(t, testCase.settings, settings)
if testCase.errMessage != "" {
assert.EqualError(t, err, testCase.errMessage)
} else {
assert.NoError(t, err)
}
})
}
}
26 changes: 26 additions & 0 deletions internal/server/middlewares/auth/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package auth

func andStrings(strings []string) (result string) {
return joinStrings(strings, "and")
}

func orStrings(strings []string) (result string) {
return joinStrings(strings, "or")
}

func joinStrings(strings []string, lastJoin string) (result string) {
if len(strings) == 0 {
return ""
}

result = strings[0]
for i := 1; i < len(strings); i++ {
if i < len(strings)-1 {
result += ", " + strings[i]
} else {
result += " " + lastJoin + " " + strings[i]
}
}

return result
}
6 changes: 6 additions & 0 deletions internal/server/middlewares/auth/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package auth

type DebugLogger interface {
Debugf(format string, args ...any)
Warnf(format string, args ...any)
}
8 changes: 8 additions & 0 deletions internal/server/middlewares/auth/interfaces_local.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package auth

import "net/http"

type authorizationChecker interface {
equal(other authorizationChecker) bool
isAuthorized(writer http.ResponseWriter, request *http.Request) bool
}
Loading

0 comments on commit 7c177f1

Please sign in to comment.