Spaces:
Running
Running
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") | |
} |