File size: 3,970 Bytes
8549d8b
 
 
9ef2fb2
 
 
 
 
 
 
 
 
 
8549d8b
 
9ef2fb2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8549d8b
9ef2fb2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48e528b
 
a7ca764
abadbbd
a7ca764
 
 
 
abadbbd
a7ca764
 
9ef2fb2
 
 
 
 
 
 
 
 
98e2d95
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
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")
}