Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ go.work
ui/node_modules/
ui/dist/

# Compiled vite config artifacts
ui/vite.config.js
ui/vite.config.d.ts

# SQLite database
*.db

Expand Down
16 changes: 16 additions & 0 deletions api/handlers/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,24 @@ func ExecuteInTransactionWithResult(c *gin.Context, db *sql.DB, operation string
c.JSON(http.StatusOK, response.Data)
}

// allowedTables is the whitelist of table names that can be used in dynamic SQL.
var allowedTables = map[string]bool{
"resources": true,
"services": true,
"middlewares": true,
"resource_services": true,
"mtls_clients": true,
"mtls_config": true,
}

// DeleteInTransaction is a specialized helper for delete operations
func DeleteInTransaction(c *gin.Context, db *sql.DB, table string, id string, additionalDeletes ...func(*sql.Tx) error) {
if !allowedTables[table] {
log.Printf("Error: attempted delete from disallowed table %q", table)
ResponseWithError(c, http.StatusBadRequest, "Invalid table name")
return
}

err := WithTransaction(db, func(tx *sql.Tx) error {
// Execute any additional deletes first (e.g., related records)
for _, deleteFn := range additionalDeletes {
Expand Down
40 changes: 0 additions & 40 deletions database/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,43 +82,3 @@ func (db *DB) BatchTransaction(operations []TxFn) error {
})
}

// UpdateInTransaction updates a record in a transaction
func (db *DB) UpdateInTransaction(table string, id string, updates map[string]interface{}) error {
return db.WithTransaction(func(tx *sql.Tx) error {
// Build the update statement
query := fmt.Sprintf("UPDATE %s SET ", table)
var params []interface{}

i := 0
for field, value := range updates {
if i > 0 {
query += ", "
}
query += field + " = ?"
params = append(params, value)
i++
}

// Add the WHERE clause and updated_at
query += ", updated_at = ? WHERE id = ?"
params = append(params, time.Now(), id)

// Execute the update
result, err := tx.Exec(query, params...)
if err != nil {
return fmt.Errorf("update failed: %w", err)
}

// Check if any rows were affected
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}

if rowsAffected == 0 {
return fmt.Errorf("no rows affected, record with ID %s not found", id)
}

return nil
})
}
1 change: 1 addition & 0 deletions models/mtls.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type CreateClientRequest struct {
Name string `json:"name" binding:"required"`
ValidityDays int `json:"validity_days"` // Default: 730 (2 years)
P12Password string `json:"p12_password" binding:"required"`
LegacyP12 bool `json:"legacy_p12"` // Use legacy encryption for iOS/older device compatibility
}

// UpdateMTLSConfigRequest represents the request to update resource mTLS settings
Expand Down
6 changes: 5 additions & 1 deletion services/cert_generator.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package services

Check failure on line 1 in services/cert_generator.go

View workflow job for this annotation

GitHub Actions / Lint

: # github.com/hhftechnology/middleware-manager/services [github.com/hhftechnology/middleware-manager/services.test]

Check failure on line 1 in services/cert_generator.go

View workflow job for this annotation

GitHub Actions / Lint

: # github.com/hhftechnology/middleware-manager/services [github.com/hhftechnology/middleware-manager/services.test]

import (
"crypto/rand"
Expand Down Expand Up @@ -246,7 +246,11 @@
}

// Generate PKCS#12 (.p12) file
p12Data, err := pkcs12.Modern.Encode(clientKey, clientCert, []*x509.Certificate{caCert}, req.P12Password)
encoder := pkcs12.Modern
if req.LegacyP12 {
encoder = pkcs12.Legacy
}
p12Data, err := encoder.Encode(clientKey, clientCert, []*x509.Certificate{caCert}, req.P12Password)
if err != nil {
return nil, fmt.Errorf("failed to generate PKCS#12: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion services/config_generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,7 @@ func (cg *ConfigGenerator) processMTLSOptions(config *TraefikConfig) error {
// Helper to fetch service names from Traefik API
func (cg *ConfigGenerator) fetchTraefikServiceNames() map[string]string {
serviceMap := make(map[string]string)
client := &http.Client{Timeout: 5 * time.Second}
client := HTTPClientWithTimeout(5 * time.Second)

// Get Traefik API URL from data source config
dsConfig, err := cg.configManager.GetActiveDataSourceConfig()
Expand Down
6 changes: 2 additions & 4 deletions services/config_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func (cm *ConfigManager) EnsureDefaultDataSources(pangolinURL, traefikURL string

// Try to determine if Traefik is available
if cm.config.ActiveDataSource == "pangolin" {
client := &http.Client{Timeout: 2 * time.Second}
client := HTTPClientWithTimeout(2 * time.Second)
traefikConfig := cm.config.DataSources["traefik"]

// Try the Traefik URL
Expand Down Expand Up @@ -282,9 +282,7 @@ func (cm *ConfigManager) UpdateDataSource(name string, config models.DataSourceC

// testDataSourceConnection tests the connection to a data source
func (cm *ConfigManager) testDataSourceConnection(ctx context.Context, config models.DataSourceConfig) error {
client := &http.Client{
Timeout: 5 * time.Second,
}
client := HTTPClientWithTimeout(5 * time.Second)

var url string
switch config.Type {
Expand Down
30 changes: 12 additions & 18 deletions services/config_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,7 @@ func NewConfigProxy(db *database.DB, configManager *ConfigManager, pangolinURL s
db: db,
configManager: configManager,
pangolinURL: pangolinURL,
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
httpClient: HTTPClientWithTimeout(10 * time.Second),
cacheDuration: 5 * time.Second, // Match typical Traefik poll interval
}
}
Expand All @@ -161,29 +159,21 @@ func (cp *ConfigProxy) GetMergedConfig() (*ProxiedTraefikConfig, error) {
defer cp.cacheMutex.RUnlock()
return cp.cache, nil
}
staleCache := cp.cache
cp.cacheMutex.RUnlock()

// Acquire write lock for cache update
cp.cacheMutex.Lock()
defer cp.cacheMutex.Unlock()

// Double-check after acquiring write lock
if cp.cache != nil && time.Now().Before(cp.cacheExpiry) {
return cp.cache, nil
}

// Fetch fresh config from Pangolin
// Fetch fresh config OUTSIDE the lock to avoid blocking readers
config, err := cp.fetchPangolinConfig()
if err != nil {
// Return stale cache on error if available
if cp.cache != nil {
if staleCache != nil {
log.Printf("Warning: Pangolin fetch failed, using stale cache: %v", err)
return cp.cache, nil
return staleCache, nil
}
return nil, fmt.Errorf("failed to fetch Pangolin config: %w", err)
}

// Merge MW-manager additions
// Merge MW-manager additions (no lock needed, operates on local config)
if err := cp.mergeMiddlewareManagerConfig(config); err != nil {
return nil, fmt.Errorf("failed to merge MW-manager config: %w", err)
}
Expand All @@ -197,9 +187,11 @@ func (cp *ConfigProxy) GetMergedConfig() (*ProxiedTraefikConfig, error) {
// Normalize middleware field ordering to match Pangolin's JSON format
cp.normalizeMiddlewareOrder(config)

// Update cache
// Lock only to swap the cache
cp.cacheMutex.Lock()
cp.cache = config
cp.cacheExpiry = time.Now().Add(cp.cacheDuration)
cp.cacheMutex.Unlock()

return config, nil
}
Expand Down Expand Up @@ -245,7 +237,7 @@ func (cp *ConfigProxy) fetchPangolinConfig() (*ProxiedTraefikConfig, error) {
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1*1024*1024)) // 1MB limit for error body
return nil, fmt.Errorf("Pangolin returned status %d: %s", resp.StatusCode, string(body))
}

Expand Down Expand Up @@ -1164,6 +1156,8 @@ func (cp *ConfigProxy) SetPangolinURL(url string) {

// SetCacheDuration updates the cache duration
func (cp *ConfigProxy) SetCacheDuration(duration time.Duration) {
cp.cacheMutex.Lock()
defer cp.cacheMutex.Unlock()
cp.cacheDuration = duration
}

Expand Down
13 changes: 5 additions & 8 deletions services/resource_watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"log"
"net/http"
"strings"
"sync/atomic"
"time"

"github.com/google/uuid"
Expand All @@ -23,7 +24,7 @@ type ResourceWatcher struct {
fetcher ResourceFetcher
configManager *ConfigManager
stopChan chan struct{}
isRunning bool
isRunning atomic.Bool
httpClient *http.Client
}

Expand All @@ -49,18 +50,15 @@ func NewResourceWatcher(db *database.DB, configManager *ConfigManager) (*Resourc
fetcher: fetcher,
configManager: configManager,
stopChan: make(chan struct{}),
isRunning: false,
httpClient: httpClient,
}, nil
}

// Start begins watching for resources
func (rw *ResourceWatcher) Start(interval time.Duration) {
if rw.isRunning {
if !rw.isRunning.CompareAndSwap(false, true) {
return
}

rw.isRunning = true
log.Printf("Resource watcher started, checking every %v", interval)

ticker := time.NewTicker(interval)
Expand Down Expand Up @@ -109,12 +107,11 @@ func (rw *ResourceWatcher) refreshFetcher() error {

// Stop stops the resource watcher
func (rw *ResourceWatcher) Stop() {
if !rw.isRunning {
if !rw.isRunning.CompareAndSwap(true, false) {
return
}

close(rw.stopChan)
rw.isRunning = false
}

// checkResources fetches resources from the configured data source and updates the database
Expand Down
50 changes: 33 additions & 17 deletions services/service_fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,8 @@ type PangolinServiceFetcher struct {
// NewPangolinServiceFetcher creates a new Pangolin API fetcher for services
func NewPangolinServiceFetcher(config models.DataSourceConfig) *PangolinServiceFetcher {
return &PangolinServiceFetcher{
config: config,
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
config: config,
httpClient: GetHTTPClient(),
}
}

Expand Down Expand Up @@ -72,7 +70,7 @@ func (f *PangolinServiceFetcher) FetchServices(ctx context.Context) (*models.Ser
}

// Process response
body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(io.LimitReader(resp.Body, 50*1024*1024)) // 50MB limit
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
Expand Down Expand Up @@ -122,7 +120,11 @@ func (f *PangolinServiceFetcher) FetchServices(ctx context.Context) (*models.Ser
}

// Create new service
configJSON, _ := json.Marshal(serviceConfig)
configJSON, err := json.Marshal(serviceConfig)
if err != nil {
log.Printf("Failed to marshal service config for %s: %v", id, err)
continue
}

newService := models.Service{
ID: id,
Expand Down Expand Up @@ -188,10 +190,8 @@ type TraefikServiceFetcher struct {
// NewTraefikServiceFetcher creates a new Traefik API fetcher for services
func NewTraefikServiceFetcher(config models.DataSourceConfig) *TraefikServiceFetcher {
return &TraefikServiceFetcher{
config: config,
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
config: config,
httpClient: GetHTTPClient(),
}
}

Expand Down Expand Up @@ -313,7 +313,7 @@ func (f *TraefikServiceFetcher) fetchHTTPServices(ctx context.Context, baseURL s
}

// Read and parse response body
body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(io.LimitReader(resp.Body, 50*1024*1024)) // 50MB limit
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
Expand Down Expand Up @@ -392,7 +392,7 @@ func (f *TraefikServiceFetcher) fetchTCPServices(ctx context.Context, baseURL st
}

// Read and parse response body
body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(io.LimitReader(resp.Body, 50*1024*1024)) // 50MB limit
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
Expand Down Expand Up @@ -435,7 +435,11 @@ func (f *TraefikServiceFetcher) fetchTCPServices(ctx context.Context, baseURL st
}

// Create service
configJSON, _ := json.Marshal(config)
configJSON, err := json.Marshal(config)
if err != nil {
log.Printf("Failed to marshal service config for %s: %v", name, err)
continue
}

services = append(services, models.Service{
ID: name,
Expand Down Expand Up @@ -476,7 +480,11 @@ func (f *TraefikServiceFetcher) fetchTCPServices(ctx context.Context, baseURL st
}

// Create service
configJSON, _ := json.Marshal(config)
configJSON, err := json.Marshal(config)
if err != nil {
log.Printf("Failed to marshal service config for %s: %v", name, err)
continue
}

services = append(services, models.Service{
ID: name,
Expand Down Expand Up @@ -522,7 +530,7 @@ func (f *TraefikServiceFetcher) fetchUDPServices(ctx context.Context, baseURL st
}

// Read and parse response body
body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(io.LimitReader(resp.Body, 50*1024*1024)) // 50MB limit
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
Expand Down Expand Up @@ -563,7 +571,11 @@ func (f *TraefikServiceFetcher) fetchUDPServices(ctx context.Context, baseURL st
}

// Create service
configJSON, _ := json.Marshal(config)
configJSON, err := json.Marshal(config)
if err != nil {
log.Printf("Failed to marshal service config for %s: %v", name, err)
continue
}

services = append(services, models.Service{
ID: name,
Expand Down Expand Up @@ -604,7 +616,11 @@ func (f *TraefikServiceFetcher) fetchUDPServices(ctx context.Context, baseURL st
}

// Create service
configJSON, _ := json.Marshal(config)
configJSON, err := json.Marshal(config)
if err != nil {
log.Printf("Failed to marshal service config for %s: %v", name, err)
continue
}

services = append(services, models.Service{
ID: name,
Expand Down
Loading
Loading