Skip to content

Commit

Permalink
all: support sso
Browse files Browse the repository at this point in the history
Updates #8
  • Loading branch information
changkun committed Nov 22, 2021
1 parent eba9aac commit 6a0e4ba
Show file tree
Hide file tree
Showing 15 changed files with 369 additions and 115 deletions.
135 changes: 135 additions & 0 deletions auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Copyright 2021 Changkun Ou. All rights reserved.
// Use of this source code is governed by a MIT
// license that can be found in the LICENSE file.

package main

import (
"errors"
"fmt"
"log"
"net/http"
"net/url"
"sync"
"sync/atomic"
"time"

"changkun.de/x/login"
"changkun.de/x/redir/internal/config"
"changkun.de/x/redir/internal/utils"
)

var errUnauthorized = errors.New("request unauthorized")

// blocklist holds the ip that should be blocked for further requests.
//
// This map may keep grow without releasing memory because of
// continuously attempts. we also do not persist this type of block info
// to the disk, which means if we reboot the service then all the blocker
// are gone and they can attack the server again.
// We clear the map very month.
var blocklist sync.Map // map[string]*blockinfo{}

func init() {
t := time.NewTicker(time.Hour * 24 * 30)
go func() {
for range t.C {
blocklist.Range(func(k, v interface{}) bool {
blocklist.Delete(k)
return true
})
}
}()
}

type blockinfo struct {
failCount int64
lastFail atomic.Value // time.Time
blockTime atomic.Value // time.Duration
}

const maxFailureAttempts = 3

func (s *server) handleAuth(w http.ResponseWriter, r *http.Request) (user string, err error) {
switch config.Conf.Auth.Enable {
case config.None:
return
case config.SSO:
user, err := login.HandleAuth(w, r)
if err != nil {
uu, _ := url.Parse(config.Conf.Auth.SSO)
q := uu.Query()
q.Set("redirect", "https://"+r.Host+r.URL.String())
uu.RawQuery = q.Encode()
http.Redirect(w, r, uu.String(), http.StatusFound)
}
return user, err
case config.Basic:
}

w.Header().Set("WWW-Authenticate", `Basic realm="redir"`)

u, p, ok := r.BasicAuth()
if !ok {
w.WriteHeader(http.StatusUnauthorized)
err = fmt.Errorf("%w: failed to parsing basic auth", errUnauthorized)
return
}

// check if the IP failure attempts are too much
// if so, direct abort the request without checking credentials
ip := utils.ReadIP(r)
if i, ok := blocklist.Load(ip); ok {
info := i.(*blockinfo)
count := atomic.LoadInt64(&info.failCount)
if count > maxFailureAttempts {
// if the ip is under block, then directly abort
last := info.lastFail.Load().(time.Time)
bloc := info.blockTime.Load().(time.Duration)

if time.Now().UTC().Sub(last.Add(bloc)) < 0 {
log.Printf("block ip %v, too much failure attempts. Block time: %v, release until: %v\n",
ip, bloc, last.Add(bloc))
err = fmt.Errorf("%w: too much failure attempts", errUnauthorized)
return
}

// clear the failcount, but increase the next block time
atomic.StoreInt64(&info.failCount, 0)
info.blockTime.Store(bloc * 2)
}
}

defer func() {
if !errors.Is(err, errUnauthorized) {
return
}

if i, ok := blocklist.Load(ip); !ok {
info := &blockinfo{
failCount: 1,
}
info.lastFail.Store(time.Now().UTC())
info.blockTime.Store(time.Second * 10)

blocklist.Store(ip, info)
} else {
info := i.(*blockinfo)
atomic.AddInt64(&info.failCount, 1)
info.lastFail.Store(time.Now().UTC())
}
}()

found := false
for _, account := range config.Conf.Auth.Basic {
if u == account.Username && p == account.Password {
found = true
break
}
}
if !found {
w.WriteHeader(http.StatusUnauthorized)
return "", fmt.Errorf("%w: username or password is invalid", errUnauthorized)
}
return u, nil
}
5 changes: 3 additions & 2 deletions data/redirconf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ x:
repo_path: https://github.com/changkun
godoc_host: https://pkg.go.dev/
auth:
enable: true
accounts:
enable: basic # or sso, none
sso: https://login.changkun.de
basic:
- username: changkun
password: redir
stats:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module changkun.de/x/redir
go 1.16

