Initial commit - SOGOMS v1.0.0
- sogoctl: supervisor avec health checks et restart auto - sogoway: gateway HTTP, auth JWT, routing par hostname - sogoms-db: microservice MariaDB avec pool par application - Protocol IPC Unix socket JSON length-prefixed - Config YAML multi-application (prokov) - Deploy script pour container Alpine gw3 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
136
internal/auth/jwt.go
Normal file
136
internal/auth/jwt.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Package auth gère l'authentification JWT et les mots de passe.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidToken = errors.New("invalid token")
|
||||
ErrExpiredToken = errors.New("token expired")
|
||||
)
|
||||
|
||||
// Claims représente les claims du JWT.
|
||||
type Claims struct {
|
||||
Sub int64 `json:"sub"` // User ID
|
||||
Email string `json:"email"` // Email
|
||||
Name string `json:"name"` // Nom
|
||||
App string `json:"app"` // Application ID
|
||||
Exp int64 `json:"exp"` // Expiration (Unix timestamp)
|
||||
Iat int64 `json:"iat"` // Issued at
|
||||
}
|
||||
|
||||
// JWT gère la génération et validation des tokens.
|
||||
type JWT struct {
|
||||
secret []byte
|
||||
expiration time.Duration
|
||||
}
|
||||
|
||||
// NewJWT crée un nouveau gestionnaire JWT.
|
||||
func NewJWT(secret string, expiration time.Duration) *JWT {
|
||||
return &JWT{
|
||||
secret: []byte(secret),
|
||||
expiration: expiration,
|
||||
}
|
||||
}
|
||||
|
||||
// Generate génère un nouveau token JWT.
|
||||
func (j *JWT) Generate(userID int64, email, name, appID string) (string, error) {
|
||||
now := time.Now()
|
||||
|
||||
claims := Claims{
|
||||
Sub: userID,
|
||||
Email: email,
|
||||
Name: name,
|
||||
App: appID,
|
||||
Iat: now.Unix(),
|
||||
Exp: now.Add(j.expiration).Unix(),
|
||||
}
|
||||
|
||||
return j.encode(claims)
|
||||
}
|
||||
|
||||
// Validate valide un token et retourne les claims.
|
||||
func (j *JWT) Validate(token string) (*Claims, error) {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
// Vérifier la signature
|
||||
signatureInput := parts[0] + "." + parts[1]
|
||||
expectedSig := j.sign(signatureInput)
|
||||
if parts[2] != expectedSig {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
// Décoder les claims
|
||||
claimsJSON, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
var claims Claims
|
||||
if err := json.Unmarshal(claimsJSON, &claims); err != nil {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
// Vérifier l'expiration
|
||||
if time.Now().Unix() > claims.Exp {
|
||||
return nil, ErrExpiredToken
|
||||
}
|
||||
|
||||
return &claims, nil
|
||||
}
|
||||
|
||||
// encode encode les claims en JWT.
|
||||
func (j *JWT) encode(claims Claims) (string, error) {
|
||||
// Header
|
||||
header := map[string]string{
|
||||
"alg": "HS256",
|
||||
"typ": "JWT",
|
||||
}
|
||||
headerJSON, _ := json.Marshal(header)
|
||||
headerB64 := base64.RawURLEncoding.EncodeToString(headerJSON)
|
||||
|
||||
// Payload
|
||||
claimsJSON, err := json.Marshal(claims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
claimsB64 := base64.RawURLEncoding.EncodeToString(claimsJSON)
|
||||
|
||||
// Signature
|
||||
signatureInput := headerB64 + "." + claimsB64
|
||||
signature := j.sign(signatureInput)
|
||||
|
||||
return signatureInput + "." + signature, nil
|
||||
}
|
||||
|
||||
// sign signe les données avec HMAC-SHA256.
|
||||
func (j *JWT) sign(data string) string {
|
||||
h := hmac.New(sha256.New, j.secret)
|
||||
h.Write([]byte(data))
|
||||
return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// ExtractToken extrait le token du header Authorization.
|
||||
func ExtractToken(authHeader string) (string, error) {
|
||||
if authHeader == "" {
|
||||
return "", fmt.Errorf("missing authorization header")
|
||||
}
|
||||
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
return "", fmt.Errorf("invalid authorization header format")
|
||||
}
|
||||
|
||||
return parts[1], nil
|
||||
}
|
||||
20
internal/auth/password.go
Normal file
20
internal/auth/password.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// HashPassword génère un hash bcrypt du mot de passe.
|
||||
func HashPassword(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
// VerifyPassword vérifie si le mot de passe correspond au hash.
|
||||
func VerifyPassword(hash, password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
182
internal/config/config.go
Normal file
182
internal/config/config.go
Normal file
@@ -0,0 +1,182 @@
|
||||
// Package config gère le chargement des configurations YAML.
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// AppConfig représente la configuration d'une application cliente.
|
||||
type AppConfig struct {
|
||||
App string `yaml:"app"`
|
||||
Version string `yaml:"version"`
|
||||
BasePath string `yaml:"base_path"`
|
||||
Hosts []string `yaml:"hosts"`
|
||||
Database Database `yaml:"database"`
|
||||
Auth Auth `yaml:"auth"`
|
||||
Routes []Route `yaml:"routes"`
|
||||
}
|
||||
|
||||
// Auth contient la configuration d'authentification.
|
||||
type Auth struct {
|
||||
JWTSecretFile string `yaml:"jwt_secret_file"`
|
||||
JWTExpiry string `yaml:"jwt_expiry"`
|
||||
jwtSecret string // Chargé depuis le fichier
|
||||
}
|
||||
|
||||
// JWTSecret retourne le secret JWT (chargé depuis le fichier).
|
||||
func (a *Auth) JWTSecret() string {
|
||||
return a.jwtSecret
|
||||
}
|
||||
|
||||
// Database contient la configuration de connexion à la base de données.
|
||||
type Database struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
User string `yaml:"user"`
|
||||
PasswordFile string `yaml:"password_file"`
|
||||
Name string `yaml:"name"`
|
||||
password string // Chargé depuis le fichier
|
||||
}
|
||||
|
||||
// Password retourne le mot de passe (chargé depuis le fichier).
|
||||
func (d *Database) Password() string {
|
||||
return d.password
|
||||
}
|
||||
|
||||
// DSN retourne la chaîne de connexion MySQL/MariaDB.
|
||||
func (d *Database) DSN() string {
|
||||
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&charset=utf8mb4",
|
||||
d.User, d.password, d.Host, d.Port, d.Name)
|
||||
}
|
||||
|
||||
// Route représente une route API.
|
||||
type Route struct {
|
||||
Path string `yaml:"path"`
|
||||
Method string `yaml:"method"`
|
||||
Scenario string `yaml:"scenario"`
|
||||
Auth *bool `yaml:"auth,omitempty"`
|
||||
}
|
||||
|
||||
// Registry stocke les configurations des applications.
|
||||
type Registry struct {
|
||||
configDir string
|
||||
apps map[string]*AppConfig // Par app_id
|
||||
byHost map[string]*AppConfig // Par hostname
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewRegistry crée un nouveau registre de configurations.
|
||||
func NewRegistry(configDir string) *Registry {
|
||||
return &Registry{
|
||||
configDir: configDir,
|
||||
apps: make(map[string]*AppConfig),
|
||||
byHost: make(map[string]*AppConfig),
|
||||
}
|
||||
}
|
||||
|
||||
// Load charge toutes les configurations depuis le répertoire routes.
|
||||
func (r *Registry) Load() error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
routesDir := filepath.Join(r.configDir, "routes")
|
||||
entries, err := os.ReadDir(routesDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read routes dir: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".yaml") {
|
||||
continue
|
||||
}
|
||||
|
||||
path := filepath.Join(routesDir, entry.Name())
|
||||
cfg, err := r.loadAppConfig(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load %s: %w", entry.Name(), err)
|
||||
}
|
||||
|
||||
r.apps[cfg.App] = cfg
|
||||
for _, host := range cfg.Hosts {
|
||||
r.byHost[host] = cfg
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadAppConfig charge une configuration d'application depuis un fichier YAML.
|
||||
func (r *Registry) loadAppConfig(path string) (*AppConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cfg AppConfig
|
||||
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Charger le mot de passe DB depuis le fichier
|
||||
if cfg.Database.PasswordFile != "" {
|
||||
passData, err := os.ReadFile(cfg.Database.PasswordFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read db password file: %w", err)
|
||||
}
|
||||
cfg.Database.password = strings.TrimSpace(string(passData))
|
||||
}
|
||||
|
||||
// Charger le secret JWT depuis le fichier
|
||||
if cfg.Auth.JWTSecretFile != "" {
|
||||
secretData, err := os.ReadFile(cfg.Auth.JWTSecretFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read jwt secret file: %w", err)
|
||||
}
|
||||
cfg.Auth.jwtSecret = strings.TrimSpace(string(secretData))
|
||||
}
|
||||
|
||||
// Port DB par défaut
|
||||
if cfg.Database.Port == 0 {
|
||||
cfg.Database.Port = 3306
|
||||
}
|
||||
|
||||
// Expiry JWT par défaut
|
||||
if cfg.Auth.JWTExpiry == "" {
|
||||
cfg.Auth.JWTExpiry = "24h"
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// GetByApp retourne la configuration d'une application par son ID.
|
||||
func (r *Registry) GetByApp(appID string) (*AppConfig, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
cfg, ok := r.apps[appID]
|
||||
return cfg, ok
|
||||
}
|
||||
|
||||
// GetByHost retourne la configuration d'une application par son hostname.
|
||||
func (r *Registry) GetByHost(host string) (*AppConfig, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
cfg, ok := r.byHost[host]
|
||||
return cfg, ok
|
||||
}
|
||||
|
||||
// Apps retourne la liste des IDs d'applications chargées.
|
||||
func (r *Registry) Apps() []string {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
apps := make([]string, 0, len(r.apps))
|
||||
for app := range r.apps {
|
||||
apps = append(apps, app)
|
||||
}
|
||||
return apps
|
||||
}
|
||||
169
internal/protocol/client.go
Normal file
169
internal/protocol/client.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client permet d'appeler un microservice via Unix socket.
|
||||
type Client struct {
|
||||
socketPath string
|
||||
conn net.Conn
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewClient crée un nouveau client.
|
||||
func NewClient(socketPath string) *Client {
|
||||
return &Client{
|
||||
socketPath: socketPath,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect établit la connexion au socket.
|
||||
func (c *Client) Connect() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.conn != nil {
|
||||
return nil // Déjà connecté
|
||||
}
|
||||
|
||||
conn, err := net.Dial("unix", c.socketPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial %s: %w", c.socketPath, err)
|
||||
}
|
||||
c.conn = conn
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close ferme la connexion.
|
||||
func (c *Client) Close() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.conn != nil {
|
||||
err := c.conn.Close()
|
||||
c.conn = nil
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Call envoie une requête et attend la réponse.
|
||||
func (c *Client) Call(ctx context.Context, req *Request) (*Response, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.conn == nil {
|
||||
return nil, fmt.Errorf("not connected")
|
||||
}
|
||||
|
||||
// Timeout
|
||||
timeout := time.Duration(req.TimeoutMs) * time.Millisecond
|
||||
if timeout == 0 {
|
||||
timeout = 5 * time.Second
|
||||
}
|
||||
deadline := time.Now().Add(timeout)
|
||||
c.conn.SetDeadline(deadline)
|
||||
|
||||
// Encoder et envoyer la requête
|
||||
data, err := Encode(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode request: %w", err)
|
||||
}
|
||||
|
||||
if err := writeMessage(c.conn, data); err != nil {
|
||||
c.conn = nil // Connexion cassée
|
||||
return nil, fmt.Errorf("write request: %w", err)
|
||||
}
|
||||
|
||||
// Lire la réponse
|
||||
respData, err := readMessage(c.conn)
|
||||
if err != nil {
|
||||
c.conn = nil // Connexion cassée
|
||||
return nil, fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
|
||||
resp, err := DecodeResponse(respData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode response: %w", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// CallAction raccourci pour appeler une action.
|
||||
func (c *Client) CallAction(ctx context.Context, action string, params map[string]any) (*Response, error) {
|
||||
req := NewRequest(action, params)
|
||||
return c.Call(ctx, req)
|
||||
}
|
||||
|
||||
// Pool gère un pool de connexions vers un service.
|
||||
type Pool struct {
|
||||
socketPath string
|
||||
clients chan *Client
|
||||
maxSize int
|
||||
}
|
||||
|
||||
// NewPool crée un pool de connexions.
|
||||
func NewPool(socketPath string, size int) *Pool {
|
||||
return &Pool{
|
||||
socketPath: socketPath,
|
||||
clients: make(chan *Client, size),
|
||||
maxSize: size,
|
||||
}
|
||||
}
|
||||
|
||||
// Get obtient un client du pool.
|
||||
func (p *Pool) Get() (*Client, error) {
|
||||
select {
|
||||
case client := <-p.clients:
|
||||
return client, nil
|
||||
default:
|
||||
// Créer un nouveau client
|
||||
client := NewClient(p.socketPath)
|
||||
if err := client.Connect(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Put remet un client dans le pool.
|
||||
func (p *Pool) Put(client *Client) {
|
||||
select {
|
||||
case p.clients <- client:
|
||||
// OK, remis dans le pool
|
||||
default:
|
||||
// Pool plein, fermer le client
|
||||
client.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Call obtient un client, exécute l'appel, et remet le client.
|
||||
func (p *Pool) Call(ctx context.Context, req *Request) (*Response, error) {
|
||||
client, err := p.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := client.Call(ctx, req)
|
||||
if err != nil {
|
||||
client.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.Put(client)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Close ferme tous les clients du pool.
|
||||
func (p *Pool) Close() {
|
||||
close(p.clients)
|
||||
for client := range p.clients {
|
||||
client.Close()
|
||||
}
|
||||
}
|
||||
90
internal/protocol/message.go
Normal file
90
internal/protocol/message.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Package protocol définit le protocole de communication IPC via Unix sockets.
|
||||
// Format: 4 bytes (big-endian length) + JSON payload
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Request représente une requête envoyée à un microservice.
|
||||
type Request struct {
|
||||
ID string `json:"id"`
|
||||
Action string `json:"action"`
|
||||
TenantID string `json:"tenant_id,omitempty"`
|
||||
Params map[string]any `json:"params,omitempty"`
|
||||
TimeoutMs int `json:"timeout_ms,omitempty"`
|
||||
}
|
||||
|
||||
// Response représente la réponse d'un microservice.
|
||||
type Response struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"` // "success" ou "error"
|
||||
Result any `json:"result,omitempty"`
|
||||
Error *Error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Error détaille une erreur.
|
||||
type Error struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// NewRequest crée une nouvelle requête avec un ID unique.
|
||||
func NewRequest(action string, params map[string]any) *Request {
|
||||
return &Request{
|
||||
ID: generateID(),
|
||||
Action: action,
|
||||
Params: params,
|
||||
TimeoutMs: 5000, // 5s par défaut
|
||||
}
|
||||
}
|
||||
|
||||
// Success crée une réponse de succès.
|
||||
func Success(reqID string, result any) *Response {
|
||||
return &Response{
|
||||
ID: reqID,
|
||||
Status: "success",
|
||||
Result: result,
|
||||
}
|
||||
}
|
||||
|
||||
// Failure crée une réponse d'erreur.
|
||||
func Failure(reqID string, code, message string) *Response {
|
||||
return &Response{
|
||||
ID: reqID,
|
||||
Status: "error",
|
||||
Error: &Error{
|
||||
Code: code,
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Encode sérialise un message en JSON.
|
||||
func Encode(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
// DecodeRequest désérialise une requête JSON.
|
||||
func DecodeRequest(data []byte) (*Request, error) {
|
||||
var req Request
|
||||
if err := json.Unmarshal(data, &req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &req, nil
|
||||
}
|
||||
|
||||
// DecodeResponse désérialise une réponse JSON.
|
||||
func DecodeResponse(data []byte) (*Response, error) {
|
||||
var resp Response
|
||||
if err := json.Unmarshal(data, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// generateID génère un ID unique basé sur le timestamp.
|
||||
func generateID() string {
|
||||
return "req_" + time.Now().Format("20060102150405.000000")
|
||||
}
|
||||
174
internal/protocol/server.go
Normal file
174
internal/protocol/server.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Handler traite une requête et retourne une réponse.
|
||||
type Handler func(ctx context.Context, req *Request) *Response
|
||||
|
||||
// Server écoute sur un Unix socket et dispatch les requêtes.
|
||||
type Server struct {
|
||||
socketPath string
|
||||
handler Handler
|
||||
listener net.Listener
|
||||
wg sync.WaitGroup
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// NewServer crée un nouveau serveur.
|
||||
func NewServer(socketPath string, handler Handler) *Server {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &Server{
|
||||
socketPath: socketPath,
|
||||
handler: handler,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// Start démarre le serveur.
|
||||
func (s *Server) Start() error {
|
||||
// Supprimer le socket existant
|
||||
if err := os.RemoveAll(s.socketPath); err != nil {
|
||||
return fmt.Errorf("remove socket: %w", err)
|
||||
}
|
||||
|
||||
listener, err := net.Listen("unix", s.socketPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen: %w", err)
|
||||
}
|
||||
s.listener = listener
|
||||
|
||||
// Permissions socket
|
||||
if err := os.Chmod(s.socketPath, 0660); err != nil {
|
||||
return fmt.Errorf("chmod socket: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[server] listening on %s", s.socketPath)
|
||||
|
||||
go s.acceptLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop arrête le serveur proprement.
|
||||
func (s *Server) Stop() {
|
||||
s.cancel()
|
||||
if s.listener != nil {
|
||||
s.listener.Close()
|
||||
}
|
||||
s.wg.Wait()
|
||||
os.RemoveAll(s.socketPath)
|
||||
log.Printf("[server] stopped")
|
||||
}
|
||||
|
||||
// acceptLoop accepte les connexions entrantes.
|
||||
func (s *Server) acceptLoop() {
|
||||
for {
|
||||
conn, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
default:
|
||||
log.Printf("[server] accept error: %v", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
s.wg.Add(1)
|
||||
go s.handleConn(conn)
|
||||
}
|
||||
}
|
||||
|
||||
// handleConn gère une connexion client.
|
||||
func (s *Server) handleConn(conn net.Conn) {
|
||||
defer s.wg.Done()
|
||||
defer conn.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// Lire la requête
|
||||
data, err := readMessage(conn)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Printf("[server] read error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Décoder la requête
|
||||
req, err := DecodeRequest(data)
|
||||
if err != nil {
|
||||
resp := Failure("", "DECODE_ERROR", err.Error())
|
||||
writeResponse(conn, resp)
|
||||
continue
|
||||
}
|
||||
|
||||
// Traiter la requête
|
||||
resp := s.handler(s.ctx, req)
|
||||
|
||||
// Envoyer la réponse
|
||||
if err := writeResponse(conn, resp); err != nil {
|
||||
log.Printf("[server] write error: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readMessage lit un message length-prefixed.
|
||||
func readMessage(r io.Reader) ([]byte, error) {
|
||||
// Lire les 4 bytes de longueur
|
||||
lengthBuf := make([]byte, 4)
|
||||
if _, err := io.ReadFull(r, lengthBuf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
length := binary.BigEndian.Uint32(lengthBuf)
|
||||
if length == 0 || length > 10*1024*1024 { // Max 10MB
|
||||
return nil, fmt.Errorf("invalid message length: %d", length)
|
||||
}
|
||||
|
||||
// Lire le payload
|
||||
data := make([]byte, length)
|
||||
if _, err := io.ReadFull(r, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// writeResponse écrit une réponse.
|
||||
func writeResponse(w io.Writer, resp *Response) error {
|
||||
data, err := Encode(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeMessage(w, data)
|
||||
}
|
||||
|
||||
// writeMessage écrit un message length-prefixed.
|
||||
func writeMessage(w io.Writer, data []byte) error {
|
||||
// Écrire la longueur
|
||||
lengthBuf := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(lengthBuf, uint32(len(data)))
|
||||
if _, err := w.Write(lengthBuf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Écrire le payload
|
||||
_, err := w.Write(data)
|
||||
return err
|
||||
}
|
||||
8
internal/version/version.go
Normal file
8
internal/version/version.go
Normal file
@@ -0,0 +1,8 @@
|
||||
// Package version contient les informations de version.
|
||||
package version
|
||||
|
||||
// Set via ldflags: -ldflags "-X sogoms.com/internal/version.Version=1.0.0"
|
||||
var (
|
||||
Version = "dev"
|
||||
BuildTime = "unknown"
|
||||
)
|
||||
Reference in New Issue
Block a user