remove environment

This commit is contained in:
rmanach 2025-01-07 10:51:54 +01:00
parent 42a3181797
commit 5e805b5b4d
9 changed files with 95 additions and 101 deletions

View File

@ -7,3 +7,8 @@ API_PORT= # defaul to 8585
API_SECURE= # default to "false" API_SECURE= # default to "false"
API_STORE_DIR= # default to "./store" API_STORE_DIR= # default to "./store"
# use a master key if you run on production
# MEILI_MASTER_KEY=
BASEURL_MEILISEARCH=http://meilisearch:7700
MEILI_ENV=development

View File

@ -8,5 +8,9 @@ build: lint
lint: lint:
golangci-lint run --fix golangci-lint run --fix
# .run-meilisearch:
# docker-compose up -d meilisearch
run: lint run: lint
# while [ "`curl --insecure -s -o /dev/null -w ''%{http_code}'' http://localhost:7700/health`" != "200" ]; do sleep 2; echo "waiting..."; done
go run main.go go run main.go

6
go.mod
View File

@ -2,11 +2,13 @@ module librapi
go 1.22.4 go 1.22.4
require github.com/rs/zerolog v1.33.0 require (
github.com/mattn/go-sqlite3 v1.14.24
github.com/rs/zerolog v1.33.0
)
require ( require (
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mattn/go-sqlite3 v1.14.24 // indirect
golang.org/x/sys v0.12.0 // indirect golang.org/x/sys v0.12.0 // indirect
) )

View File

@ -135,7 +135,7 @@ func postLogin(w http.ResponseWriter, r *http.Request, a services.IAuthenticate)
return return
} }
cookie := session.GenerateCookie() cookie := session.GenerateCookie(a.IsSecure())
http.SetCookie(w, cookie) http.SetCookie(w, cookie)
buf := bytes.NewBufferString("") buf := bytes.NewBufferString("")

35
main.go
View File

