diff --git a/backend/dockerfile b/backend/dockerfile index e123b07..0609f2b 100644 --- a/backend/dockerfile +++ b/backend/dockerfile @@ -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"] diff --git a/backend/internal/api/controller/web_proxy.go b/backend/internal/api/controller/web_proxy.go index 06c4e07..23f0fa5 100644 --- a/backend/internal/api/controller/web_proxy.go +++ b/backend/internal/api/controller/web_proxy.go @@ -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) } diff --git a/backend/internal/api/middleware/logger.go b/backend/internal/api/middleware/logger.go index 43e270b..4d8d70b 100644 --- a/backend/internal/api/middleware/logger.go +++ b/backend/internal/api/middleware/logger.go @@ -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 } diff --git a/backend/internal/api/router/router.go b/backend/internal/api/router/router.go index 3d33ceb..cf320b9 100644 --- a/backend/internal/api/router/router.go +++ b/backend/internal/api/router/router.go @@ -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() diff --git a/backend/internal/service/web_proxy/content.go b/backend/internal/service/web_proxy/content.go index 47e8a06..8fb8d12 100644 --- a/backend/internal/service/web_proxy/content.go +++ b/backend/internal/service/web_proxy/content.go @@ -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:
3 && matches[3] != "", matches[3], "") - return fmt.Sprintf(`%s%s://asset-%d.%s%s"`, matches[1], scheme, assetID, baseDomain, path) - }, - }, - // Link hrefs: 3 && matches[3] != "", matches[3], "") - return fmt.Sprintf(`%s%s://asset-%d.%s%s"`, matches[1], scheme, assetID, baseDomain, path) - }, - }, - // Static resources: `, session.SessionId) @@ -269,7 +230,7 @@ func ProcessHTMLResponse(resp *http.Response, assetID int, scheme, proxyHost str `, session.CurrentHost, assetID, baseDomain, scheme, session.Permissions.FileDownload) + `, session.CurrentHost, baseDomain, scheme, session.Permissions.FileDownload) // Always inject session management and URL interceptor diff --git a/backend/internal/service/web_proxy/service.go b/backend/internal/service/web_proxy/service.go index e17ee6f..9a41fcf 100644 --- a/backend/internal/service/web_proxy/service.go +++ b/backend/internal/service/web_proxy/service.go @@ -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 } diff --git a/backend/internal/sshsrv/handler.go b/backend/internal/sshsrv/handler.go index 1a4bde0..03df701 100644 --- a/backend/internal/sshsrv/handler.go +++ b/backend/internal/sshsrv/handler.go @@ -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)) }