mirror of https://github.com/mpolden/echoip
Added JWT Authentication
This commit is contained in:
parent
712e2166d5
commit
d9bf3259e0
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -1,5 +1,18 @@
|
|||
# Changelog
|
||||
|
||||
## 1.2.0 (2023-10-06)
|
||||
|
||||
### Features
|
||||
|
||||
- JWT Authentication
|
||||
|
||||
## 1.1.0 (2023-10-06)
|
||||
|
||||
### Features
|
||||
|
||||
- Environment Variable Configuration [712e216](https://github.com/levelsoftware/echoip/commit/712e2166d51fdb85229f52caa380743245f31dfa)
|
||||
|
||||
|
||||
## 1.0.0 (2023-10-06)
|
||||
|
||||
### Features
|
||||
|
|
|
@ -133,7 +133,7 @@ AsnFile = ""
|
|||
```
|
||||
|
||||
### Environment Variables for Configuration
|
||||
You can also use environment variables for configuration, most likely used for Docker.
|
||||
You can also use environment variables for configuration, most likely used for Docker. Configuration file takes precedence first, and then environment variables. Remove the value from the config file if you wish to use the environment variable.
|
||||
|
||||
```
|
||||
ECHOIP_LISTEN=":8080"
|
||||
|
|
|
@ -102,14 +102,20 @@ func main() {
|
|||
serverCache = &cache.Null{}
|
||||
}
|
||||
|
||||
server := http.New(parser, serverCache, runConfig.CacheTtl, runConfig.Profile)
|
||||
server := http.New(parser, serverCache, &runConfig)
|
||||
server.IPHeaders = runConfig.TrustedHeaders
|
||||
|
||||
if _, err := os.Stat(runConfig.TemplateDir); err == nil {
|
||||
server.Template = runConfig.TemplateDir
|
||||
} else {
|
||||
if _, err := os.Stat(runConfig.TemplateDir); err != nil {
|
||||
runConfig.TemplateDir = ""
|
||||
log.Printf("Not configuring default handler: Template not found: %s", runConfig.TemplateDir)
|
||||
}
|
||||
if runConfig.Jwt.Enabled {
|
||||
log.Println("Enabling JWT Auth")
|
||||
|
||||
if len(runConfig.Jwt.Secret) == 0 {
|
||||
log.Fatal("Please provide a JWT Token secret when JWT is enabled")
|
||||
}
|
||||
}
|
||||
if runConfig.ReverseLookup {
|
||||
log.Println("Enabling reverse lookup")
|
||||
server.LookupAddr = iputil.LookupAddr
|
||||
|
@ -118,9 +124,9 @@ func main() {
|
|||
log.Println("Enabling port lookup")
|
||||
server.LookupPort = iputil.LookupPort
|
||||
}
|
||||
|
||||
if runConfig.ShowSponsor {
|
||||
log.Println("Enabling sponsor logo")
|
||||
server.Sponsor = runConfig.ShowSponsor
|
||||
}
|
||||
if len(runConfig.TrustedHeaders) > 0 {
|
||||
log.Printf("Trusting remote IP from header(s): %s", runConfig.TrustedHeaders)
|
||||
|
|
|
@ -18,6 +18,11 @@ type GeoIP struct {
|
|||
AsnFile string
|
||||
}
|
||||
|
||||
type Jwt struct {
|
||||
Enabled bool
|
||||
Secret string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Listen string
|
||||
TemplateDir string
|
||||
|
@ -30,9 +35,11 @@ type Config struct {
|
|||
|
||||
Database string
|
||||
Profile bool
|
||||
Debug bool
|
||||
|
||||
IPStack IPStack
|
||||
GeoIP GeoIP
|
||||
Jwt Jwt
|
||||
}
|
||||
|
||||
func GetConfig() (Config, error) {
|
||||
|
@ -41,6 +48,9 @@ func GetConfig() (Config, error) {
|
|||
TemplateDir: getenv_string("ECHOIP_TEMPLATE_DIR", "html/"),
|
||||
RedisUrl: getenv_string("ECHOIP_REDIS_URL", ""),
|
||||
Database: getenv_string("ECHOIP_DATABASE", "geoip"),
|
||||
Jwt: Jwt{
|
||||
Secret: getenv_string("ECHOIP_JWT_SECRET", ""),
|
||||
},
|
||||
IPStack: IPStack{
|
||||
ApiKey: getenv_string("ECHOIP_IPSTACK_API_KEY", ""),
|
||||
},
|
||||
|
@ -51,6 +61,18 @@ func GetConfig() (Config, error) {
|
|||
},
|
||||
}
|
||||
|
||||
jwtAuthEnabled, err := getenv_bool("ECHOIP_JWT_AUTH", false)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
defaultConfig.Jwt.Enabled = jwtAuthEnabled
|
||||
|
||||
debug, err := getenv_bool("ECHOIP_DEBUG", false)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
defaultConfig.Debug = debug
|
||||
|
||||
cacheTtl, err := getenv_int("ECHOIP_CACHE_TTL", 3600)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
|
|
|
@ -7,7 +7,12 @@ PortLookup = true
|
|||
ShowSponsor = true
|
||||
Database = "ipstack" # use "IP Stack" or "GeoIP"
|
||||
TrustedHeaders = []
|
||||
Profile = false # enable debug / profiling
|
||||
Profile = false # enable profiling
|
||||
Debug = false # enable debugging, ex print jwt token errors
|
||||
|
||||
[Jwt]
|
||||
Enabled = false
|
||||
Secret = ""
|
||||
|
||||
[IPStack]
|
||||
ApiKey = "HelloWorld"
|
||||
|
|
1
go.mod
1
go.mod
|
@ -5,6 +5,7 @@ go 1.18
|
|||
require (
|
||||
github.com/BurntSushi/toml v1.3.2
|
||||
github.com/go-redis/cache/v9 v9.0.0
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/oschwald/geoip2-golang v1.5.0
|
||||
github.com/qioalice/ipstack v1.0.1
|
||||
github.com/redis/go-redis/v9 v9.2.1
|
||||
|
|
2
go.sum
2
go.sum
|
@ -21,6 +21,8 @@ github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV
|
|||
github.com/go-redis/cache/v9 v9.0.0 h1:0thdtFo0xJi0/WXbRVu8B066z8OvVymXTJGaXrVWnN0=
|
||||
github.com/go-redis/cache/v9 v9.0.0/go.mod h1:cMwi1N8ASBOufbIvk7cdXe2PbPjK/WMRL95FFHWsSgI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package http
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type appError struct {
|
||||
Error error
|
||||
|
@ -22,6 +25,11 @@ func notFound(err error) *appError {
|
|||
}
|
||||
|
||||
func badRequest(err error) *appError {
|
||||
badAuth := new(InvalidTokenError)
|
||||
if errors.As(err, &badAuth) {
|
||||
return &appError{Error: err, Code: http.StatusUnauthorized}
|
||||
}
|
||||
|
||||
return &appError{Error: err, Code: http.StatusBadRequest}
|
||||
}
|
||||
|
||||
|
|
58
http/http.go
58
http/http.go
|
@ -12,7 +12,9 @@ import (
|
|||
"net/http/pprof"
|
||||
|
||||
rcache "github.com/go-redis/cache/v9"
|
||||
"github.com/golang-jwt/jwt"
|
||||
"github.com/levelsoftware/echoip/cache"
|
||||
"github.com/levelsoftware/echoip/config"
|
||||
parser "github.com/levelsoftware/echoip/iputil/paser"
|
||||
"github.com/levelsoftware/echoip/useragent"
|
||||
|
||||
|
@ -27,15 +29,12 @@ const (
|
|||
)
|
||||
|
||||
type Server struct {
|
||||
Template string
|
||||
IPHeaders []string
|
||||
LookupAddr func(net.IP) (string, error)
|
||||
LookupPort func(net.IP, uint64) error
|
||||
cache cache.Cache
|
||||
cacheTtl int
|
||||
runConfig *config.Config
|
||||
parser parser.Parser
|
||||
profile bool
|
||||
Sponsor bool
|
||||
}
|
||||
|
||||
type PortResponse struct {
|
||||
|
@ -44,8 +43,8 @@ type PortResponse struct {
|
|||
Reachable bool `json:"reachable"`
|
||||
}
|
||||
|
||||
func New(parser parser.Parser, cache cache.Cache, cacheTtl int, profile bool) *Server {
|
||||
return &Server{cache: cache, cacheTtl: cacheTtl, parser: parser, profile: profile}
|
||||
func New(parser parser.Parser, cache cache.Cache, runConfig *config.Config) *Server {
|
||||
return &Server{cache: cache, parser: parser, runConfig: runConfig}
|
||||
}
|
||||
|
||||
func ipFromForwardedForHeader(v string) string {
|
||||
|
@ -104,6 +103,10 @@ func userAgentFromRequest(r *http.Request) *useragent.UserAgent {
|
|||
}
|
||||
|
||||
func (s *Server) newResponse(r *http.Request) (parser.Response, error) {
|
||||
if err := handleAuth(r, s.runConfig); err != nil {
|
||||
return parser.Response{}, err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
ip, err := ipFromRequest(s.IPHeaders, r, true)
|
||||
|
@ -130,7 +133,7 @@ func (s *Server) newResponse(r *http.Request) (parser.Response, error) {
|
|||
response, err = s.parser.Parse(ip, hostname)
|
||||
|
||||
log.Printf("Caching response for %s", ip.String())
|
||||
if err := s.cache.Set(ctx, ip.String(), cachedResponse.Build(response), s.cacheTtl); err != nil {
|
||||
if err := s.cache.Set(ctx, ip.String(), cachedResponse.Build(response), s.runConfig.CacheTtl); err != nil {
|
||||
return parser.Response{}, err
|
||||
}
|
||||
|
||||
|
@ -158,6 +161,10 @@ func (s *Server) newPortResponse(r *http.Request) (PortResponse, error) {
|
|||
}
|
||||
|
||||
func (s *Server) CLIHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
if err := handleAuth(r, s.runConfig); err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
|
||||
ip, err := ipFromRequest(s.IPHeaders, r, true)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
|
@ -259,10 +266,12 @@ func (s *Server) DefaultHandler(w http.ResponseWriter, r *http.Request) *appErro
|
|||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error())
|
||||
}
|
||||
t, err := template.ParseGlob(s.Template + "/*")
|
||||
|
||||
t, err := template.ParseGlob(s.runConfig.TemplateDir + "/*")
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
|
||||
json, err := json.MarshalIndent(response, "", " ")
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
|
@ -287,7 +296,7 @@ func (s *Server) DefaultHandler(w http.ResponseWriter, r *http.Request) *appErro
|
|||
response.Longitude + 0.05,
|
||||
string(json),
|
||||
s.LookupPort != nil,
|
||||
s.Sponsor,
|
||||
s.runConfig.ShowSponsor,
|
||||
}
|
||||
|
||||
if err := t.Execute(w, &data); err != nil {
|
||||
|
@ -373,7 +382,7 @@ func (s *Server) Handler() http.Handler {
|
|||
}
|
||||
|
||||
// Browser
|
||||
if s.Template != "" {
|
||||
if s.runConfig.TemplateDir != "" {
|
||||
r.Route("GET", "/", s.DefaultHandler)
|
||||
}
|
||||
|
||||
|
@ -383,7 +392,7 @@ func (s *Server) Handler() http.Handler {
|
|||
}
|
||||
|
||||
// Profiling
|
||||
if s.profile {
|
||||
if s.runConfig.Profile {
|
||||
r.Route("GET", "/debug/pprof/cmdline", wrapHandlerFunc(pprof.Cmdline))
|
||||
r.Route("GET", "/debug/pprof/profile", wrapHandlerFunc(pprof.Profile))
|
||||
r.Route("GET", "/debug/pprof/symbol", wrapHandlerFunc(pprof.Symbol))
|
||||
|
@ -401,3 +410,30 @@ func (s *Server) ListenAndServe(addr string) error {
|
|||
func formatCoordinate(c float64) string {
|
||||
return strconv.FormatFloat(c, 'f', 6, 64)
|
||||
}
|
||||
|
||||
type InvalidTokenError struct{}
|
||||
|
||||
func (m *InvalidTokenError) Error() string {
|
||||
return "invalid_token"
|
||||
}
|
||||
|
||||
func handleAuth(r *http.Request, runConfig *config.Config) error {
|
||||
if !runConfig.Jwt.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
authorization := r.Header.Get("Authorization")
|
||||
tokenString := strings.ReplaceAll(authorization, "Bearer ", "")
|
||||
|
||||
if _, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(runConfig.Jwt.Secret), nil
|
||||
}); err != nil {
|
||||
if runConfig.Debug {
|
||||
log.Printf("Error validating token ( %s ): %s \n", tokenString, err)
|
||||
}
|
||||
|
||||
return new(InvalidTokenError)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue