package main import ( "io" "log" "net/http" "net/url" "strings" "time" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" ) // SecureHeaders sets security-related HTTP headers func SecureHeaders() gin.HandlerFunc { return func(c *gin.Context) { c.Writer.Header().Set("X-Content-Type-Options", "nosniff") c.Next() } } // ValidReqPath checks the X-Req-Path is safe for proxying func ValidReqPath(path string) bool { if !strings.HasPrefix(path, "https://") { return false } if strings.Contains(path, "..") { return false } if strings.TrimSpace(path) == "" { return false } return true } // Extract the root domain (e.g., "example.com" from "sub.example.com") func extractRootDomain(urlStr string) (string, error) { parsedURL, err := url.Parse(urlStr) if err != nil { return "", err } hostname := parsedURL.Hostname() parts := strings.Split(hostname, ".") // Handle special cases with fewer than 2 parts if len(parts) < 2 { return hostname, nil } // Get the last two parts as the root domain domain := parts[len(parts)-2] + "." + parts[len(parts)-1] return domain, nil } // IsValidBackendURL validates that the constructed URL belongs to the expected backend func IsValidBackendURL(targetURL, backend string) bool { targetApexDomain, err := extractRootDomain(targetURL) if err != nil { return false } backendApexDomain, err := extractRootDomain(targetURL) if err != nil { return false } return targetApexDomain == backendApexDomain } func main() { r := gin.Default() r.Use(SecureHeaders()) // CORS config for /api/req api := r.Group("/api") api.Use(cors.New(cors.Config{ AllowOrigins: []string{"*"}, AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowHeaders: []string{"Origin", "Content-Type", "Authorization", "X-Req-Path"}, ExposeHeaders: []string{"Content-Length"}, AllowCredentials: true, MaxAge: 12 * time.Hour, })) // Secure proxy forward api.Any("/req", func(c *gin.Context) { const backend = "https://deeploy.ml" reqPath := c.GetHeader("X-Req-Path") if !ValidReqPath(reqPath) { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid X-Req-Path"}) return } targetURL := reqPath if !IsValidBackendURL(targetURL, backend) { c.JSON(http.StatusForbidden, gin.H{"error": "Target not allowed"}) return } // Prepare the proxied request req, err := http.NewRequest(c.Request.Method, targetURL, c.Request.Body) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Request creation failed"}) return } req.Header = c.Request.Header.Clone() req.Header.Del("X-Req-Path") // Don't forward custom header client := &http.Client{ Timeout: 60 * time.Second, } resp, err := client.Do(req) if err != nil { log.Printf("Error proxying request: Method=%s URL=%s Headers=%v Error=%v", req.Method, req.URL, req.Header, err) c.JSON(http.StatusBadGateway, gin.H{"error": "Backend request failed."}) return } defer resp.Body.Close() // Copy headers & status from the backend for k, v := range resp.Header { for _, vv := range v { c.Writer.Header().Add(k, vv) } } c.Status(resp.StatusCode) io.Copy(c.Writer, resp.Body) }) // Serve static files from ./frontend, no directory listing r.StaticFS("/assets", gin.Dir("./assets", false)) r.GET("/", func(c *gin.Context) { log.Println("root route") // Load HTML templates from ./frontend folder // Render the template by name r.LoadHTMLFiles("index.html") // Render the template by name c.HTML(http.StatusOK, "index.html", gin.H{}) }) // Route to serve parsed HTML template partials r.GET("/partials", func(c *gin.Context) { // Load HTML templates from ./partials folder r.LoadHTMLGlob("./frontend/partials/*") // Render the template by name c.HTML(http.StatusOK, "index_partial.html", gin.H{}) }) log.Println("Serving on http://localhost:7860") r.Run(":7860") }