require (
changkun.de/x/login v0.0.0-20211122130521-1ad63a31a4e7
github.com/yuin/goldmark v1.4.2
go.mongodb.org/mongo-driver v1.5.1
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
changkun.de/x/login v0.0.0-20211122130521-1ad63a31a4e7 h1:nzxKIYUZF08AgXPqZMqGTnpZIWYaIkvk2F6nnxVQ4VA=
changkun.de/x/login v0.0.0-20211122130521-1ad63a31a4e7/go.mod h1:sxQtRW27EJgQr9R/SFKtyLppBbEOS8Fk0MXA1ptakEw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/aws/aws-sdk-go v1.34.28 h1:sscPpn/Ns3i0F4HPEWAVcwdIRaZZCuL7llJ2/60yPIk=
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
Expand Down Expand Up @@ -31,6 +33,7 @@ github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWe
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
Expand Down Expand Up @@ -88,6 +91,7 @@ github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.4.2 h1:5qVKCqCRBaGz8EepBTi7pbIw8gGCFnB1Mi6kXU4dYv8=
github.com/yuin/goldmark v1.4.2/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.mongodb.org/mongo-driver v1.5.1 h1:9nOVLGDfOaZ9R0tBumx/BcuqkbFpyTCU2r/Po7A2azI=
go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
Expand All @@ -110,6 +114,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
Expand Down
15 changes: 12 additions & 3 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ import (
"gopkg.in/yaml.v3"
)

type authType string

var (
None authType = "none"
Basic authType = "basic"
SSO authType = "sso"
)

type config struct {
Title string `yaml:"title"`
Host string `yaml:"host"`
Expand All @@ -41,11 +49,12 @@ type config struct {
GoDocHost string `yaml:"godoc_host"`
} `yaml:"x"`
Auth struct {
Enable bool `yaml:"enable"`
Accounts []struct {
Enable authType `yaml:"enable"`
SSO string `yaml:"sso"`
Basic []struct {
Username string `yaml:"username"`
Password string `yaml:"password"`
} `yaml:"accounts"`
} `yaml:"basic"`
} `yaml:"auth"`
Stats struct {
Enable bool `yaml:"enable"`
Expand Down
5 changes: 3 additions & 2 deletions internal/config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ x:
repo_path: https://github.com/changkun
godoc_host: https://pkg.go.dev/
auth:
enable: true
accounts:
enable: basic # or sso, none
sso: https://login.changkun.de
basic:
- username: changkun
password: redir
stats:
Expand Down
111 changes: 3 additions & 108 deletions short.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"

"changkun.de/x/redir/internal/config"
Expand All @@ -27,8 +25,6 @@ import (
"changkun.de/x/redir/internal/utils"
)

var errUnauthorized = errors.New("request unauthorized")

// shortHandler redirects the current request to a known link if the alias is
// found in the redir store.
func (s *server) shortHandler(kind models.AliasKind) http.Handler {
Expand Down Expand Up @@ -59,107 +55,6 @@ func (s *server) shortHandler(kind models.AliasKind) http.Handler {
})
}

// blocklist holds the ip that should be blocked for further requests.
//
// This map may keep grow without releasing memory because of
// continuously attempts. we also do not persist this type of block info
// to the disk, which means if we reboot the service then all the blocker
// are gone and they can attack the server again.
// We clear the map very month.
var blocklist sync.Map // map[string]*blockinfo{}

func init() {
t := time.NewTicker(time.Hour * 24 * 30)
go func() {
for range t.C {
blocklist.Range(func(k, v interface{}) bool {
blocklist.Delete(k)
return true
})
}
}()
}

type blockinfo struct {
failCount int64
lastFail atomic.Value // time.Time
blockTime atomic.Value // time.Duration
}

const maxFailureAttempts = 3

func (s *server) handleAuth(w http.ResponseWriter, r *http.Request) (user, pass string, err error) {
if !config.Conf.Auth.Enable {
return
}

w.Header().Set("WWW-Authenticate", `Basic realm="redir"`)

u, p, ok := r.BasicAuth()
if !ok {
w.WriteHeader(http.StatusUnauthorized)
err = fmt.Errorf("%w: failed to parsing basic auth", errUnauthorized)
return
}

// check if the IP failure attempts are too much
// if so, direct abort the request without checking credentials
ip := utils.ReadIP(r)
if i, ok := blocklist.Load(ip); ok {
info := i.(*blockinfo)
count := atomic.LoadInt64(&info.failCount)
if count > maxFailureAttempts {
// if the ip is under block, then directly abort
last := info.lastFail.Load().(time.Time)
bloc := info.blockTime.Load().(time.Duration)

if time.Now().UTC().Sub(last.Add(bloc)) < 0 {
log.Printf("block ip %v, too much failure attempts. Block time: %v, release until: %v\n",
ip, bloc, last.Add(bloc))
err = fmt.Errorf("%w: too much failure attempts", errUnauthorized)
return
}

// clear the failcount, but increase the next block time
atomic.StoreInt64(&info.failCount, 0)
info.blockTime.Store(bloc * 2)
}
}

defer func() {
if !errors.Is(err, errUnauthorized) {
return
}

if i, ok := blocklist.Load(ip); !ok {
info := &blockinfo{
failCount: 1,
}
info.lastFail.Store(time.Now().UTC())
info.blockTime.Store(time.Second * 10)

blocklist.Store(ip, info)
} else {
info := i.(*blockinfo)
atomic.AddInt64(&info.failCount, 1)
info.lastFail.Store(time.Now().UTC())
}
}()

found := false
for _, account := range config.Conf.Auth.Accounts {
if u == account.Username && p == account.Password {
found = true
break
}
}
if !found {
w.WriteHeader(http.StatusUnauthorized)
return "", "", fmt.Errorf("%w: username or password is invalid", errUnauthorized)
}
return u, p, nil
}

type shortInput struct {
Op short.Op `json:"op"`
Alias string `json:"alias"`
Expand All @@ -186,7 +81,7 @@ func (s *server) shortHandlerPost(kind models.AliasKind, w http.ResponseWriter,
}()

// All post request must be authenticated.
user, _, err := s.handleAuth(w, r)
user, err := s.handleAuth(w, r)
if err != nil {
return
}
Expand Down Expand Up @@ -568,7 +463,7 @@ func (s *server) sIndex(
case "index-pro": // data with statistics
return s.indexData(ctx, w, r, kind, false)
case "admin":
_, _, err := s.handleAuth(w, r)
_, err := s.handleAuth(w, r)
if err != nil {
return err
}
Expand Down Expand Up @@ -600,7 +495,7 @@ func (s *server) indexData(
public bool,
) error {
if !public {
_, _, err := s.handleAuth(w, r)
_, err := s.handleAuth(w, r)
if err != nil {
return err
}
Expand Down
15 changes: 15 additions & 0 deletions vendor/changkun.de/x/login/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/
Loading

0 comments on commit 6a0e4ba

Please sign in to comment.