mirror of
https://github.com/veops/oneterm.git
synced 2025-09-26 19:31:14 +08:00
perf(web_proxy): convert from dynamic asset-{id} subdomain to fixed webproxy subdomain
This commit is contained in:
@@ -10,7 +10,6 @@ RUN apk add tzdata
|
||||
ENV TZ=Asia/Shanghai
|
||||
ENV TERM=xterm-256color
|
||||
WORKDIR /oneterm
|
||||
COPY --from=0 /oneterm/configs/config.example.yaml ./config.yaml
|
||||
COPY --from=0 /oneterm/internal/i18n/locales ./locales
|
||||
COPY --from=0 /oneterm/build/oneterm .
|
||||
CMD [ "./oneterm","run","-c","./config.yaml"]
|
||||
|
@@ -337,7 +337,7 @@ func (c *WebProxyController) recordWebActivity(session *WebProxySession, req *ht
|
||||
web_proxy.RecordWebActivity(session.SessionId, &gin.Context{Request: req})
|
||||
}
|
||||
|
||||
// extractAssetIDFromHost extracts asset ID from subdomain host
|
||||
// extractAssetIDFromHost is kept for compatibility but deprecated in fixed subdomain approach
|
||||
func (c *WebProxyController) extractAssetIDFromHost(host string) (int, error) {
|
||||
return web_proxy.ExtractAssetIDFromHost(host)
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ func LoggerMiddleware() gin.HandlerFunc {
|
||||
// Skip logging for web proxy requests to reduce noise
|
||||
url := ctx.Request.URL.String()
|
||||
host := ctx.Request.Host
|
||||
if strings.HasPrefix(host, "asset-") {
|
||||
if strings.HasPrefix(host, "webproxy.") {
|
||||
return
|
||||
}
|
||||
|
||||
|
@@ -1,12 +1,12 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
|
||||
"strings"
|
||||
|
||||
"github.com/veops/oneterm/internal/api/controller"
|
||||
"github.com/veops/oneterm/internal/api/docs"
|
||||
"github.com/veops/oneterm/internal/api/middleware"
|
||||
@@ -21,13 +21,13 @@ func SetupRouter(r *gin.Engine) {
|
||||
// Start web session cleanup routine
|
||||
controller.StartSessionCleanupRoutine()
|
||||
|
||||
// Subdomain proxy middleware for asset- subdomains
|
||||
// Fixed webproxy subdomain middleware
|
||||
webProxy := controller.NewWebProxyController()
|
||||
r.Use(func(c *gin.Context) {
|
||||
host := c.Request.Host
|
||||
|
||||
// Check if this is an asset subdomain request
|
||||
if strings.HasPrefix(host, "asset-") {
|
||||
// Check if this is the webproxy subdomain request
|
||||
if strings.HasPrefix(host, "webproxy.") {
|
||||
// Allow API requests to pass through to normal routing
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/api/oneterm/v1/") {
|
||||
c.Next()
|
||||
|
@@ -12,86 +12,6 @@ import (
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// RewriteHTMLContent rewrites HTML content to redirect external links through proxy
|
||||
func RewriteHTMLContent(resp *http.Response, assetID int, scheme, proxyHost string) {
|
||||
if resp.Body == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Remove Content-Encoding to avoid decoding issues
|
||||
resp.Header.Del("Content-Encoding")
|
||||
resp.Header.Del("Content-Length")
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
baseDomain := lo.Ternary(strings.HasPrefix(proxyHost, "asset-"),
|
||||
func() string {
|
||||
parts := strings.SplitN(proxyHost, ".", 2)
|
||||
return lo.Ternary(len(parts) > 1, parts[1], proxyHost)
|
||||
}(),
|
||||
proxyHost)
|
||||
|
||||
content := string(body)
|
||||
|
||||
// Universal URL rewriting patterns - catch ALL external URLs
|
||||
patterns := []struct {
|
||||
pattern string
|
||||
rewrite func(matches []string) string
|
||||
}{
|
||||
// JavaScript location assignments: window.location = "http://example.com/path"
|
||||
{
|
||||
`(window\.location(?:\.href)?\s*=\s*["'])https?://([^/'"]+)(/[^"']*)?["']`,
|
||||
func(matches []string) string {
|
||||
path := lo.Ternary(len(matches) > 3 && matches[3] != "", matches[3], "")
|
||||
return fmt.Sprintf(`%s%s://asset-%d.%s%s"`, matches[1], scheme, assetID, baseDomain, path)
|
||||
},
|
||||
},
|
||||
// Form actions: <form action="http://example.com/path"
|
||||
{
|
||||
`(action\s*=\s*["'])https?://([^/'"]+)(/[^"']*)?["']`,
|
||||
func(matches []string) string {
|
||||
path := lo.Ternary(len(matches) > 3 && matches[3] != "", matches[3], "")
|
||||
return fmt.Sprintf(`%s%s://asset-%d.%s%s"`, matches[1], scheme, assetID, baseDomain, path)
|
||||
},
|
||||
},
|
||||
// Link hrefs: <a href="http://example.com/path"
|
||||
{
|
||||
`(href\s*=\s*["'])https?://([^/'"]+)(/[^"']*)?["']`,
|
||||
func(matches []string) string {
|
||||
path := lo.Ternary(len(matches) > 3 && matches[3] != "", matches[3], "")
|
||||
return fmt.Sprintf(`%s%s://asset-%d.%s%s"`, matches[1], scheme, assetID, baseDomain, path)
|
||||
},
|
||||
},
|
||||
// Static resources: <img src=""> <script src=""> <link href="">
|
||||
{
|
||||
`(src\s*=\s*["'])https?://([^/'"]+)(/[^"']*)?["']`,
|
||||
func(matches []string) string {
|
||||
path := lo.Ternary(len(matches) > 3 && matches[3] != "", matches[3], "")
|
||||
return fmt.Sprintf(`%s%s://asset-%d.%s%s"`, matches[1], scheme, assetID, baseDomain, path)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, p := range patterns {
|
||||
re := regexp.MustCompile(p.pattern)
|
||||
content = re.ReplaceAllStringFunc(content, func(match string) string {
|
||||
matches := re.FindStringSubmatch(match)
|
||||
if len(matches) >= 4 {
|
||||
return p.rewrite(matches)
|
||||
}
|
||||
return match
|
||||
})
|
||||
}
|
||||
|
||||
newBody := bytes.NewReader([]byte(content))
|
||||
resp.Body = io.NopCloser(newBody)
|
||||
resp.ContentLength = int64(len(content))
|
||||
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(content)))
|
||||
}
|
||||
|
||||
// ProcessHTMLResponse processes HTML response for content rewriting and injection
|
||||
func ProcessHTMLResponse(resp *http.Response, assetID int, scheme, proxyHost string, session *WebProxySession) {
|
||||
@@ -99,8 +19,15 @@ func ProcessHTMLResponse(resp *http.Response, assetID int, scheme, proxyHost str
|
||||
return
|
||||
}
|
||||
|
||||
// Check if content is compressed
|
||||
// Check if content is compressed BEFORE removing headers
|
||||
contentEncoding := resp.Header.Get("Content-Encoding")
|
||||
|
||||
// Only log search-related requests for debugging login issue
|
||||
isSearchRequest := strings.Contains(resp.Request.URL.String(), "/s?") || strings.Contains(resp.Request.URL.String(), "search")
|
||||
if isSearchRequest {
|
||||
fmt.Printf("[SEARCH] URL: %s, Status: %d, Content-Type: %s, Content-Encoding: %s\n",
|
||||
resp.Request.URL.String(), resp.StatusCode, resp.Header.Get("Content-Type"), contentEncoding)
|
||||
}
|
||||
|
||||
// Remove Content-Encoding to avoid decoding issues
|
||||
resp.Header.Del("Content-Encoding")
|
||||
@@ -118,6 +45,21 @@ func ProcessHTMLResponse(resp *http.Response, assetID int, scheme, proxyHost str
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
body, err = io.ReadAll(gzipReader)
|
||||
} else if contentEncoding == "br" || contentEncoding == "deflate" {
|
||||
// For br/deflate, we need to decompress but don't have the library
|
||||
// For now, keep the headers and let the browser handle decompression
|
||||
resp.Header.Set("Content-Encoding", contentEncoding)
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
// Skip HTML processing for compressed content we can't decompress
|
||||
if isSearchRequest {
|
||||
fmt.Printf("[SEARCH] Skipping HTML processing due to %s encoding\n", contentEncoding)
|
||||
}
|
||||
// Set response without HTML processing
|
||||
newBody := bytes.NewReader(body)
|
||||
resp.Body = io.NopCloser(newBody)
|
||||
resp.ContentLength = int64(len(body))
|
||||
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(body)))
|
||||
return
|
||||
} else {
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
}
|
||||
@@ -128,10 +70,17 @@ func ProcessHTMLResponse(resp *http.Response, assetID int, scheme, proxyHost str
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
// Preserve original encoding - convert bytes to string properly
|
||||
content := string(body)
|
||||
|
||||
// Log search content processing for debugging garbled text
|
||||
if isSearchRequest {
|
||||
fmt.Printf("[SEARCH] Body length: %d, Content preview: %.100s...\n",
|
||||
len(body), strings.ReplaceAll(string(body[:min(100, len(body))]), "\n", "\\n"))
|
||||
}
|
||||
|
||||
// URL rewriting for external links
|
||||
baseDomain := lo.Ternary(strings.HasPrefix(proxyHost, "asset-"),
|
||||
baseDomain := lo.Ternary(strings.HasPrefix(proxyHost, "webproxy."),
|
||||
func() string {
|
||||
parts := strings.SplitN(proxyHost, ".", 2)
|
||||
return lo.Ternary(len(parts) > 1, parts[1], proxyHost)
|
||||
@@ -146,28 +95,39 @@ func ProcessHTMLResponse(resp *http.Response, assetID int, scheme, proxyHost str
|
||||
`(window\.location(?:\.href)?\s*=\s*["'])https?://([^/'"]+)(/[^"']*)?["']`,
|
||||
func(matches []string) string {
|
||||
path := lo.Ternary(len(matches) > 3 && matches[3] != "", matches[3], "")
|
||||
return fmt.Sprintf(`%s%s://asset-%d.%s%s"`, matches[1], scheme, assetID, baseDomain, path)
|
||||
return fmt.Sprintf(`%s%s://webproxy.%s%s"`, matches[1], scheme, baseDomain, path)
|
||||
},
|
||||
},
|
||||
{
|
||||
`(action\s*=\s*["'])https?://([^/'"]+)(/[^"']*)?["']`,
|
||||
func(matches []string) string {
|
||||
path := lo.Ternary(len(matches) > 3 && matches[3] != "", matches[3], "")
|
||||
return fmt.Sprintf(`%s%s://asset-%d.%s%s"`, matches[1], scheme, assetID, baseDomain, path)
|
||||
return fmt.Sprintf(`%s%s://webproxy.%s%s"`, matches[1], scheme, baseDomain, path)
|
||||
},
|
||||
},
|
||||
{
|
||||
`(href\s*=\s*["'])https?://([^/'"]+)(/[^"']*)?["']`,
|
||||
func(matches []string) string {
|
||||
hostname := matches[2]
|
||||
path := lo.Ternary(len(matches) > 3 && matches[3] != "", matches[3], "")
|
||||
return fmt.Sprintf(`%s%s://asset-%d.%s%s"`, matches[1], scheme, assetID, baseDomain, path)
|
||||
},
|
||||
},
|
||||
{
|
||||
`(src\s*=\s*["'])https?://([^/'"]+)(/[^"']*)?["']`,
|
||||
func(matches []string) string {
|
||||
path := lo.Ternary(len(matches) > 3 && matches[3] != "", matches[3], "")
|
||||
return fmt.Sprintf(`%s%s://asset-%d.%s%s"`, matches[1], scheme, assetID, baseDomain, path)
|
||||
|
||||
// Check if hostname belongs to the same domain family (e.g., *.baidu.com)
|
||||
sessionHostParts := strings.Split(session.CurrentHost, ".")
|
||||
hostnameParts := strings.Split(hostname, ".")
|
||||
|
||||
// Compare the last 2 parts for domain matching (e.g., baidu.com)
|
||||
isSameDomain := false
|
||||
if len(sessionHostParts) >= 2 && len(hostnameParts) >= 2 {
|
||||
sessionDomain := strings.Join(sessionHostParts[len(sessionHostParts)-2:], ".")
|
||||
hostDomain := strings.Join(hostnameParts[len(hostnameParts)-2:], ".")
|
||||
isSameDomain = sessionDomain == hostDomain
|
||||
}
|
||||
|
||||
if isSameDomain {
|
||||
return fmt.Sprintf(`%s%s://webproxy.%s%s"`, matches[1], scheme, baseDomain, path)
|
||||
}
|
||||
// Keep external URLs unchanged
|
||||
return matches[0]
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -241,18 +201,18 @@ func ProcessHTMLResponse(resp *http.Response, assetID int, scheme, proxyHost str
|
||||
// Add session management JavaScript (always inject)
|
||||
sessionJS := fmt.Sprintf(`
|
||||
<script>
|
||||
(function() {tbeat', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({session_id: sessionId})
|
||||
}).catch(function() {});
|
||||
}
|
||||
(function() {
|
||||
var sessionId = '%s';
|
||||
var heartbeatInterval;
|
||||
|
||||
// Send heartbeat every 15 seconds
|
||||
function sendHeartbeat() {
|
||||
fetch('/api/oneterm/v1/web_proxy/hear
|
||||
fetch('/api/oneterm/v1/web_proxy/heartbeat', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({session_id: sessionId})
|
||||
}).catch(function() {});
|
||||
}
|
||||
|
||||
// Universal heartbeat mechanism - no complex event handling
|
||||
// The server will handle session cleanup based on heartbeat timeout
|
||||
@@ -260,6 +220,7 @@ func ProcessHTMLResponse(resp *http.Response, assetID int, scheme, proxyHost str
|
||||
|
||||
// Send initial heartbeat immediately
|
||||
sendHeartbeat();
|
||||
|
||||
})();
|
||||
</script>`, session.SessionId)
|
||||
|
||||
@@ -269,7 +230,7 @@ func ProcessHTMLResponse(resp *http.Response, assetID int, scheme, proxyHost str
|
||||
<script>
|
||||
(function() {
|
||||
var originalHost = '%s';
|
||||
var proxyHost = 'asset-%d.%s';
|
||||
var proxyHost = 'webproxy.%s';
|
||||
var proxyScheme = '%s';
|
||||
|
||||
function rewriteUrl(url) {
|
||||
@@ -277,21 +238,38 @@ func ProcessHTMLResponse(resp *http.Response, assetID int, scheme, proxyHost str
|
||||
// Handle absolute URLs
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
var urlObj = new URL(url);
|
||||
// Only rewrite external domains, not our proxy domain
|
||||
if (urlObj.hostname !== window.location.hostname &&
|
||||
urlObj.hostname !== 'localhost' &&
|
||||
urlObj.hostname !== '127.0.0.1') {
|
||||
// Preserve the original path and query, but use proxy hostname
|
||||
var newUrl = proxyScheme + '://' + proxyHost + urlObj.pathname + urlObj.search + urlObj.hash;
|
||||
console.log('Rewriting URL:', url, '->', newUrl);
|
||||
return newUrl;
|
||||
|
||||
// Check if this URL belongs to the same domain we're proxying
|
||||
var isSameDomain = false;
|
||||
|
||||
// Exact match
|
||||
if (urlObj.hostname === originalHost) {
|
||||
isSameDomain = true;
|
||||
}
|
||||
}
|
||||
// Handle relative URLs starting with /
|
||||
else if (url.startsWith('/')) {
|
||||
// Keep relative URLs as-is, they will be relative to current proxy domain
|
||||
// Check if they share the same root domain (e.g., www.baidu.com and baidu.com)
|
||||
else {
|
||||
var getBaseDomain = function(hostname) {
|
||||
var parts = hostname.split('.');
|
||||
if (parts.length >= 2) {
|
||||
return parts.slice(-2).join('.');
|
||||
}
|
||||
return hostname;
|
||||
};
|
||||
|
||||
if (getBaseDomain(urlObj.hostname) === getBaseDomain(originalHost)) {
|
||||
isSameDomain = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isSameDomain) {
|
||||
// Convert to relative URL so it goes through proxy
|
||||
return urlObj.pathname + urlObj.search + urlObj.hash;
|
||||
}
|
||||
|
||||
// For external CDN URLs, let them access directly (most secure for bastion host)
|
||||
return url;
|
||||
}
|
||||
// Handle relative URLs - keep as is
|
||||
return url;
|
||||
} catch (e) {
|
||||
console.warn('URL rewrite error:', e, 'for URL:', url);
|
||||
@@ -426,7 +404,7 @@ func ProcessHTMLResponse(resp *http.Response, assetID int, scheme, proxyHost str
|
||||
}, 1000);
|
||||
}
|
||||
})();
|
||||
</script>`, session.CurrentHost, assetID, baseDomain, scheme, session.Permissions.FileDownload)
|
||||
</script>`, session.CurrentHost, baseDomain, scheme, session.Permissions.FileDownload)
|
||||
|
||||
// Always inject session management and URL interceptor
|
||||
|
||||
|
@@ -130,26 +130,20 @@ func StartWebSession(ctx *gin.Context, req StartWebSessionRequest) (*StartWebSes
|
||||
}
|
||||
StoreSession(sessionId, webSession)
|
||||
|
||||
// Generate subdomain-based proxy URL
|
||||
// Generate fixed webproxy subdomain URL
|
||||
// Use the complete domain for webproxy subdomain
|
||||
baseDomain := strings.Split(ctx.Request.Host, ":")[0]
|
||||
if strings.Contains(baseDomain, ".") {
|
||||
parts := strings.Split(baseDomain, ".")
|
||||
if len(parts) > 2 {
|
||||
baseDomain = strings.Join(parts[1:], ".")
|
||||
}
|
||||
}
|
||||
|
||||
// Determine proxy scheme based on current request only (not asset protocol)
|
||||
scheme := lo.Ternary(ctx.Request.TLS != nil, "https", "http")
|
||||
scheme := lo.Ternary(ctx.GetHeader("X-Forwarded-Proto") == "https", "https", "http")
|
||||
|
||||
portSuffix := ""
|
||||
if strings.Contains(ctx.Request.Host, ":") {
|
||||
portSuffix = ":" + strings.Split(ctx.Request.Host, ":")[1]
|
||||
}
|
||||
|
||||
// Create subdomain URL with session_id for first access (cookie will handle subsequent requests)
|
||||
subdomainHost := fmt.Sprintf("asset-%d.%s%s", req.AssetId, baseDomain, portSuffix)
|
||||
proxyURL := fmt.Sprintf("%s://%s/?session_id=%s", scheme, subdomainHost, sessionId)
|
||||
// Create fixed webproxy URL with asset_id and session_id for first access
|
||||
webproxyHost := fmt.Sprintf("webproxy.%s%s", baseDomain, portSuffix)
|
||||
proxyURL := fmt.Sprintf("%s://%s/?asset_id=%d&session_id=%s", scheme, webproxyHost, req.AssetId, sessionId)
|
||||
|
||||
// Create database session record for history (same as other protocols)
|
||||
currentUser, _ := acl.GetSessionFromCtx(ctx)
|
||||
@@ -257,35 +251,11 @@ func BuildTargetURLWithHost(asset *model.Asset, host string) string {
|
||||
return fmt.Sprintf("%s://%s:%d", protocol, host, port)
|
||||
}
|
||||
|
||||
// ExtractAssetIDFromHost extracts asset ID from subdomain host
|
||||
// ExtractAssetIDFromHost extracts asset ID from query parameter (fixed webproxy subdomain)
|
||||
func ExtractAssetIDFromHost(host string) (int, error) {
|
||||
// Remove port if present
|
||||
hostParts := strings.Split(host, ":")
|
||||
hostname := hostParts[0]
|
||||
|
||||
// Check for asset- prefix
|
||||
if !strings.HasPrefix(hostname, "asset-") {
|
||||
return 0, fmt.Errorf("host does not start with asset- prefix: %s", hostname)
|
||||
}
|
||||
|
||||
// Extract asset ID: asset-123.domain.com -> 123
|
||||
parts := strings.Split(hostname, ".")
|
||||
if len(parts) == 0 {
|
||||
return 0, fmt.Errorf("invalid hostname format: %s", hostname)
|
||||
}
|
||||
|
||||
assetPart := parts[0] // asset-123
|
||||
assetIDStr := strings.TrimPrefix(assetPart, "asset-")
|
||||
if assetIDStr == assetPart {
|
||||
return 0, fmt.Errorf("failed to extract asset ID from: %s", assetPart)
|
||||
}
|
||||
|
||||
assetID, err := strconv.Atoi(assetIDStr)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid asset ID format: %s", assetIDStr)
|
||||
}
|
||||
|
||||
return assetID, nil
|
||||
// This is now handled by ExtractAssetIDFromRequest in the controller
|
||||
// but kept for interface compatibility
|
||||
return 0, fmt.Errorf("asset ID should be extracted from query parameter in fixed subdomain approach")
|
||||
}
|
||||
|
||||
// IsSameDomainOrSubdomain checks if two hosts belong to the same domain or subdomain
|
||||
@@ -412,6 +382,30 @@ func ExtractSessionAndAssetInfo(ctx *gin.Context, extractAssetIDFromHost func(st
|
||||
sessionID = cookie
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Try to get asset_id from existing session first
|
||||
var assetID int
|
||||
var err error
|
||||
|
||||
if sessionID != "" {
|
||||
if session, exists := GetSession(sessionID); exists {
|
||||
assetID = session.AssetId
|
||||
}
|
||||
}
|
||||
|
||||
// If no session or no asset_id from session, get from query parameter
|
||||
if assetID == 0 {
|
||||
assetIDStr := ctx.Query("asset_id")
|
||||
if assetIDStr == "" {
|
||||
return nil, fmt.Errorf("asset_id parameter required")
|
||||
}
|
||||
|
||||
assetID, err = strconv.Atoi(assetIDStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid asset_id format")
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try from redirect parameter (for login redirects)
|
||||
if sessionID == "" {
|
||||
@@ -424,13 +418,7 @@ func ExtractSessionAndAssetInfo(ctx *gin.Context, extractAssetIDFromHost func(st
|
||||
}
|
||||
}
|
||||
|
||||
// Extract asset ID from Host header: asset-11.oneterm.com -> 11
|
||||
assetID, err := extractAssetIDFromHost(host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid subdomain format: %w", err)
|
||||
}
|
||||
|
||||
// Try to get session_id from Referer header as fallback
|
||||
// 3. Try to get session_id from Referer header as fallback
|
||||
if sessionID == "" {
|
||||
referer := ctx.GetHeader("Referer")
|
||||
if referer != "" {
|
||||
@@ -451,7 +439,7 @@ func ExtractSessionAndAssetInfo(ctx *gin.Context, extractAssetIDFromHost func(st
|
||||
}
|
||||
}
|
||||
|
||||
// For static resources, try harder to find session_id
|
||||
// 4. For static resources, try harder to find session_id
|
||||
if sessionID == "" {
|
||||
// Check if this looks like a static resource
|
||||
isStaticResource := strings.Contains(ctx.Request.URL.Path, "/img/") ||
|
||||
@@ -527,7 +515,15 @@ func ValidateSessionAndPermissions(ctx *gin.Context, proxyCtx *ProxyRequestConte
|
||||
|
||||
// Auto-renew cookie for user operations
|
||||
cookieMaxAge := int(model.GlobalConfig.Load().Timeout)
|
||||
ctx.SetCookie("oneterm_session_id", proxyCtx.SessionID, cookieMaxAge, "/", "", false, true)
|
||||
// Set cookie domain for webproxy subdomain
|
||||
cookieDomain := ""
|
||||
if strings.HasPrefix(ctx.Request.Host, "webproxy.") {
|
||||
parts := strings.SplitN(ctx.Request.Host, ".", 2)
|
||||
if len(parts) > 1 {
|
||||
cookieDomain = "." + parts[1] // .domain.com
|
||||
}
|
||||
}
|
||||
ctx.SetCookie("oneterm_session_id", proxyCtx.SessionID, cookieMaxAge, "/", cookieDomain, false, true)
|
||||
}
|
||||
|
||||
// Check Web-specific access controls
|
||||
@@ -552,7 +548,14 @@ func SetupReverseProxy(ctx *gin.Context, proxyCtx *ProxyRequestContext, buildTar
|
||||
return nil, fmt.Errorf("invalid target URL")
|
||||
}
|
||||
|
||||
currentScheme := lo.Ternary(ctx.Request.TLS != nil, "https", "http")
|
||||
// Determine scheme with multiple fallback methods
|
||||
currentScheme := "http"
|
||||
if ctx.GetHeader("X-Forwarded-Proto") == "https" ||
|
||||
ctx.GetHeader("X-Forwarded-Ssl") == "on" ||
|
||||
ctx.GetHeader("X-Url-Scheme") == "https" ||
|
||||
ctx.Request.TLS != nil {
|
||||
currentScheme = "https"
|
||||
}
|
||||
|
||||
// Create transparent reverse proxy
|
||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||
@@ -577,9 +580,15 @@ func SetupReverseProxy(ctx *gin.Context, proxyCtx *ProxyRequestContext, buildTar
|
||||
}
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
q.Del("session_id")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
// Remove session_id from query parameters without re-encoding
|
||||
if req.URL.RawQuery != "" {
|
||||
q := req.URL.Query()
|
||||
if q.Has("session_id") {
|
||||
q.Del("session_id")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
}
|
||||
// Keep original RawQuery if no session_id to remove
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect interception for bastion control
|
||||
@@ -634,7 +643,7 @@ func SetupReverseProxy(ctx *gin.Context, proxyCtx *ProxyRequestContext, buildTar
|
||||
shouldIntercept := redirectURL.IsAbs()
|
||||
|
||||
if shouldIntercept {
|
||||
baseDomain := lo.Ternary(strings.HasPrefix(proxyCtx.Host, "asset-"),
|
||||
baseDomain := lo.Ternary(strings.HasPrefix(proxyCtx.Host, "webproxy."),
|
||||
func() string {
|
||||
parts := strings.SplitN(proxyCtx.Host, ".", 2)
|
||||
return lo.Ternary(len(parts) > 1, parts[1], proxyCtx.Host)
|
||||
@@ -643,14 +652,14 @@ func SetupReverseProxy(ctx *gin.Context, proxyCtx *ProxyRequestContext, buildTar
|
||||
|
||||
if isSameDomainOrSubdomain(target.Host, redirectURL.Host) {
|
||||
UpdateSessionHost(proxyCtx.SessionID, redirectURL.Host)
|
||||
newProxyURL := fmt.Sprintf("%s://asset-%d.%s%s", currentScheme, proxyCtx.AssetID, baseDomain, redirectURL.Path)
|
||||
newProxyURL := fmt.Sprintf("%s://webproxy.%s%s", currentScheme, baseDomain, redirectURL.Path)
|
||||
if redirectURL.RawQuery != "" {
|
||||
newProxyURL += "?" + redirectURL.RawQuery
|
||||
}
|
||||
resp.Header.Set("Location", newProxyURL)
|
||||
} else {
|
||||
newLocation := fmt.Sprintf("%s://asset-%d.%s/external?url=%s",
|
||||
currentScheme, proxyCtx.AssetID, baseDomain, url.QueryEscape(redirectURL.String()))
|
||||
newLocation := fmt.Sprintf("%s://webproxy.%s/external?url=%s",
|
||||
currentScheme, baseDomain, url.QueryEscape(redirectURL.String()))
|
||||
resp.Header.Set("Location", newLocation)
|
||||
}
|
||||
} else {
|
||||
@@ -673,6 +682,7 @@ func SetupReverseProxy(ctx *gin.Context, proxyCtx *ProxyRequestContext, buildTar
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
return proxy, nil
|
||||
}
|
||||
|
@@ -97,28 +97,28 @@ func handler(sess ssh.Session) {
|
||||
|
||||
func signer() ssh.Signer {
|
||||
sysConfigService := service.NewSystemConfigService()
|
||||
|
||||
|
||||
// Retry logic to wait for database table creation
|
||||
var privateKey string
|
||||
var err error
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
|
||||
for i := range 10 {
|
||||
privateKey, err = sysConfigService.EnsureSSHPrivateKey()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
// If table doesn't exist, wait and retry
|
||||
if strings.Contains(err.Error(), "doesn't exist") {
|
||||
logger.L().Info("Waiting for database initialization...", zap.Int("attempt", i+1))
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// Other errors are fatal
|
||||
logger.L().Fatal("failed to ensure SSH private key", zap.Error(err))
|
||||
}
|
||||
|
||||
|
||||
if err != nil {
|
||||
logger.L().Fatal("failed to ensure SSH private key after retries", zap.Error(err))
|
||||
}
|
||||
|
Reference in New Issue
Block a user