feat: add SQLite storage for login attempts and sessions
Adds persistent storage using modernc.org/sqlite (pure Go). Login attempts are deduplicated by (username, password, ip) with counts. Sessions and session logs are tracked with UUID IDs. Includes embedded SQL migrations, configurable retention with background pruning, and an in-memory store for tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"git.t-juice.club/torjus/oubliette/internal/auth"
|
||||
"git.t-juice.club/torjus/oubliette/internal/config"
|
||||
"git.t-juice.club/torjus/oubliette/internal/storage"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
@@ -21,15 +22,17 @@ const sessionTimeout = 30 * time.Second
|
||||
|
||||
type Server struct {
|
||||
cfg config.Config
|
||||
store storage.Store
|
||||
authenticator *auth.Authenticator
|
||||
sshConfig *ssh.ServerConfig
|
||||
logger *slog.Logger
|
||||
connSem chan struct{} // semaphore limiting concurrent connections
|
||||
}
|
||||
|
||||
func New(cfg config.Config, logger *slog.Logger) (*Server, error) {
|
||||
func New(cfg config.Config, store storage.Store, logger *slog.Logger) (*Server, error) {
|
||||
s := &Server{
|
||||
cfg: cfg,
|
||||
store: store,
|
||||
authenticator: auth.NewAuthenticator(cfg.Auth),
|
||||
logger: logger,
|
||||
connSem: make(chan struct{}, cfg.SSH.MaxConnections),
|
||||
@@ -123,6 +126,18 @@ func (s *Server) handleConn(conn net.Conn) {
|
||||
func (s *Server) handleSession(channel ssh.Channel, requests <-chan *ssh.Request, conn *ssh.ServerConn) {
|
||||
defer channel.Close()
|
||||
|
||||
ip := extractIP(conn.RemoteAddr())
|
||||
sessionID, err := s.store.CreateSession(context.Background(), ip, conn.User(), "")
|
||||
if err != nil {
|
||||
s.logger.Error("failed to create session", "err", err)
|
||||
} else {
|
||||
defer func() {
|
||||
if err := s.store.EndSession(context.Background(), sessionID, time.Now()); err != nil {
|
||||
s.logger.Error("failed to end session", "err", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Handle session requests (pty-req, shell, etc.)
|
||||
go func() {
|
||||
for req := range requests {
|
||||
@@ -179,6 +194,10 @@ func (s *Server) passwordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.
|
||||
"reason", d.Reason,
|
||||
)
|
||||
|
||||
if err := s.store.RecordLoginAttempt(context.Background(), conn.User(), string(password), ip); err != nil {
|
||||
s.logger.Error("failed to record login attempt", "err", err)
|
||||
}
|
||||
|
||||
if d.Accepted {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.t-juice.club/torjus/oubliette/internal/config"
|
||||
"git.t-juice.club/torjus/oubliette/internal/storage"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
@@ -112,7 +113,8 @@ func TestIntegrationSSHConnect(t *testing.T) {
|
||||
}
|
||||
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
srv, err := New(cfg, logger)
|
||||
store := storage.NewMemoryStore()
|
||||
srv, err := New(cfg, store, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("creating server: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user