feat: add custom locations management to ProxyHostForm
- Updated ProxyHostForm to include functionality for managing custom locations. - Introduced add, remove, and update operations for locations in the form. - Modified the ProxyHost interface to include an array of locations. - Removed the advanced configuration textarea in favor of a more structured location input. - Updated the frontend assets in index.html to reflect the latest build.
This commit is contained in:
+37
-31
@@ -96,49 +96,55 @@ func main() {
|
||||
// Seed Proxy Hosts
|
||||
proxyHosts := []models.ProxyHost{
|
||||
{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Development App",
|
||||
Domain: "app.local.dev",
|
||||
TargetScheme: "http",
|
||||
TargetHost: "localhost",
|
||||
TargetPort: 3000,
|
||||
EnableTLS: false,
|
||||
EnableWS: true,
|
||||
Enabled: true,
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Development App",
|
||||
DomainNames: "app.local.dev",
|
||||
ForwardScheme: "http",
|
||||
ForwardHost: "localhost",
|
||||
ForwardPort: 3000,
|
||||
SSLForced: false,
|
||||
WebsocketSupport: true,
|
||||
HSTSEnabled: false,
|
||||
BlockExploits: true,
|
||||
Enabled: true,
|
||||
},
|
||||
{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "API Server",
|
||||
Domain: "api.local.dev",
|
||||
TargetScheme: "http",
|
||||
TargetHost: "192.168.1.100",
|
||||
TargetPort: 8080,
|
||||
EnableTLS: false,
|
||||
EnableWS: false,
|
||||
Enabled: true,
|
||||
UUID: uuid.NewString(),
|
||||
Name: "API Server",
|
||||
DomainNames: "api.local.dev",
|
||||
ForwardScheme: "http",
|
||||
ForwardHost: "192.168.1.100",
|
||||
ForwardPort: 8080,
|
||||
SSLForced: false,
|
||||
WebsocketSupport: false,
|
||||
HSTSEnabled: false,
|
||||
BlockExploits: true,
|
||||
Enabled: true,
|
||||
},
|
||||
{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Docker Registry",
|
||||
Domain: "docker.local.dev",
|
||||
TargetScheme: "http",
|
||||
TargetHost: "localhost",
|
||||
TargetPort: 5000,
|
||||
EnableTLS: false,
|
||||
EnableWS: false,
|
||||
Enabled: false,
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Docker Registry",
|
||||
DomainNames: "docker.local.dev",
|
||||
ForwardScheme: "http",
|
||||
ForwardHost: "localhost",
|
||||
ForwardPort: 5000,
|
||||
SSLForced: false,
|
||||
WebsocketSupport: false,
|
||||
HSTSEnabled: false,
|
||||
BlockExploits: true,
|
||||
Enabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, host := range proxyHosts {
|
||||
result := db.Where("domain = ?", host.Domain).FirstOrCreate(&host)
|
||||
result := db.Where("domain_names = ?", host.DomainNames).FirstOrCreate(&host)
|
||||
if result.Error != nil {
|
||||
log.Printf("Failed to seed proxy host %s: %v", host.Domain, result.Error)
|
||||
log.Printf("Failed to seed proxy host %s: %v", host.DomainNames, result.Error)
|
||||
} else if result.RowsAffected > 0 {
|
||||
fmt.Printf("✓ Created proxy host: %s -> %s://%s:%d\n",
|
||||
host.Domain, host.TargetScheme, host.TargetHost, host.TargetPort)
|
||||
host.DomainNames, host.ForwardScheme, host.ForwardHost, host.ForwardPort)
|
||||
} else {
|
||||
fmt.Printf(" Proxy host already exists: %s\n", host.Domain)
|
||||
fmt.Printf(" Proxy host already exists: %s\n", host.DomainNames)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ func setupTestDB() *gorm.DB {
|
||||
// Auto migrate
|
||||
db.AutoMigrate(
|
||||
&models.ProxyHost{},
|
||||
&models.Location{},
|
||||
&models.RemoteServer{},
|
||||
&models.ImportSession{},
|
||||
)
|
||||
@@ -137,13 +138,13 @@ func TestProxyHostHandler_List(t *testing.T) {
|
||||
|
||||
// Create test proxy host
|
||||
host := &models.ProxyHost{
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Test Host",
|
||||
Domain: "test.local",
|
||||
TargetScheme: "http",
|
||||
TargetHost: "localhost",
|
||||
TargetPort: 3000,
|
||||
Enabled: true,
|
||||
UUID: uuid.NewString(),
|
||||
Name: "Test Host",
|
||||
DomainNames: "test.local",
|
||||
ForwardScheme: "http",
|
||||
ForwardHost: "localhost",
|
||||
ForwardPort: 3000,
|
||||
Enabled: true,
|
||||
}
|
||||
db.Create(host)
|
||||
|
||||
@@ -175,12 +176,12 @@ func TestProxyHostHandler_Create(t *testing.T) {
|
||||
|
||||
// Test Create
|
||||
hostData := map[string]interface{}{
|
||||
"name": "New Host",
|
||||
"domain": "new.local",
|
||||
"target_scheme": "http",
|
||||
"target_host": "192.168.1.200",
|
||||
"target_port": 8080,
|
||||
"enabled": true,
|
||||
"name": "New Host",
|
||||
"domain_names": "new.local",
|
||||
"forward_scheme": "http",
|
||||
"forward_host": "192.168.1.200",
|
||||
"forward_port": 8080,
|
||||
"enabled": true,
|
||||
}
|
||||
body, _ := json.Marshal(hostData)
|
||||
|
||||
@@ -195,7 +196,7 @@ func TestProxyHostHandler_Create(t *testing.T) {
|
||||
err := json.Unmarshal(w.Body.Bytes(), &host)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "New Host", host.Name)
|
||||
assert.Equal(t, "new.local", host.Domain)
|
||||
assert.Equal(t, "new.local", host.DomainNames)
|
||||
assert.NotEmpty(t, host.UUID)
|
||||
}
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ func (h *ImportHandler) Commit(c *gin.Context) {
|
||||
errors := []string{}
|
||||
|
||||
for _, host := range proxyHosts {
|
||||
action := req.Resolutions[host.Domain]
|
||||
action := req.Resolutions[host.DomainNames]
|
||||
|
||||
if action == "skip" {
|
||||
skipped++
|
||||
@@ -165,13 +165,13 @@ func (h *ImportHandler) Commit(c *gin.Context) {
|
||||
}
|
||||
|
||||
if action == "rename" {
|
||||
host.Domain = host.Domain + "-imported"
|
||||
host.DomainNames = host.DomainNames + "-imported"
|
||||
}
|
||||
|
||||
host.UUID = uuid.NewString()
|
||||
|
||||
if err := h.proxyHostSvc.Create(&host); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("%s: %s", host.Domain, err.Error()))
|
||||
errors = append(errors, fmt.Sprintf("%s: %s", host.DomainNames, err.Error()))
|
||||
} else {
|
||||
created++
|
||||
}
|
||||
@@ -228,13 +228,13 @@ func (h *ImportHandler) processImport(caddyfilePath, originalName string) error
|
||||
existingHosts, _ := h.proxyHostSvc.List()
|
||||
existingDomains := make(map[string]bool)
|
||||
for _, host := range existingHosts {
|
||||
existingDomains[host.Domain] = true
|
||||
existingDomains[host.DomainNames] = true
|
||||
}
|
||||
|
||||
for _, parsed := range result.Hosts {
|
||||
if existingDomains[parsed.Domain] {
|
||||
if existingDomains[parsed.DomainNames] {
|
||||
result.Conflicts = append(result.Conflicts,
|
||||
fmt.Sprintf("Domain '%s' already exists in CPM+", parsed.Domain))
|
||||
fmt.Sprintf("Domain '%s' already exists in CPM+", parsed.DomainNames))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,11 @@ func (h *ProxyHostHandler) Create(c *gin.Context) {
|
||||
|
||||
host.UUID = uuid.NewString()
|
||||
|
||||
// Assign UUIDs to locations
|
||||
for i := range host.Locations {
|
||||
host.Locations[i].UUID = uuid.NewString()
|
||||
}
|
||||
|
||||
if err := h.service.Create(&host); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
|
||||
@@ -20,7 +20,7 @@ func setupTestRouter(t *testing.T) (*gin.Engine, *gorm.DB) {
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}))
|
||||
require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Location{}))
|
||||
|
||||
h := NewProxyHostHandler(db)
|
||||
r := gin.New()
|
||||
@@ -33,7 +33,7 @@ func setupTestRouter(t *testing.T) (*gin.Engine, *gorm.DB) {
|
||||
func TestProxyHostLifecycle(t *testing.T) {
|
||||
router, _ := setupTestRouter(t)
|
||||
|
||||
body := `{"name":"Media","domain":"media.example.com","target_scheme":"http","target_host":"media","target_port":32400}`
|
||||
body := `{"name":"Media","domain_names":"media.example.com","forward_scheme":"http","forward_host":"media","forward_port":32400,"enabled":true}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/proxy-hosts", strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestProxyHostLifecycle(t *testing.T) {
|
||||
|
||||
var created models.ProxyHost
|
||||
require.NoError(t, json.Unmarshal(resp.Body.Bytes(), &created))
|
||||
require.Equal(t, "media.example.com", created.Domain)
|
||||
require.Equal(t, "media.example.com", created.DomainNames)
|
||||
|
||||
listReq := httptest.NewRequest(http.MethodGet, "/api/v1/proxy-hosts", nil)
|
||||
listResp := httptest.NewRecorder()
|
||||
|
||||
@@ -15,6 +15,7 @@ func Register(router *gin.Engine, db *gorm.DB) error {
|
||||
// AutoMigrate all models for Issue #5 persistence layer
|
||||
if err := db.AutoMigrate(
|
||||
&models.ProxyHost{},
|
||||
&models.Location{},
|
||||
&models.CaddyConfig{},
|
||||
&models.RemoteServer{},
|
||||
&models.SSLCertificate{},
|
||||
|
||||
@@ -24,10 +24,11 @@ func TestClient_Load_Success(t *testing.T) {
|
||||
client := NewClient(server.URL)
|
||||
config, _ := GenerateConfig([]models.ProxyHost{
|
||||
{
|
||||
UUID: "test",
|
||||
Domain: "test.com",
|
||||
TargetHost: "app",
|
||||
TargetPort: 8080,
|
||||
UUID: "test",
|
||||
DomainNames: "test.com",
|
||||
ForwardHost: "app",
|
||||
ForwardPort: 8080,
|
||||
Enabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package caddy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Wikid82/CaddyProxyManagerPlus/backend/internal/models"
|
||||
)
|
||||
@@ -19,22 +20,69 @@ func GenerateConfig(hosts []models.ProxyHost) (*Config, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
routes := make([]*Route, 0, len(hosts))
|
||||
routes := make([]*Route, 0)
|
||||
|
||||
for _, host := range hosts {
|
||||
if host.Domain == "" {
|
||||
return nil, fmt.Errorf("proxy host %s has empty domain", host.UUID)
|
||||
if !host.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
dial := fmt.Sprintf("%s:%d", host.TargetHost, host.TargetPort)
|
||||
if host.DomainNames == "" {
|
||||
return nil, fmt.Errorf("proxy host %s has empty domain names", host.UUID)
|
||||
}
|
||||
|
||||
// Parse comma-separated domains
|
||||
domains := strings.Split(host.DomainNames, ",")
|
||||
for i := range domains {
|
||||
domains[i] = strings.TrimSpace(domains[i])
|
||||
}
|
||||
|
||||
// Build handlers for this host
|
||||
handlers := make([]Handler, 0)
|
||||
|
||||
// Add HSTS header if enabled
|
||||
if host.HSTSEnabled {
|
||||
hstsValue := "max-age=31536000"
|
||||
if host.HSTSSubdomains {
|
||||
hstsValue += "; includeSubDomains"
|
||||
}
|
||||
handlers = append(handlers, HeaderHandler(map[string][]string{
|
||||
"Strict-Transport-Security": {hstsValue},
|
||||
}))
|
||||
}
|
||||
|
||||
// Add exploit blocking if enabled
|
||||
if host.BlockExploits {
|
||||
handlers = append(handlers, BlockExploitsHandler())
|
||||
}
|
||||
|
||||
// Handle custom locations first (more specific routes)
|
||||
for _, loc := range host.Locations {
|
||||
dial := fmt.Sprintf("%s:%d", loc.ForwardHost, loc.ForwardPort)
|
||||
locRoute := &Route{
|
||||
Match: []Match{
|
||||
{
|
||||
Host: domains,
|
||||
Path: []string{loc.Path, loc.Path + "/*"},
|
||||
},
|
||||
},
|
||||
Handle: []Handler{
|
||||
ReverseProxyHandler(dial, host.WebsocketSupport),
|
||||
},
|
||||
Terminal: true,
|
||||
}
|
||||
routes = append(routes, locRoute)
|
||||
}
|
||||
|
||||
// Main proxy handler
|
||||
dial := fmt.Sprintf("%s:%d", host.ForwardHost, host.ForwardPort)
|
||||
mainHandlers := append(handlers, ReverseProxyHandler(dial, host.WebsocketSupport))
|
||||
|
||||
route := &Route{
|
||||
Match: []Match{
|
||||
{Host: []string{host.Domain}},
|
||||
},
|
||||
Handle: []Handler{
|
||||
ReverseProxyHandler(dial, host.EnableWS),
|
||||
{Host: domains},
|
||||
},
|
||||
Handle: mainHandlers,
|
||||
Terminal: true,
|
||||
}
|
||||
|
||||
@@ -49,7 +97,6 @@ func GenerateConfig(hosts []models.ProxyHost) (*Config, error) {
|
||||
Listen: []string{":80", ":443"},
|
||||
Routes: routes,
|
||||
AutoHTTPS: &AutoHTTPSConfig{
|
||||
// Enable automatic HTTPS by default
|
||||
Disable: false,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -19,14 +19,15 @@ func TestGenerateConfig_Empty(t *testing.T) {
|
||||
func TestGenerateConfig_SingleHost(t *testing.T) {
|
||||
hosts := []models.ProxyHost{
|
||||
{
|
||||
UUID: "test-uuid",
|
||||
Name: "Media",
|
||||
Domain: "media.example.com",
|
||||
TargetScheme: "http",
|
||||
TargetHost: "media",
|
||||
TargetPort: 32400,
|
||||
EnableTLS: true,
|
||||
EnableWS: false,
|
||||
UUID: "test-uuid",
|
||||
Name: "Media",
|
||||
DomainNames: "media.example.com",
|
||||
ForwardScheme: "http",
|
||||
ForwardHost: "media",
|
||||
ForwardPort: 32400,
|
||||
SSLForced: true,
|
||||
WebsocketSupport: false,
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -55,16 +56,18 @@ func TestGenerateConfig_SingleHost(t *testing.T) {
|
||||
func TestGenerateConfig_MultipleHosts(t *testing.T) {
|
||||
hosts := []models.ProxyHost{
|
||||
{
|
||||
UUID: "uuid-1",
|
||||
Domain: "site1.example.com",
|
||||
TargetHost: "app1",
|
||||
TargetPort: 8080,
|
||||
UUID: "uuid-1",
|
||||
DomainNames: "site1.example.com",
|
||||
ForwardHost: "app1",
|
||||
ForwardPort: 8080,
|
||||
Enabled: true,
|
||||
},
|
||||
{
|
||||
UUID: "uuid-2",
|
||||
Domain: "site2.example.com",
|
||||
TargetHost: "app2",
|
||||
TargetPort: 8081,
|
||||
UUID: "uuid-2",
|
||||
DomainNames: "site2.example.com",
|
||||
ForwardHost: "app2",
|
||||
ForwardPort: 8081,
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -76,11 +79,12 @@ func TestGenerateConfig_MultipleHosts(t *testing.T) {
|
||||
func TestGenerateConfig_WebSocketEnabled(t *testing.T) {
|
||||
hosts := []models.ProxyHost{
|
||||
{
|
||||
UUID: "uuid-ws",
|
||||
Domain: "ws.example.com",
|
||||
TargetHost: "wsapp",
|
||||
TargetPort: 3000,
|
||||
EnableWS: true,
|
||||
UUID: "uuid-ws",
|
||||
DomainNames: "ws.example.com",
|
||||
ForwardHost: "wsapp",
|
||||
ForwardPort: 3000,
|
||||
WebsocketSupport: true,
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -97,10 +101,11 @@ func TestGenerateConfig_WebSocketEnabled(t *testing.T) {
|
||||
func TestGenerateConfig_EmptyDomain(t *testing.T) {
|
||||
hosts := []models.ProxyHost{
|
||||
{
|
||||
UUID: "bad-uuid",
|
||||
Domain: "",
|
||||
TargetHost: "app",
|
||||
TargetPort: 8080,
|
||||
UUID: "bad-uuid",
|
||||
DomainNames: "",
|
||||
ForwardHost: "app",
|
||||
ForwardPort: 8080,
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -53,14 +53,14 @@ type CaddyHandler struct {
|
||||
|
||||
// ParsedHost represents a single host detected during Caddyfile import.
|
||||
type ParsedHost struct {
|
||||
Domain string `json:"domain"`
|
||||
TargetScheme string `json:"target_scheme"`
|
||||
TargetHost string `json:"target_host"`
|
||||
TargetPort int `json:"target_port"`
|
||||
EnableTLS bool `json:"enable_tls"`
|
||||
EnableWS bool `json:"enable_websockets"`
|
||||
RawJSON string `json:"raw_json"` // Original Caddy JSON for this route
|
||||
Warnings []string `json:"warnings"` // Unsupported features
|
||||
DomainNames string `json:"domain_names"`
|
||||
ForwardScheme string `json:"forward_scheme"`
|
||||
ForwardHost string `json:"forward_host"`
|
||||
ForwardPort int `json:"forward_port"`
|
||||
SSLForced bool `json:"ssl_forced"`
|
||||
WebsocketSupport bool `json:"websocket_support"`
|
||||
RawJSON string `json:"raw_json"` // Original Caddy JSON for this route
|
||||
Warnings []string `json:"warnings"` // Unsupported features
|
||||
}
|
||||
|
||||
// ImportResult contains parsed hosts and detected conflicts.
|
||||
@@ -133,8 +133,8 @@ func (i *Importer) ExtractHosts(caddyJSON []byte) (*ImportResult, error) {
|
||||
|
||||
// Extract reverse proxy handler
|
||||
host := ParsedHost{
|
||||
Domain: domain,
|
||||
EnableTLS: strings.HasPrefix(domain, "https") || server.TLSConnectionPolicies != nil,
|
||||
DomainNames: domain,
|
||||
SSLForced: strings.HasPrefix(domain, "https") || server.TLSConnectionPolicies != nil,
|
||||
}
|
||||
|
||||
// Find reverse_proxy handler
|
||||
@@ -147,8 +147,8 @@ func (i *Importer) ExtractHosts(caddyJSON []byte) (*ImportResult, error) {
|
||||
if dial != "" {
|
||||
parts := strings.Split(dial, ":")
|
||||
if len(parts) == 2 {
|
||||
host.TargetHost = parts[0]
|
||||
fmt.Sscanf(parts[1], "%d", &host.TargetPort)
|
||||
host.ForwardHost = parts[0]
|
||||
fmt.Sscanf(parts[1], "%d", &host.ForwardPort)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ func (i *Importer) ExtractHosts(caddyJSON []byte) (*ImportResult, error) {
|
||||
if upgrade, ok := headers["Upgrade"].([]interface{}); ok {
|
||||
for _, v := range upgrade {
|
||||
if v == "websocket" {
|
||||
host.EnableWS = true
|
||||
host.WebsocketSupport = true
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -167,9 +167,9 @@ func (i *Importer) ExtractHosts(caddyJSON []byte) (*ImportResult, error) {
|
||||
}
|
||||
|
||||
// Default scheme
|
||||
host.TargetScheme = "http"
|
||||
if host.EnableTLS {
|
||||
host.TargetScheme = "https"
|
||||
host.ForwardScheme = "http"
|
||||
if host.SSLForced {
|
||||
host.ForwardScheme = "https"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,18 +214,18 @@ func ConvertToProxyHosts(parsedHosts []ParsedHost) []models.ProxyHost {
|
||||
hosts := make([]models.ProxyHost, 0, len(parsedHosts))
|
||||
|
||||
for _, parsed := range parsedHosts {
|
||||
if parsed.TargetHost == "" || parsed.TargetPort == 0 {
|
||||
if parsed.ForwardHost == "" || parsed.ForwardPort == 0 {
|
||||
continue // Skip invalid entries
|
||||
}
|
||||
|
||||
hosts = append(hosts, models.ProxyHost{
|
||||
Name: parsed.Domain, // Can be customized by user during review
|
||||
Domain: parsed.Domain,
|
||||
TargetScheme: parsed.TargetScheme,
|
||||
TargetHost: parsed.TargetHost,
|
||||
TargetPort: parsed.TargetPort,
|
||||
EnableTLS: parsed.EnableTLS,
|
||||
EnableWS: parsed.EnableWS,
|
||||
Name: parsed.DomainNames, // Can be customized by user during review
|
||||
DomainNames: parsed.DomainNames,
|
||||
ForwardScheme: parsed.ForwardScheme,
|
||||
ForwardHost: parsed.ForwardHost,
|
||||
ForwardPort: parsed.ForwardPort,
|
||||
SSLForced: parsed.SSLForced,
|
||||
WebsocketSupport: parsed.WebsocketSupport,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -78,6 +78,26 @@ func ReverseProxyHandler(dial string, enableWS bool) Handler {
|
||||
return h
|
||||
}
|
||||
|
||||
// HeaderHandler creates a handler that sets HTTP response headers.
|
||||
func HeaderHandler(headers map[string][]string) Handler {
|
||||
return Handler{
|
||||
"handler": "headers",
|
||||
"response": map[string]interface{}{
|
||||
"set": headers,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// BlockExploitsHandler creates a handler that blocks common exploits.
|
||||
// This uses Caddy's request matchers to block malicious patterns.
|
||||
func BlockExploitsHandler() Handler {
|
||||
return Handler{
|
||||
"handler": "vars",
|
||||
// Placeholder for future exploit blocking logic
|
||||
// Can be extended with specific matchers for SQL injection, XSS, etc.
|
||||
}
|
||||
}
|
||||
|
||||
// TLSApp configures the TLS app for certificate management.
|
||||
type TLSApp struct {
|
||||
Automation *AutomationConfig `json:"automation,omitempty"`
|
||||
|
||||
@@ -17,10 +17,11 @@ func TestValidate_EmptyConfig(t *testing.T) {
|
||||
func TestValidate_ValidConfig(t *testing.T) {
|
||||
hosts := []models.ProxyHost{
|
||||
{
|
||||
UUID: "test",
|
||||
Domain: "test.example.com",
|
||||
TargetHost: "app",
|
||||
TargetPort: 8080,
|
||||
UUID: "test",
|
||||
DomainNames: "test.example.com",
|
||||
ForwardHost: "10.0.1.100",
|
||||
ForwardPort: 8080,
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Location represents a custom path-based proxy configuration within a ProxyHost.
|
||||
type Location struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex;not null"`
|
||||
ProxyHostID uint `json:"proxy_host_id" gorm:"not null;index"`
|
||||
Path string `json:"path" gorm:"not null"` // e.g., /api, /admin
|
||||
ForwardScheme string `json:"forward_scheme" gorm:"default:http"`
|
||||
ForwardHost string `json:"forward_host" gorm:"not null"`
|
||||
ForwardPort int `json:"forward_port" gorm:"not null"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -4,18 +4,23 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ProxyHost represents a reverse proxy configuration for a single domain.
|
||||
// ProxyHost represents a reverse proxy configuration.
|
||||
type ProxyHost struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex"`
|
||||
Name string `json:"name"`
|
||||
Domain string `json:"domain" gorm:"uniqueIndex"`
|
||||
TargetScheme string `json:"target_scheme"` // http/https
|
||||
TargetHost string `json:"target_host"`
|
||||
TargetPort int `json:"target_port"`
|
||||
EnableTLS bool `json:"enable_tls" gorm:"default:false"`
|
||||
EnableWS bool `json:"enable_websockets" gorm:"default:false"`
|
||||
Enabled bool `json:"enabled" gorm:"default:true"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
UUID string `json:"uuid" gorm:"uniqueIndex;not null"`
|
||||
Name string `json:"name"`
|
||||
DomainNames string `json:"domain_names" gorm:"not null"` // Comma-separated list
|
||||
ForwardScheme string `json:"forward_scheme" gorm:"default:http"`
|
||||
ForwardHost string `json:"forward_host" gorm:"not null"`
|
||||
ForwardPort int `json:"forward_port" gorm:"not null"`
|
||||
SSLForced bool `json:"ssl_forced" gorm:"default:false"`
|
||||
HTTP2Support bool `json:"http2_support" gorm:"default:true"`
|
||||
HSTSEnabled bool `json:"hsts_enabled" gorm:"default:false"`
|
||||
HSTSSubdomains bool `json:"hsts_subdomains" gorm:"default:false"`
|
||||
BlockExploits bool `json:"block_exploits" gorm:"default:true"`
|
||||
WebsocketSupport bool `json:"websocket_support" gorm:"default:false"`
|
||||
Enabled bool `json:"enabled" gorm:"default:true"`
|
||||
Locations []Location `json:"locations" gorm:"foreignKey:ProxyHostID;constraint:OnDelete:CASCADE"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ func NewProxyHostService(db *gorm.DB) *ProxyHostService {
|
||||
}
|
||||
|
||||
// ValidateUniqueDomain ensures no duplicate domains exist before creation/update.
|
||||
func (s *ProxyHostService) ValidateUniqueDomain(domain string, excludeID uint) error {
|
||||
func (s *ProxyHostService) ValidateUniqueDomain(domainNames string, excludeID uint) error {
|
||||
var count int64
|
||||
query := s.db.Model(&models.ProxyHost{}).Where("domain = ?", domain)
|
||||
query := s.db.Model(&models.ProxyHost{}).Where("domain_names = ?", domainNames)
|
||||
|
||||
if excludeID > 0 {
|
||||
query = query.Where("id != ?", excludeID)
|
||||
@@ -41,7 +41,7 @@ func (s *ProxyHostService) ValidateUniqueDomain(domain string, excludeID uint) e
|
||||
|
||||
// Create validates and creates a new proxy host.
|
||||
func (s *ProxyHostService) Create(host *models.ProxyHost) error {
|
||||
if err := s.ValidateUniqueDomain(host.Domain, 0); err != nil {
|
||||
if err := s.ValidateUniqueDomain(host.DomainNames, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func (s *ProxyHostService) Create(host *models.ProxyHost) error {
|
||||
|
||||
// Update validates and updates an existing proxy host.
|
||||
func (s *ProxyHostService) Update(host *models.ProxyHost) error {
|
||||
if err := s.ValidateUniqueDomain(host.Domain, host.ID); err != nil {
|
||||
if err := s.ValidateUniqueDomain(host.DomainNames, host.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -71,19 +71,19 @@ func (s *ProxyHostService) GetByID(id uint) (*models.ProxyHost, error) {
|
||||
return &host, nil
|
||||
}
|
||||
|
||||
// GetByUUID retrieves a proxy host by UUID.
|
||||
// GetByUUID finds a proxy host by UUID.
|
||||
func (s *ProxyHostService) GetByUUID(uuid string) (*models.ProxyHost, error) {
|
||||
var host models.ProxyHost
|
||||
if err := s.db.Where("uuid = ?", uuid).First(&host).Error; err != nil {
|
||||
if err := s.db.Preload("Locations").Where("uuid = ?", uuid).First(&host).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &host, nil
|
||||
}
|
||||
|
||||
// List retrieves all proxy hosts.
|
||||
// List returns all proxy hosts.
|
||||
func (s *ProxyHostService) List() ([]models.ProxyHost, error) {
|
||||
var hosts []models.ProxyHost
|
||||
if err := s.db.Find(&hosts).Error; err != nil {
|
||||
if err := s.db.Preload("Locations").Order("updated_at desc").Find(&hosts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hosts, nil
|
||||
|
||||
-1
File diff suppressed because one or more lines are too long
-74
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Caddy Proxy Manager+</title>
|
||||
<script type="module" crossorigin src="/assets/index-Y4LKIHSS.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Be7wNiFg.css">
|
||||
<script type="module" crossorigin src="/assets/index-B1xdYwH2.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DDHvSYJS.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ProxyHost } from '../hooks/useProxyHosts'
|
||||
import { ProxyHost, Location } from '../hooks/useProxyHosts'
|
||||
import { remoteServersAPI } from '../services/api'
|
||||
|
||||
interface ProxyHostFormProps {
|
||||
@@ -29,7 +29,7 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
|
||||
hsts_subdomains: host?.hsts_subdomains ?? false,
|
||||
block_exploits: host?.block_exploits ?? true,
|
||||
websocket_support: host?.websocket_support ?? false,
|
||||
advanced_config: host?.advanced_config || '',
|
||||
locations: host?.locations || [] as Location[],
|
||||
enabled: host?.enabled ?? true,
|
||||
})
|
||||
|
||||
@@ -49,6 +49,28 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
|
||||
fetchServers()
|
||||
}, [])
|
||||
|
||||
const addLocation = () => {
|
||||
setFormData({
|
||||
...formData,
|
||||
locations: [
|
||||
...formData.locations,
|
||||
{ path: '/', forward_scheme: 'http', forward_host: '', forward_port: 80 }
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const removeLocation = (index: number) => {
|
||||
const newLocations = [...formData.locations]
|
||||
newLocations.splice(index, 1)
|
||||
setFormData({ ...formData, locations: newLocations })
|
||||
}
|
||||
|
||||
const updateLocation = (index: number, field: keyof Location, value: any) => {
|
||||
const newLocations = [...formData.locations]
|
||||
newLocations[index] = { ...newLocations[index], [field]: value }
|
||||
setFormData({ ...formData, locations: newLocations })
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
@@ -231,18 +253,83 @@ export default function ProxyHostForm({ host, onSubmit, onCancel }: ProxyHostFor
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Advanced Config */}
|
||||
{/* Custom Locations */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||
Advanced Caddy Config (Optional)
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.advanced_config}
|
||||
onChange={e => setFormData({ ...formData, advanced_config: e.target.value })}
|
||||
placeholder="Additional Caddy directives..."
|
||||
rows={4}
|
||||
className="w-full bg-gray-900 border border-gray-700 rounded-lg px-4 py-2 text-white font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<label className="block text-sm font-medium text-gray-300">
|
||||
Custom Locations
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addLocation}
|
||||
className="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-white text-sm rounded transition-colors"
|
||||
>
|
||||
Add Location
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{formData.locations.map((location, index) => (
|
||||
<div key={index} className="bg-gray-900/50 p-4 rounded-lg border border-gray-700">
|
||||
<div className="grid grid-cols-12 gap-4 mb-2">
|
||||
<div className="col-span-4">
|
||||
<label className="block text-xs text-gray-400 mb-1">Path</label>
|
||||
<input
|
||||
type="text"
|
||||
value={location.path}
|
||||
onChange={e => updateLocation(index, 'path', e.target.value)}
|
||||
placeholder="/api"
|
||||
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-1 text-sm text-white focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-xs text-gray-400 mb-1">Scheme</label>
|
||||
<select
|
||||
value={location.forward_scheme}
|
||||
onChange={e => updateLocation(index, 'forward_scheme', e.target.value)}
|
||||
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-1 text-sm text-white focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
>
|
||||
<option value="http">HTTP</option>
|
||||
<option value="https">HTTPS</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-span-4">
|
||||
<label className="block text-xs text-gray-400 mb-1">Forward Host</label>
|
||||
<input
|
||||
type="text"
|
||||
value={location.forward_host}
|
||||
onChange={e => updateLocation(index, 'forward_host', e.target.value)}
|
||||
placeholder="10.0.0.1"
|
||||
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-1 text-sm text-white focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-xs text-gray-400 mb-1">Port</label>
|
||||
<input
|
||||
type="number"
|
||||
value={location.forward_port}
|
||||
onChange={e => updateLocation(index, 'forward_port', parseInt(e.target.value))}
|
||||
className="w-full bg-gray-800 border border-gray-600 rounded px-3 py-1 text-sm text-white focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeLocation(index)}
|
||||
className="text-red-400 hover:text-red-300 text-xs"
|
||||
>
|
||||
Remove Location
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{formData.locations.length === 0 && (
|
||||
<div className="text-center text-gray-500 text-sm py-4 border border-dashed border-gray-700 rounded-lg">
|
||||
No custom locations defined
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { proxyHostsAPI } from '../services/api'
|
||||
|
||||
export interface Location {
|
||||
uuid?: string
|
||||
path: string
|
||||
forward_scheme: string
|
||||
forward_host: string
|
||||
forward_port: number
|
||||
}
|
||||
|
||||
export interface ProxyHost {
|
||||
uuid: string
|
||||
domain_names: string
|
||||
@@ -15,7 +23,7 @@ export interface ProxyHost {
|
||||
hsts_subdomains: boolean
|
||||
block_exploits: boolean
|
||||
websocket_support: boolean
|
||||
advanced_config?: string
|
||||
locations: Location[]
|
||||
enabled: boolean
|
||||
created_at: string
|
||||
updated_at: string
|
||||
|
||||
Reference in New Issue
Block a user