Skip to content

Commit

Permalink
Init project
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandreh2ag committed Jun 17, 2022
0 parents commit 3825151
Show file tree
Hide file tree
Showing 38 changed files with 3,043 additions and 0 deletions.
16 changes: 16 additions & 0 deletions .traefik.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
displayName: Ip filter or Basic auth
type: middleware

import: github.com/alexandreh2ag/traefik-ipfilter-basicauth

summary: 'Restricts access to your services by ip whitelist or basic auth'

testData:
basicAuth:
realm: "Realm"
users:
- traefik:$apr1$imy7rq16$PbXJYj5lsqZ71HoIBfm/T0 # traefik / traefik
ipWhiteList:
sourceRange:
- "127.0.0.1"
- "10.0.0.1/32"
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Ip Filter - Basic auth

Ip Filter - Basic auth is a middleware plugin for [Traefik](https://github.com/traefik/traefik) which try to authorize client by IP address or at least by Basic auth.

## Configuration

### Static

```toml
[pilot]
token = "xxxx"

[experimental.plugins.ipFilter_basicAuth]
modulename = "github.com/alexandreh2ag/traefik-ipfilter-basicauth"
version = "vX.X.X"
```

### Dynamic

To configure the `Ip Filter - Basic auth` plugin you should create a [middleware](https://doc.traefik.io/traefik/middlewares/overview/) in
your dynamic configuration as explained [here](https://doc.traefik.io/traefik/middlewares/overview/).

You must define at least one source range IP and one user from configuration or file.

The configuration of middleware is quite similar then traefik middlewares ([IPWhiteList](https://doc.traefik.io/traefik/middlewares/http/ipwhitelist/) / [BasicAuth](https://doc.traefik.io/traefik/middlewares/http/basicauth/)).

```yaml
http:
middlewares:
my-ipFilter_basicAuth:
plugin:
ipFilter_basicAuth:
basicAuth:
realm: "Realm"
usersFile: "/path/to/my/usersfile"
users:
- traefik:$apr1$imy7rq16$PbXJYj5lsqZ71HoIBfm/T0 # traefik / traefik
headerField: "X-WebAuth-User"
removeHeader: true
ipWhiteList:
sourceRange:
- "127.0.0.1"
- "10.0.0.1/32"
```
65 changes: 65 additions & 0 deletions auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package traefik_ipfilter_basicauth

import (
"os"
"strings"
)

// UserParser Parses a string and return a userName/userHash. An error if the format of the string is incorrect.
type UserParser func(user string) (string, string, error)

const (
defaultRealm = "traefik"
authorizationHeader = "Authorization"
)

func getUsers(fileName string, appendUsers []string, parser UserParser) (map[string]string, error) {
users, err := loadUsers(fileName, appendUsers)
if err != nil {
return nil, err
}

userMap := make(map[string]string)
for _, user := range users {
userName, userHash, err := parser(user)
if err != nil {
return nil, err
}
userMap[userName] = userHash
}

return userMap, nil
}

func loadUsers(fileName string, appendUsers []string) ([]string, error) {
var users []string
var err error

if fileName != "" {
users, err = getLinesFromFile(fileName)
if err != nil {
return nil, err
}
}

return append(users, appendUsers...), nil
}

func getLinesFromFile(filename string) ([]string, error) {
dat, err := os.ReadFile(filename)
if err != nil {
return nil, err
}

// Trim lines and filter out blanks
rawLines := strings.Split(string(dat), "\n")
var filteredLines []string
for _, rawLine := range rawLines {
line := strings.TrimSpace(rawLine)
if line != "" && !strings.HasPrefix(line, "#") {
filteredLines = append(filteredLines, line)
}
}

return filteredLines, nil
}
101 changes: 101 additions & 0 deletions checker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package traefik_ipfilter_basicauth

import (
"errors"
"fmt"
"net"
"strings"
)

// Checker allows to check that addresses are in a trusted IPs.
type Checker struct {
authorizedIPs []*net.IP
authorizedIPsNet []*net.IPNet
}

// NewChecker builds a new Checker given a list of CIDR-Strings to trusted IPs.
func NewChecker(trustedIPs []string) (*Checker, error) {
if len(trustedIPs) == 0 {
return nil, errors.New("no trusted IPs provided")
}

checker := &Checker{}

for _, ipMask := range trustedIPs {
if ipAddr := net.ParseIP(ipMask); ipAddr != nil {
checker.authorizedIPs = append(checker.authorizedIPs, &ipAddr)
continue
}

_, ipAddr, err := net.ParseCIDR(ipMask)
if err != nil {
return nil, fmt.Errorf("parsing CIDR trusted IPs %s: %w", ipAddr, err)
}
checker.authorizedIPsNet = append(checker.authorizedIPsNet, ipAddr)
}

return checker, nil
}

// IsAuthorized checks if provided request is authorized by the trusted IPs.
func (ip *Checker) IsAuthorized(addr string) error {
var invalidMatches []string

host, _, err := net.SplitHostPort(addr)
if err != nil {
host = addr
}

ok, err := ip.Contains(host)
if err != nil {
return err
}

if !ok {
invalidMatches = append(invalidMatches, addr)
return fmt.Errorf("%q matched none of the trusted IPs", strings.Join(invalidMatches, ", "))
}

return nil
}

// Contains checks if provided address is in the trusted IPs.
func (ip *Checker) Contains(addr string) (bool, error) {
if len(addr) == 0 {
return false, errors.New("empty IP address")
}

ipAddr, err := parseIP(addr)
if err != nil {
return false, fmt.Errorf("unable to parse address: %s: %w", addr, err)
}

return ip.ContainsIP(ipAddr), nil
}

// ContainsIP checks if provided address is in the trusted IPs.
func (ip *Checker) ContainsIP(addr net.IP) bool {
for _, authorizedIP := range ip.authorizedIPs {
fmt.Println("Client IP: " + addr.String() + " - ip check : " + authorizedIP.String())
if authorizedIP.Equal(addr) {
return true
}
}

for _, authorizedNet := range ip.authorizedIPsNet {
if authorizedNet.Contains(addr) {
return true
}
}

return false
}

func parseIP(addr string) (net.IP, error) {
userIP := net.ParseIP(addr)
if userIP == nil {
return nil, fmt.Errorf("can't parse IP from address %s", addr)
}

return userIP, nil
}
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module github.com/alexandreh2ag/traefik-ipfilter-basicauth

go 1.17

require (
github.com/abbot/go-http-auth v0.4.0 // indirect
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
golang.org/x/net v0.0.0-20220614195744-fb05da6f9022 // indirect
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0=
github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20220614195744-fb05da6f9022 h1:0qjDla5xICC2suMtyRH/QqX3B1btXTfNsIt/i4LFgO0=
golang.org/x/net v0.0.0-20220614195744-fb05da6f9022/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
130 changes: 130 additions & 0 deletions middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package traefik_ipfilter_basicauth

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

goauth "github.com/abbot/go-http-auth"
)

type BasicAuth struct {
Users []string `json:"users,omitempty" toml:"users,omitempty" yaml:"users,omitempty" loggable:"false"`
UsersFile string `json:"usersFile,omitempty" toml:"usersFile,omitempty" yaml:"usersFile,omitempty"`
Realm string `json:"realm,omitempty" toml:"realm,omitempty" yaml:"realm,omitempty"`
RemoveHeader bool `json:"removeHeader,omitempty" toml:"removeHeader,omitempty" yaml:"removeHeader,omitempty" export:"true"`
HeaderField string `json:"headerField,omitempty" toml:"headerField,omitempty" yaml:"headerField,omitempty" export:"true"`
}

type IPWhiteList struct {
SourceRange []string `json:"sourceRange,omitempty" toml:"sourceRange,omitempty" yaml:"sourceRange,omitempty"`
}

// Config the plugin configuration.
type Config struct {
BasicAuth BasicAuth `json:"basicAuth,omitempty" toml:"basicAuth,omitempty" yaml:"basicAuth,omitempty"`
IPWhiteList IPWhiteList `json:"ipWhiteList,omitempty" toml:"ipWhiteList,omitempty" yaml:"ipWhiteList,omitempty"`
}

// CreateConfig creates the default plugin configuration.
func CreateConfig() *Config {
return &Config{}
}

// Middleware a Middleware plugin.
type Middleware struct {
auth *goauth.BasicAuth
next http.Handler
users map[string]string
headerField string
removeHeader bool
whiteLister *Checker
name string
}

// New created a new Middleware plugin.
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
if len(config.IPWhiteList.SourceRange) == 0 {
return nil, errors.New("sourceRange is empty, IPWhiteLister not created")
}

checker, err := NewChecker(config.IPWhiteList.SourceRange)
if err != nil {
return nil, fmt.Errorf("cannot parse CIDR whitelist %s: %w", config.IPWhiteList.SourceRange, err)
}

users, err := getUsers(config.BasicAuth.UsersFile, config.BasicAuth.Users, basicUserParser)
if err != nil {
return nil, err
}
m := &Middleware{
users: users,
whiteLister: checker,
removeHeader: config.BasicAuth.RemoveHeader,
headerField: config.BasicAuth.HeaderField,
next: next,
name: name,
}
realm := defaultRealm
if len(config.BasicAuth.Realm) > 0 {
realm = config.BasicAuth.Realm
}
m.auth = &goauth.BasicAuth{Realm: realm, Secrets: m.secretBasic}

return m, nil
}

func (m *Middleware) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
user := ""
if req.RemoteAddr == "" {
fmt.Println("RemoteAddr is empty")
return
}

err := m.whiteLister.IsAuthorized(req.RemoteAddr)
if err != nil {
fmt.Println("Try basic auth")
ok := false
if user = m.auth.CheckAuth(req); user == "" {
ok = false
} else {
ok = true
}

if !ok {
m.auth.RequireAuth(rw, req)
fmt.Println("IP is not authorize and basic auth is not valid")
return
}
}
req.URL.User = url.User(user)
if m.headerField != "" {
req.Header[m.headerField] = []string{user}
}

if m.removeHeader {
req.Header.Del(authorizationHeader)
}

fmt.Println("Request authorized")
m.next.ServeHTTP(rw, req)
}

func (m *Middleware) secretBasic(user, realm string) string {
if secret, ok := m.users[user]; ok {
return secret
}

return ""
}

func basicUserParser(user string) (string, string, error) {
split := strings.Split(user, ":")
if len(split) != 2 {
return "", "", fmt.Errorf("error parsing BasicUser: %v", user)
}
return split[0], split[1], nil
}
5 changes: 5 additions & 0 deletions vendor/github.com/abbot/go-http-auth/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 3825151

Please sign in to comment.