@ -6,6 +6,8 @@ import (
"librapi/services" "librapi/services"
"os" "os"
"os/signal" "os/signal"
"strconv"
"sync"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
@ -15,6 +17,33 @@ import (
"librapi/handlers/upload" "librapi/handlers/upload"
) )
const (
defaultPort = 8585
defaulStoreDir = "./store"
)
var (
isSecure = os.Getenv("API_SECURE") == "true"
port = sync.OnceValue[int](func() int {
port, err := strconv.Atoi(os.Getenv("API_PORT"))
if err != nil {
log.Warn().Err(err).Int("default", defaultPort).Msg("unable to load API_PORT, set to default")
return defaultPort
}
return port
})
storeDir = sync.OnceValue[string](func() string {
storeDir := os.Getenv("API_STORE_DIR")
if storeDir == "" {
log.Warn().Str("default", defaulStoreDir).Msg("API_STORE_DIR env var empty, set to default")
return defaulStoreDir
}
return storeDir
})
)
func initLogger() { func initLogger() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Logger = log.With().Caller().Logger().Output(zerolog.ConsoleWriter{Out: os.Stderr}) log.Logger = log.With().Caller().Logger().Output(zerolog.ConsoleWriter{Out: os.Stderr})
@ -26,12 +55,12 @@ func main() {
ctx, fnCancel := signal.NotifyContext(context.Background(), os.Kill, os.Interrupt) ctx, fnCancel := signal.NotifyContext(context.Background(), os.Kill, os.Interrupt)
defer fnCancel() defer fnCancel()
auth := services.NewAuthentication(ctx) auth := services.NewAuthentication(ctx, isSecure)
bs := services.NewBookStore(services.GetEnv().GetDir()) bs := services.NewBookStore(storeDir())
srv := server.NewServer( srv := server.NewServer(
ctx, ctx,
services.GetEnv().GetPort(), port(),
server.NewHandler(home.URL, home.Handler(bs)), server.NewHandler(home.URL, home.Handler(bs)),
server.NewHandler(upload.URL, upload.Handler(auth, bs)), server.NewHandler(upload.URL, upload.Handler(auth, bs)),
server.NewHandler(login.URL, login.Handler(auth)), server.NewHandler(login.URL, login.Handler(auth)),

View File

@ -3,7 +3,6 @@ package server
import ( import (
"context" "context"
"errors" "errors"
"librapi/services"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
@ -34,11 +33,6 @@ type Server struct {
type ServerOption func() type ServerOption func()
func NewServer(ctx context.Context, port int, handlers ...Handler) Server { func NewServer(ctx context.Context, port int, handlers ...Handler) Server {
if port == 0 {
log.Warn().Int("port", services.GetEnv().GetPort()).Msg("no port detected, set to default")
port = services.GetEnv().GetPort()
}
srvmux := http.NewServeMux() srvmux := http.NewServeMux()
for _, h := range handlers { for _, h := range handlers {
srvmux.HandleFunc(h.url, h.fnHandle) srvmux.HandleFunc(h.url, h.fnHandle)

View File

@ -6,17 +6,52 @@ import (
"encoding/hex" "encoding/hex"
"errors" "errors"
"net/http" "net/http"
"os"
"strconv"
"sync" "sync"
"time" "time"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
const (
defaultAPISessionExpirationDuration = 5 * 60 * time.Second
defaultAdminPassword = "admin"
defaultAdminUsername = "admin"
)
var ( var (
ErrSessionIDCollision = errors.New("sessionId collision") ErrSessionIDCollision = errors.New("sessionId collision")
ErrUnauthorized = errors.New("unauthorized") ErrUnauthorized = errors.New("unauthorized")
) )
var adminPassword = sync.OnceValue[string](func() string {
adminPassword := os.Getenv("API_ADMIN_PASSWORD")
if adminPassword == "" {
log.Error().Msg("API_ADMIN_PASSWORD env var is empty, set to default")
return defaultAdminPassword
}
return adminPassword
})
var adminUsername = sync.OnceValue[string](func() string {
adminUsername := os.Getenv("API_ADMIN_USERNAME")
if adminUsername == "" {
log.Error().Msg("API_ADMIN_USERNAME env var is empty, set to default")
return defaultAdminUsername
}
return adminUsername
})
var sessionExpirationTime = sync.OnceValue[time.Duration](func() time.Duration {
sessionExpirationDuration, err := strconv.Atoi(os.Getenv("API_SESSION_EXPIRATION_DURATION"))
if err != nil {
log.Warn().Err(err).Dur("default", defaultAPISessionExpirationDuration).Msg("unable to load API_SESSION_EXPIRATION_DURATION, set to default")
return defaultAPISessionExpirationDuration
}
return time.Duration(sessionExpirationDuration)
})
func generateSessionID() (string, error) { func generateSessionID() (string, error) {
sessionID := make([]byte, 32) //nolint sessionID := make([]byte, 32) //nolint
if _, err := rand.Read(sessionID); err != nil { if _, err := rand.Read(sessionID); err != nil {
@ -32,7 +67,7 @@ type Session struct {
expirationTime time.Time expirationTime time.Time
} }
func (s *Session) GenerateCookie() *http.Cookie { func (s *Session) GenerateCookie(isSecure bool) *http.Cookie {
s.l.RLock() s.l.RLock()
defer s.l.RUnlock() defer s.l.RUnlock()
@ -40,7 +75,7 @@ func (s *Session) GenerateCookie() *http.Cookie {
Name: "session_id", Name: "session_id",
Value: s.sessionID, Value: s.sessionID,
HttpOnly: true, HttpOnly: true,
Secure: GetEnv().isSecure, Secure: isSecure,
Expires: s.expirationTime, Expires: s.expirationTime,
} }
} }
@ -48,6 +83,7 @@ func (s *Session) GenerateCookie() *http.Cookie {
type IAuthenticate interface { type IAuthenticate interface {
IsLogged(r *http.Request) bool IsLogged(r *http.Request) bool
Authenticate(username, password string) (*Session, error) Authenticate(username, password string) (*Session, error)
IsSecure() bool
} }
var _ IAuthenticate = (*Authentication)(nil) var _ IAuthenticate = (*Authentication)(nil)
@ -59,15 +95,17 @@ type Authentication struct {
fnCancel context.CancelFunc fnCancel context.CancelFunc
sessions map[string]*Session sessions map[string]*Session
isSecure bool
} }
func NewAuthentication(ctx context.Context) *Authentication { func NewAuthentication(ctx context.Context, isSecure bool) *Authentication {
ctxChild, fnCancel := context.WithCancel(ctx) ctxChild, fnCancel := context.WithCancel(ctx)
s := &Authentication{ s := &Authentication{
ctx: ctxChild, ctx: ctxChild,
fnCancel: fnCancel, fnCancel: fnCancel,
sessions: map[string]*Session{}, sessions: map[string]*Session{},
isSecure: isSecure,
} }
s.purgeWorker() s.purgeWorker()
@ -108,6 +146,10 @@ func (a *Authentication) purgeWorker() {
}() }()
} }
func (a *Authentication) IsSecure() bool {
return a.isSecure
}
func (a *Authentication) Stop() { func (a *Authentication) Stop() {
a.fnCancel() a.fnCancel()
} }
@ -117,8 +159,7 @@ func (a *Authentication) Done() <-chan struct{} {
} }
func (a *Authentication) Authenticate(username, password string) (*Session, error) { func (a *Authentication) Authenticate(username, password string) (*Session, error) {
adminUsername, adminPassword := GetEnv().GetCredentials() if username != adminUsername() || password != adminPassword() {
if username != adminUsername || password != adminPassword {
return nil, ErrUnauthorized return nil, ErrUnauthorized
} }
@ -136,7 +177,7 @@ func (a *Authentication) Authenticate(username, password string) (*Session, erro
return nil, ErrSessionIDCollision return nil, ErrSessionIDCollision
} }
now := time.Now().Add(GetEnv().GetSessionExpirationDuration()) now := time.Now().Add(sessionExpirationTime())
session := Session{expirationTime: now, sessionID: sessionID} session := Session{expirationTime: now, sessionID: sessionID}
a.sessions[sessionID] = &session a.sessions[sessionID] = &session

View File

@ -174,7 +174,7 @@ func (bs *BookStore) Save(bm *BookMetadata, content io.ReadCloser) error {
defer content.Close() defer content.Close()
bm.Path = filepath.Join(GetEnv().GetDir(), bm.getFormattedName()) bm.Path = filepath.Join(bs.dir, bm.getFormattedName())
dst, err := os.Create(bm.Path) dst, err := os.Create(bm.Path)
if err != nil { if err != nil {

View File

@ -1,81 +0,0 @@
package services
import (
"os"
"strconv"
"sync"
"time"
"github.com/rs/zerolog/log"
)
const (
defaultAPISessionExpirationDuration = 5 * 60 * time.Second
defaultPort = 8585
defaultMainDir = "./store"
)
var env = sync.OnceValue[environment](newEnv)
type environment struct {
adminUsername string
adminPassword string
sessionExpirationDuration time.Duration
port int
isSecure bool
storeDir string
}
func (e environment) GetCredentials() (username, password string) {
return e.adminUsername, e.adminPassword
}
func (e environment) GetSessionExpirationDuration() time.Duration {
return e.sessionExpirationDuration
}
func (e environment) GetPort() int {
return e.port
}
func (e environment) IsSecure() bool {
return e.isSecure
}
func (e environment) GetDir() string {
return e.storeDir
}
func newEnv() environment {
env := environment{
adminUsername: "test",
adminPassword: "test",
isSecure: os.Getenv("API_SECURE") == "true",
}
sessionExpirationDuration, err := strconv.Atoi(os.Getenv("API_SESSION_EXPIRATION_DURATION"))
env.sessionExpirationDuration = time.Duration(sessionExpirationDuration)
if err != nil {
log.Warn().Err(err).Dur("default", defaultAPISessionExpirationDuration).Msg("unable to load API_SESSION_EXPIRATION_DURATION, set to default")
env.sessionExpirationDuration = defaultAPISessionExpirationDuration
}
port, err := strconv.Atoi(os.Getenv("API_PORT"))
env.port = port
if err != nil {
log.Warn().Err(err).Int("default", defaultPort).Msg("unable to load API_PORT, set to default")
env.port = defaultPort
}
storeDir := os.Getenv("API_STORE_DIR")
if storeDir == "" {
storeDir = defaultMainDir
}
env.storeDir = storeDir
return env
}
func GetEnv() environment {
return env()
}