mirror of
https://github.com/veops/oneterm.git
synced 2025-10-14 11:33:54 +08:00
714 lines
23 KiB
Go
714 lines
23 KiB
Go
package web_proxy
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"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) {
|
|
if resp.Body == nil {
|
|
return
|
|
}
|
|
|
|
// Check if content is compressed
|
|
contentEncoding := resp.Header.Get("Content-Encoding")
|
|
|
|
// Remove Content-Encoding to avoid decoding issues
|
|
resp.Header.Del("Content-Encoding")
|
|
resp.Header.Del("Content-Length")
|
|
|
|
var body []byte
|
|
var err error
|
|
|
|
// Handle compressed content
|
|
if contentEncoding == "gzip" {
|
|
gzipReader, err := gzip.NewReader(resp.Body)
|
|
if err != nil {
|
|
resp.Body.Close()
|
|
return
|
|
}
|
|
defer gzipReader.Close()
|
|
body, err = io.ReadAll(gzipReader)
|
|
} else {
|
|
body, err = io.ReadAll(resp.Body)
|
|
}
|
|
|
|
if err != nil {
|
|
resp.Body.Close()
|
|
return
|
|
}
|
|
resp.Body.Close()
|
|
|
|
content := string(body)
|
|
|
|
// URL rewriting for external links
|
|
baseDomain := lo.Ternary(strings.HasPrefix(proxyHost, "asset-"),
|
|
func() string {
|
|
parts := strings.SplitN(proxyHost, ".", 2)
|
|
return lo.Ternary(len(parts) > 1, parts[1], proxyHost)
|
|
}(),
|
|
proxyHost)
|
|
|
|
patterns := []struct {
|
|
pattern string
|
|
rewrite func(matches []string) string
|
|
}{
|
|
{
|
|
`(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)
|
|
},
|
|
},
|
|
{
|
|
`(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)
|
|
},
|
|
},
|
|
{
|
|
`(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)
|
|
},
|
|
},
|
|
{
|
|
`(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
|
|
})
|
|
}
|
|
|
|
// Step 2: Add watermark if enabled
|
|
if session.WebConfig != nil && session.WebConfig.ProxySettings != nil && session.WebConfig.ProxySettings.WatermarkEnabled {
|
|
watermarkCSS := `
|
|
<style>
|
|
.oneterm-watermark-container {
|
|
position: fixed;
|
|
top: -50%;
|
|
left: -50%;
|
|
width: 200%;
|
|
height: 200%;
|
|
z-index: 1;
|
|
pointer-events: none;
|
|
user-select: none;
|
|
transform: rotate(-45deg);
|
|
overflow: hidden;
|
|
}
|
|
.oneterm-watermark-text {
|
|
position: absolute;
|
|
color: rgba(128,128,128,0.08);
|
|
font-size: 32px;
|
|
font-family: Arial, sans-serif;
|
|
font-weight: bold;
|
|
white-space: nowrap;
|
|
}
|
|
</style>`
|
|
|
|
// Generate watermark HTML with multiple OneTerm texts
|
|
var watermarkTexts []string
|
|
for row := 0; row < 30; row++ {
|
|
for col := 0; col < 15; col++ {
|
|
top := row * 100
|
|
left := col * 300
|
|
watermarkTexts = append(watermarkTexts,
|
|
fmt.Sprintf(`<div class="oneterm-watermark-text" style="top: %dpx; left: %dpx;">OneTerm</div>`, top, left))
|
|
}
|
|
}
|
|
|
|
watermarkHTML := fmt.Sprintf(`
|
|
<div class="oneterm-watermark-container">
|
|
%s
|
|
</div>`, strings.Join(watermarkTexts, "\n"))
|
|
|
|
if strings.Contains(content, "</head>") {
|
|
content = strings.Replace(content, "</head>", watermarkCSS+"</head>", 1)
|
|
} else {
|
|
content = watermarkCSS + content
|
|
}
|
|
|
|
if strings.Contains(content, "</body>") {
|
|
content = strings.Replace(content, "</body>", watermarkHTML+"</body>", 1)
|
|
} else {
|
|
content = content + watermarkHTML
|
|
}
|
|
}
|
|
|
|
// 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() {});
|
|
}
|
|
var sessionId = '%s';
|
|
var heartbeatInterval;
|
|
|
|
// Send heartbeat every 15 seconds
|
|
function sendHeartbeat() {
|
|
fetch('/api/oneterm/v1/web_proxy/hear
|
|
|
|
// Universal heartbeat mechanism - no complex event handling
|
|
// The server will handle session cleanup based on heartbeat timeout
|
|
heartbeatInterval = setInterval(sendHeartbeat, 15000);
|
|
|
|
// Send initial heartbeat immediately
|
|
sendHeartbeat();
|
|
})();
|
|
</script>`, session.SessionId)
|
|
|
|
// Add JavaScript URL interceptor for dynamic requests (always inject - moved outside watermark condition)
|
|
|
|
urlInterceptorJS := fmt.Sprintf(`
|
|
<script>
|
|
(function() {
|
|
var originalHost = '%s';
|
|
var proxyHost = 'asset-%d.%s';
|
|
var proxyScheme = '%s';
|
|
|
|
function rewriteUrl(url) {
|
|
try {
|
|
// 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;
|
|
}
|
|
}
|
|
// Handle relative URLs starting with /
|
|
else if (url.startsWith('/')) {
|
|
// Keep relative URLs as-is, they will be relative to current proxy domain
|
|
return url;
|
|
}
|
|
return url;
|
|
} catch (e) {
|
|
console.warn('URL rewrite error:', e, 'for URL:', url);
|
|
return url;
|
|
}
|
|
}
|
|
|
|
// Download control enforcement
|
|
var hasDownloadPermission = %t;
|
|
|
|
// Override fetch API with download control
|
|
if (window.fetch) {
|
|
var originalFetch = window.fetch;
|
|
window.fetch = function(input, init) {
|
|
// Handle both string URLs and Request objects
|
|
if (typeof input === 'string') {
|
|
input = rewriteUrl(input);
|
|
} else if (input && typeof input === 'object' && input.url) {
|
|
// Handle Request object
|
|
var rewrittenUrl = rewriteUrl(input.url);
|
|
if (rewrittenUrl !== input.url) {
|
|
input = new Request(rewrittenUrl, input);
|
|
}
|
|
}
|
|
|
|
// Monitor for potential data export APIs
|
|
if (!hasDownloadPermission) {
|
|
var url = typeof input === 'string' ? input : (input.url || '');
|
|
if (url.includes('/export') || url.includes('/download') ||
|
|
url.includes('/report') || url.includes('/data')) {
|
|
console.warn('Data export API access detected, but download permission denied');
|
|
alert('File download denied: You do not have download permission to access data export APIs');
|
|
return Promise.reject(new Error('Download permission required'));
|
|
}
|
|
}
|
|
|
|
return originalFetch.call(this, input, init);
|
|
};
|
|
}
|
|
|
|
// Override XMLHttpRequest with download control
|
|
if (window.XMLHttpRequest) {
|
|
var OriginalXHR = window.XMLHttpRequest;
|
|
window.XMLHttpRequest = function() {
|
|
var xhr = new OriginalXHR();
|
|
var originalOpen = xhr.open;
|
|
xhr.open = function(method, url, async, user, password) {
|
|
if (typeof url === 'string') {
|
|
url = rewriteUrl(url);
|
|
|
|
// Monitor for potential data export APIs in XHR
|
|
if (!hasDownloadPermission && (
|
|
url.includes('/export') || url.includes('/download') ||
|
|
url.includes('/report') || url.includes('/data'))) {
|
|
console.warn('XHR data export API access detected, download permission denied');
|
|
alert('File download denied: You do not have download permission to access data export APIs');
|
|
throw new Error('Download permission required');
|
|
}
|
|
}
|
|
return originalOpen.call(this, method, url, async, user, password);
|
|
};
|
|
return xhr;
|
|
};
|
|
// Copy static properties
|
|
for (var prop in OriginalXHR) {
|
|
if (OriginalXHR.hasOwnProperty(prop)) {
|
|
window.XMLHttpRequest[prop] = OriginalXHR[prop];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Override window.open for popup windows
|
|
if (window.open) {
|
|
var originalOpen = window.open;
|
|
window.open = function(url, name, specs) {
|
|
if (typeof url === 'string') {
|
|
url = rewriteUrl(url);
|
|
}
|
|
return originalOpen.call(this, url, name, specs);
|
|
};
|
|
}
|
|
|
|
// Client-side download monitoring
|
|
if (!hasDownloadPermission) {
|
|
// Monitor blob URL creation (used by XLSX.js and similar libraries)
|
|
if (window.URL && window.URL.createObjectURL) {
|
|
var originalCreateObjectURL = window.URL.createObjectURL;
|
|
window.URL.createObjectURL = function(blob) {
|
|
console.warn('Blob URL creation detected, download permission denied');
|
|
// Block blob URL creation for file downloads
|
|
if (blob && blob.type && (
|
|
blob.type.includes('sheet') ||
|
|
blob.type.includes('excel') ||
|
|
blob.type.includes('pdf') ||
|
|
blob.type.includes('zip') ||
|
|
blob.type.includes('octet-stream')
|
|
)) {
|
|
alert('File download denied: You do not have download permission to create file download links');
|
|
throw new Error('File download not permitted');
|
|
}
|
|
return originalCreateObjectURL.call(this, blob);
|
|
};
|
|
}
|
|
|
|
// Monitor file download through anchor elements with download attribute
|
|
document.addEventListener('click', function(e) {
|
|
if (e.target && e.target.tagName === 'A' && e.target.hasAttribute('download')) {
|
|
console.warn('Direct file download attempt detected, download permission denied');
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
alert('File download denied: You do not have download permission');
|
|
return false;
|
|
}
|
|
}, true);
|
|
|
|
// Monitor common file export libraries
|
|
setTimeout(function() {
|
|
// Block XLSX library
|
|
if (window.XLSX && window.XLSX.writeFile) {
|
|
window.XLSX.writeFile = function() {
|
|
alert('File export denied: You do not have download permission to export Excel files');
|
|
throw new Error('Excel export not permitted');
|
|
};
|
|
}
|
|
// Block FileSaver.js
|
|
if (window.saveAs) {
|
|
window.saveAs = function() {
|
|
alert('File save denied: You do not have download permission to save files');
|
|
throw new Error('File save not permitted');
|
|
};
|
|
}
|
|
}, 1000);
|
|
}
|
|
})();
|
|
</script>`, session.CurrentHost, assetID, baseDomain, scheme, session.Permissions.FileDownload)
|
|
|
|
// Always inject session management and URL interceptor
|
|
|
|
if strings.Contains(content, "</body>") {
|
|
content = strings.Replace(content, "</body>", sessionJS+urlInterceptorJS+"</body>", 1)
|
|
} else {
|
|
content = content + sessionJS + urlInterceptorJS
|
|
}
|
|
|
|
// Step 3: Record activity if enabled
|
|
if session.WebConfig != nil && session.WebConfig.ProxySettings != nil && session.WebConfig.ProxySettings.RecordingEnabled {
|
|
// Activity recording is handled elsewhere to avoid accessing ctx.Request here
|
|
}
|
|
|
|
// Update response
|
|
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)))
|
|
}
|
|
|
|
// RenderExternalRedirectPage renders the page shown when external redirect is blocked
|
|
func RenderExternalRedirectPage(targetURL string) string {
|
|
return fmt.Sprintf(`<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>External Redirect Blocked - OneTerm</title>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%%, #764ba2 100%%);
|
|
min-height: 100vh;
|
|
margin: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.container {
|
|
background: white;
|
|
padding: 40px;
|
|
border-radius: 12px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
}
|
|
.blocked { color: #e74c3c; }
|
|
.info { color: #666; margin: 20px 0; }
|
|
.target {
|
|
background: #f8f9fa;
|
|
padding: 10px;
|
|
border-radius: 4px;
|
|
word-break: break-all;
|
|
font-family: monospace;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1 class="blocked">🛡️ External Redirect Blocked</h1>
|
|
<div class="info">
|
|
The target website attempted to redirect you to an external domain,
|
|
which has been blocked by the bastion host for security reasons.
|
|
</div>
|
|
<div class="info"><strong>Target URL:</strong></div>
|
|
<div class="target">%s</div>
|
|
<div class="info">
|
|
All web access must go through the bastion host to maintain security
|
|
and audit compliance. External redirects are not permitted.
|
|
</div>
|
|
<div class="info">
|
|
<a href="javascript:history.back()">← Go Back</a>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>`, targetURL)
|
|
}
|
|
|
|
// RenderErrorPage renders a general error page for web proxy errors
|
|
func RenderErrorPage(errorType, title, reason, details string) string {
|
|
var bgColor, iconEmoji string
|
|
|
|
switch errorType {
|
|
case "access_denied":
|
|
bgColor = "#ff6b6b 0%, #ee5a52 100%"
|
|
iconEmoji = "🚫"
|
|
case "session_expired":
|
|
bgColor = "#f39c12 0%, #e67e22 100%"
|
|
iconEmoji = "⏰"
|
|
case "connection_error":
|
|
bgColor = "#95a5a6 0%, #7f8c8d 100%"
|
|
iconEmoji = "🔌"
|
|
case "server_error":
|
|
bgColor = "#8e44ad 0%, #9b59b6 100%"
|
|
iconEmoji = "⚠️"
|
|
case "concurrent_limit":
|
|
bgColor = "#e74c3c 0%, #c0392b 100%"
|
|
iconEmoji = "🚦"
|
|
default:
|
|
bgColor = "#34495e 0%, #2c3e50 100%"
|
|
iconEmoji = "❌"
|
|
}
|
|
|
|
detailsHtml := ""
|
|
if details != "" {
|
|
detailsHtml = fmt.Sprintf(`
|
|
<div class="info"><strong>Details:</strong></div>
|
|
<div class="details">%s</div>`, details)
|
|
}
|
|
|
|
return fmt.Sprintf(`<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>%s - OneTerm</title>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: linear-gradient(135deg, %s);
|
|
min-height: 100vh;
|
|
margin: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.container {
|
|
background: white;
|
|
padding: 40px;
|
|
border-radius: 12px;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
max-width: 600px;
|
|
text-align: center;
|
|
}
|
|
.error-title { color: #e74c3c; font-size: 2em; margin-bottom: 20px; }
|
|
.info { color: #666; margin: 20px 0; text-align: left; }
|
|
.details {
|
|
background: #f8f9fa;
|
|
padding: 15px;
|
|
border-radius: 4px;
|
|
border-left: 4px solid #e74c3c;
|
|
font-family: monospace;
|
|
font-size: 14px;
|
|
text-align: left;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
.action {
|
|
background: #e8f5e8;
|
|
padding: 15px;
|
|
border-radius: 4px;
|
|
border-left: 4px solid #27ae60;
|
|
margin-top: 20px;
|
|
text-align: center;
|
|
}
|
|
.back-link {
|
|
color: #3498db;
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
margin: 0 10px;
|
|
}
|
|
.back-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
.reason {
|
|
background: #fff3cd;
|
|
color: #856404;
|
|
padding: 15px;
|
|
border-radius: 4px;
|
|
border-left: 4px solid #ffc107;
|
|
margin: 20px 0;
|
|
text-align: left;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1 class="error-title">%s %s</h1>
|
|
<div class="reason">%s</div>
|
|
%s
|
|
<div class="action">
|
|
<a href="javascript:history.back()" class="back-link">← Go Back</a>
|
|
<a href="javascript:location.reload()" class="back-link">🔄 Refresh</a>
|
|
<a href="/" class="back-link">🏠 Home</a>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>`, title, bgColor, iconEmoji, title, reason, detailsHtml)
|
|
}
|
|
|
|
// RenderAccessDeniedPage renders the page shown when access is denied (download, read-only, etc.)
|
|
func RenderAccessDeniedPage(reason, details string) string {
|
|
return RenderErrorPage("access_denied", "Access Denied", reason, details)
|
|
}
|
|
|
|
// RenderSessionExpiredPage renders the page shown when session has expired
|
|
func RenderSessionExpiredPage(reason string) string {
|
|
return RenderErrorPage("session_expired", "Session Expired", reason, "")
|
|
}
|
|
|
|
// RenderConcurrentLimitPage renders the page when concurrent limit is exceeded
|
|
func RenderConcurrentLimitPage(maxConcurrent int) string {
|
|
reason := fmt.Sprintf("Maximum concurrent connections (%d) exceeded", maxConcurrent)
|
|
details := "Please wait for an existing session to end, or contact your administrator to increase the limit."
|
|
return RenderErrorPage("concurrent_limit", "Connection Limit Exceeded", reason, details)
|
|
}
|
|
|
|
// RenderServerErrorPage renders the page for server errors
|
|
func RenderServerErrorPage(reason, details string) string {
|
|
return RenderErrorPage("server_error", "Server Error", reason, details)
|
|
}
|
|
|
|
// RenderConnectionErrorPage renders the page for connection errors
|
|
func RenderConnectionErrorPage(reason, details string) string {
|
|
return RenderErrorPage("connection_error", "Connection Error", reason, details)
|
|
}
|
|
|
|
// Legacy function - keeping the original style for compatibility
|
|
func RenderSessionExpiredPageOld(reason string) string {
|
|
return fmt.Sprintf(`<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Session Expired - OneTerm</title>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<style>
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%%, #764ba2 100%%);
|
|
min-height: 100vh;
|
|
margin: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.container {
|
|
background: white;
|
|
padding: 40px;
|
|
border-radius: 12px;
|
|
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
|
|
text-align: center;
|
|
width: 100%%;
|
|
max-width: 400px;
|
|
}
|
|
.icon { font-size: 4rem; margin-bottom: 20px; display: block; }
|
|
.title { color: #333; font-size: 1.5rem; font-weight: 600; margin-bottom: 16px; }
|
|
.message { color: #666; font-size: 1rem; line-height: 1.5; margin-bottom: 24px; }
|
|
.reason {
|
|
background: #f8f9fa;
|
|
border-left: 4px solid #ffa726;
|
|
padding: 12px 16px;
|
|
margin: 20px 0;
|
|
font-size: 0.9rem;
|
|
color: #555;
|
|
border-radius: 4px;
|
|
}
|
|
.button {
|
|
background: #667eea;
|
|
color: white;
|
|
padding: 12px 24px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-size: 1rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
display: inline-block;
|
|
transition: all 0.2s;
|
|
}
|
|
.button:hover { background: #5a6fd8; transform: translateY(-1px); }
|
|
.footer { margin-top: 24px; font-size: 0.8rem; color: #999; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<span class="icon">⏰</span>
|
|
<div class="title">Session Expired</div>
|
|
<div class="message">Your web proxy session has expired and you need to reconnect.</div>
|
|
<div class="reason">Reason: %s</div>
|
|
<a href="javascript:history.back()" class="button">← Go Back</a>
|
|
<div class="footer">OneTerm Bastion Host</div>
|
|
</div>
|
|
</body>
|
|
</html>`, reason)
|
|
}
|