Write the HTTP proxy error page to a file

Signed-off-by: Jianhui Zhao <zhaojh329@gmail.com>
This commit is contained in:
Jianhui Zhao
2025-08-10 18:09:47 +08:00
parent 19604debb4
commit 1ef76bd855
10 changed files with 199 additions and 164 deletions
+1
View File
@@ -26,6 +26,7 @@ jobs:
cd ui
npm install
npm run build
mv dist ../assets
cd ..
- name: build
env:
+1
View File
@@ -82,6 +82,7 @@ jobs:
cd ui
npm install
npm run build
mv dist ../assets
cd ..
- name: Build rttys release
+2
View File
@@ -12,3 +12,5 @@
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/
assets/dist
+1 -1
View File
@@ -6,7 +6,7 @@ RUN npm install && npm run build
FROM golang:latest AS rttys
WORKDIR /rttys-build
COPY . .
COPY --from=ui /rttys-ui/dist ui/dist
COPY --from=ui /rttys-ui/dist assets/dist
RUN CGO_ENABLED=0 \
GitCommit=$(git log --pretty=format:"%h" -1) \
BuildTime=$(date +%FT%T%z) \
+1 -5
View File
@@ -6,7 +6,6 @@
package main
import (
"embed"
"io/fs"
"net"
"net/http"
@@ -24,9 +23,6 @@ import (
const httpSessionExpire = 30 * time.Minute
//go:embed ui/dist
var staticFs embed.FS
func (srv *RttyServer) ListenAPI() error {
cfg := &srv.cfg
@@ -34,7 +30,7 @@ func (srv *RttyServer) ListenAPI() error {
r := gin.New()
fs, err := fs.Sub(staticFs, "ui/dist")
fs, err := fs.Sub(staticFs, "assets/dist")
if err != nil {
return err
}
+151
View File
@@ -0,0 +1,151 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RTTY</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #555;
line-height: 1.6;
}
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
text-align: center;
}
.error-icon {
margin-bottom: 2rem;
animation: fadeIn 0.8s ease-in-out;
}
.error-icon svg {
width: 90px;
height: 90px;
fill: #f56565;
}
.error-content {
max-width: 700px;
animation: slideUp 0.8s ease-out 0.2s both;
}
.error-title {
font-size: 1.8rem;
font-weight: 600;
color: #7a8fb0;
margin-bottom: 1rem;
line-height: 1.2;
}
.error-message {
font-size: 1rem;
color: #b6c1d3;
margin-bottom: 2rem;
line-height: 1.6;
text-align: left;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
</head>
<body>
<div class="error-container">
<div class="error-icon">
<svg viewBox="0 0 24 24">
<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
</svg>
</div>
<div class="error-content">
<h2 class="error-title" id="errorTitle"></h2>
<p class="error-message" id="errorMessage"></p>
</div>
</div>
<script>
const translations = {
en: {
'Device Unavailable': 'Device Unavailable',
'Invalid Request': 'Invalid Request',
'Unauthorized Access': 'Unauthorized Access',
'Device offline message': 'The device is currently offline. Please check the device status and try again.',
'Invalid request message': 'The request is invalid or malformed',
'Unauthorized request message': 'You are not authorized to access this resource. Please check your session and try again.'
},
'zh-CN': {
'Device Unavailable': '设备不可用',
'Invalid Request': '无效请求',
'Unauthorized Access': '未授权访问',
'Device offline message': '设备当前离线请检查设备状态后重试',
'Invalid request message': '请求无效或格式错误',
'Unauthorized request message': '您无权访问此资源请检查您的会话并重试'
}
};
function t(key, lang) {
return translations[lang][key] || translations.en[key] || key;
}
function updateContent() {
const errorType = '{{.}}';
const lang = navigator.language === 'zh-CN' ? 'zh-CN' : 'en';
let title = '', message = '';
switch (errorType) {
case 'offline':
title = t('Device Unavailable', lang);
message = t('Device offline message', lang);
break;
case 'invalid':
title = t('Invalid Request', lang);
message = t('Invalid request message', lang);
break;
case 'unauthorized':
title = t('Unauthorized Access', lang);
message = t('Unauthorized request message', lang);
break;
}
document.getElementById('errorTitle').textContent = title;
document.getElementById('errorMessage').textContent = message;
// Update page title
if (title) {
document.title = title + ' - RTTY';
} else {
document.title = 'Error - RTTY';
}
}
// Initialize page on load
document.addEventListener('DOMContentLoaded', updateContent);
</script>
</body>
</html>
+5
View File
@@ -21,6 +21,11 @@ ARCH="$1" # Pass architecture as an argument, e.g., amd64 or arm64
exit 1;
}
[ -d assets/dist ] || {
echo "Please build ui first"
exit 1
}
# Build directory
BUILD_DIR="build-deb"
INSTALL_DIR="$BUILD_DIR/usr"
+5
View File
@@ -10,6 +10,11 @@ BuildTime=$(date +%FT%T%z)
exit 1
}
[ -d assets/dist ] || {
echo "Please build ui first"
exit 1
}
generate() {
local os="$1"
local arch="$2"
+11
View File
@@ -0,0 +1,11 @@
/* SPDX-License-Identifier: MIT */
/*
* Author: Jianhui Zhao <zhaojh329@gmail.com>
*/
package main
import "embed"
//go:embed assets
var staticFs embed.FS
+21 -158
View File
@@ -7,10 +7,13 @@ package main
import (
"bufio"
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"io/fs"
"net"
"net/http"
"net/url"
@@ -373,170 +376,30 @@ func (rw *HttpProxyWriter) WriteRequest(req *http.Request) {
req.Write(rw)
}
func generateErrorHTML(errorType string) string {
return fmt.Sprintf(
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RTTY</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: #555;
line-height: 1.6;
}
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
text-align: center;
}
.error-icon {
margin-bottom: 2rem;
animation: fadeIn 0.8s ease-in-out;
}
.error-icon svg {
width: 90px;
height: 90px;
fill: #f56565;
}
.error-content {
max-width: 700px;
animation: slideUp 0.8s ease-out 0.2s both;
}
.error-title {
font-size: 1.8rem;
font-weight: 600;
color: #7a8fb0;
margin-bottom: 1rem;
line-height: 1.2;
}
.error-message {
font-size: 1rem;
color: #b6c1d3;
margin-bottom: 2rem;
line-height: 1.6;
text-align: left;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
</head>
<body>
<div class="error-container">
<div class="error-icon">
<svg viewBox="0 0 24 24">
<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
</svg>
</div>
<div class="error-content">
<h2 class="error-title" id="errorTitle"></h2>
<p class="error-message" id="errorMessage"></p>
</div>
</div>
<script>
const translations = {
en: {
'Device Unavailable': 'Device Unavailable',
'Invalid Request': 'Invalid Request',
'Unauthorized Access': 'Unauthorized Access',
'Device offline message': 'The device is currently offline. Please check the device status and try again.',
'Invalid request message': 'The request is invalid or malformed',
'Unauthorized request message': 'You are not authorized to access this resource. Please check your session and try again.'
},
'zh-CN': {
'Device Unavailable': '设备不可用',
'Invalid Request': '无效请求',
'Unauthorized Access': '未授权访问',
'Device offline message': '设备当前离线,请检查设备状态后重试。',
'Invalid request message': '请求无效或格式错误',
'Unauthorized request message': '您无权访问此资源。请检查您的会话并重试。'
}
};
function t(key, lang) {
return translations[lang][key] || translations.en[key] || key;
}
function updateContent() {
const errorType = '%s';
const lang = navigator.language === 'zh-CN' ? 'zh-CN' : 'en';
let title = '', message = '';
switch (errorType) {
case 'offline':
title = t('Device Unavailable', lang);
message = t('Device offline message', lang);
break;
case 'invalid':
title = t('Invalid Request', lang);
message = t('Invalid request message', lang);
break;
case 'unauthorized':
title = t('Unauthorized Access', lang);
message = t('Unauthorized request message', lang);
break;
}
document.getElementById('errorTitle').textContent = title;
document.getElementById('errorMessage').textContent = message;
// Update page title
if (title) {
document.title = title + ' - RTTY';
} else {
document.title = 'Error - RTTY';
}
}
// Initialize page on load
document.addEventListener('DOMContentLoaded', updateContent);
</script>
</body>
</html>`, errorType)
}
func sendHTTPErrorResponse(conn net.Conn, errorType string) {
htmlContent := generateErrorHTML(errorType)
fs, _ := fs.Sub(staticFs, "assets")
f, err := fs.Open("http-proxy-err.html")
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
content, err := io.ReadAll(f)
if err != nil {
fmt.Println(err)
return
}
content = bytes.ReplaceAll(content, []byte("{{.}}"), []byte(errorType))
response := "HTTP/1.1 200 OK\r\n"
response += "Content-Type: text/html; charset=utf-8\r\n"
response += fmt.Sprintf("Content-Length: %d\r\n", len(htmlContent))
response += fmt.Sprintf("Content-Length: %d\r\n", len(content))
response += "Connection: close\r\n"
response += "\r\n"
response += htmlContent
conn.Write([]byte(response))
conn.Write(content)
}