diff --git a/config.example.json b/config.example.json index 8634437..8bbe1d2 100644 --- a/config.example.json +++ b/config.example.json @@ -12,7 +12,8 @@ "write_timeout": 30, "idle_timeout": 300, "max_header_bytes": 1 - } + }, + "show_server_version": false }, "password": { "memory": 4, diff --git a/internal/handler/install.go b/internal/handler/install.go index cad0827..8f8fb2a 100644 --- a/internal/handler/install.go +++ b/internal/handler/install.go @@ -20,7 +20,8 @@ func CheckInstall(config *service.Config) { router.Handle("/font-awesome/", staticRouter) router.Handle("/js/", staticRouter) router.Handle("/img/", staticRouter) - server.Handler = router + router.Handle("/.well-known/", RouteWebDevTools()) + server.Handler = InjectWebServerHeaders(config, router) service.ListenWebService(config, server) return } @@ -32,6 +33,15 @@ func InstallWebHandler(w http.ResponseWriter, r *http.Request) { ErrorHandler(w, r, http.StatusNotFound) return } + data := installPageData{ + StatusCode: 200, + } + loadInstallTemplate() + w.WriteHeader(200) + SetHeaders(w, "text/html; charset=utf-8") + if err := installTemplate.Execute(w, data); err != nil { + log.Printf("[Error] Failed to render install page: %v", err) + } } func GetInstallLock(config *service.Config) bool { diff --git a/internal/handler/middleware.go b/internal/handler/middleware.go index abeebd1..9c45149 100644 --- a/internal/handler/middleware.go +++ b/internal/handler/middleware.go @@ -1 +1,22 @@ package handler + +import ( + "fmt" + "net/http" + + "github.com/OmniX-Space/MeowBox-Core/internal/service" +) + +func InjectWebServerHeaders(config *service.Config, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if config.Server.ShowServerVersion { + version := service.GetVersion() + serverHeader := fmt.Sprintf("MeowBox/%s", version) + w.Header().Set("Server", serverHeader) + next.ServeHTTP(w, r) + return + } + w.Header().Set("Server", "MeowBox") + next.ServeHTTP(w, r) + }) +} diff --git a/internal/handler/route.go b/internal/handler/route.go index 20d85f0..02dc87a 100644 --- a/internal/handler/route.go +++ b/internal/handler/route.go @@ -2,6 +2,14 @@ package handler import "net/http" +func RouteWebDevTools() *http.ServeMux { + mux := http.NewServeMux() + mux.HandleFunc("/.well-known/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNoContent) + }) + return mux +} + func RouteStaticFiles() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/favicon.ico", StaticFileHandler) diff --git a/internal/handler/static.go b/internal/handler/static.go index a8488aa..067280b 100644 --- a/internal/handler/static.go +++ b/internal/handler/static.go @@ -14,6 +14,9 @@ import ( //go:embed web var webFiles embed.FS +//go:embed web/install.html +var installTemplateContent string + //go:embed web/index.html var pageTemplateContent string @@ -21,12 +24,25 @@ var pageTemplateContent string var errorTemplateContent string var ( - pageTemplate *template.Template - errorTemplate *template.Template - errorOnce sync.Once - pageOnce sync.Once + installTemplate *template.Template + pageTemplate *template.Template + errorTemplate *template.Template + installOnce sync.Once + pageOnce sync.Once + errorOnce sync.Once ) +// loadInstallTemplate Initialize template (executed only once) +func loadInstallTemplate() { + installOnce.Do(func() { + var err error + installTemplate, err = template.New("install").Parse(installTemplateContent) + if err != nil { + log.Fatalf("[Error] Failed to parse install template: %v", err) + } + }) +} + // loadPageTemplate Initialize template (executed only once) func loadPageTemplate() { pageOnce.Do(func() { @@ -78,5 +94,4 @@ func StaticFileHandler(w http.ResponseWriter, r *http.Request) { func SetHeaders(w http.ResponseWriter, contentType string) { w.Header().Set("Content-Type", contentType) - w.Header().Set("Server", "CloudCat-Project") } diff --git a/internal/handler/struct.go b/internal/handler/struct.go index 6dd2880..df855ca 100644 --- a/internal/handler/struct.go +++ b/internal/handler/struct.go @@ -14,6 +14,11 @@ type errorPageData struct { Message string } +// InstallPageData Data model for install page template +type installPageData struct { + StatusCode int +} + // IndexPageData Data model for index page template type indexPageData struct { StatusCode int diff --git a/internal/handler/web/css/install.css b/internal/handler/web/css/install.css new file mode 100644 index 0000000..feadb10 --- /dev/null +++ b/internal/handler/web/css/install.css @@ -0,0 +1,63 @@ +.welcome { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + flex-direction: column-reverse; +} + +.welcome-button { + position: absolute; + bottom: 64px; + width: 150px; + backdrop-filter: blur(3px); + border: 0; + border-radius: 20px; + background-color: rgba(255, 255, 255, 0.2); + color: white; + padding: 8px; + font-size: 24px; + cursor: pointer; +} + +.welcome h1 { + font-weight: bold; + font-size: 64px; + color: white; + font-family: 'Comic Sans MS', cursive, sans-serif; + margin: 0; + opacity: 0; +} + +@keyframes fadeInOut { + 0% { + opacity: 0; + } + + 20% { + opacity: 1; + } + + 80% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +.welcome-animation { + animation: fadeInOut 3s forwards; +} + +.container { + width: 80%; + height: calc(100% - 100px); + border-radius: 20px; + backdrop-filter: blur(3px); + background-color: rgba(255, 255, 255, 0.2); + margin: 50px auto; +} \ No newline at end of file diff --git a/internal/handler/web/css/style.css b/internal/handler/web/css/layout.css similarity index 88% rename from internal/handler/web/css/style.css rename to internal/handler/web/css/layout.css index 4628a21..be4df92 100644 --- a/internal/handler/web/css/style.css +++ b/internal/handler/web/css/layout.css @@ -7,8 +7,9 @@ body { } /* Main styles */ -.container { +#page { height: 100vh; background: url('/img/background.webp') no-repeat center center fixed; background-size: cover; + overflow: hidden; } \ No newline at end of file diff --git a/internal/handler/web/httperr.html b/internal/handler/web/httperr.html index e69de29..e82f3c8 100644 --- a/internal/handler/web/httperr.html +++ b/internal/handler/web/httperr.html @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/internal/handler/web/img/26-Tahoe-Light-1024.webp b/internal/handler/web/img/background.webp similarity index 100% rename from internal/handler/web/img/26-Tahoe-Light-1024.webp rename to internal/handler/web/img/background.webp diff --git a/internal/handler/web/index.html b/internal/handler/web/index.html index e69de29..5035b6e 100644 --- a/internal/handler/web/index.html +++ b/internal/handler/web/index.html @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/internal/handler/web/install.html b/internal/handler/web/install.html index 096b584..cb6d1f5 100644 --- a/internal/handler/web/install.html +++ b/internal/handler/web/install.html @@ -5,8 +5,6 @@ - - diff --git a/internal/handler/web/js/i18n.js b/internal/handler/web/js/i18n.js new file mode 100644 index 0000000..9584d4b --- /dev/null +++ b/internal/handler/web/js/i18n.js @@ -0,0 +1,19 @@ +/* + * i18n.js + * MeowBox-Core + * Created by MoeCinnamo on 2025/11/24. + * + * This file contains the i18n strings for the web interface. + */ + +const en = { + "hello": "Hello", +}; +const zhCN = { + "hello": "你好", +}; +const jp = { + "hello": "こんにちは", +}; + +const languages = [en, zhCN, jp]; \ No newline at end of file diff --git a/internal/handler/web/js/install.js b/internal/handler/web/js/install.js index 18d01bc..2982481 100644 --- a/internal/handler/web/js/install.js +++ b/internal/handler/web/js/install.js @@ -1,9 +1,75 @@ -function install() { - document.title = "安装程序 - MeowBox"; - const container = document.createElement('div'); - container.classList.add('container'); - document.body.appendChild(container); +function welcome() { + document.title = "Welcome - MeowBox"; + const page = document.getElementById("page"); + let currentIndex = 0; + const welcomeDiv = document.createElement("div"); + welcomeDiv.classList.add("welcome"); + const welcomeButton = document.createElement("button"); + welcomeButton.classList.add("welcome-button"); + welcomeButton.innerHTML = 'Next '; + welcomeDiv.appendChild(welcomeButton); + function displayNextLanguage() { + if (currentIndex >= languages.length) { + currentIndex = 0; + } + const welcomeTitle = document.createElement("h1"); + welcomeTitle.classList.add("welcome-animation") + welcomeTitle.textContent = languages[currentIndex]["hello"]; + welcomeDiv.appendChild(welcomeTitle); + setTimeout(() => { + welcomeDiv.removeChild(welcomeTitle); + currentIndex++; + displayNextLanguage(); + }, 3000); + } + displayNextLanguage(); + page.appendChild(welcomeDiv); } + +function install() { +} + +// Load external script +function loadScript(scriptUrl) { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = scriptUrl; + script.async = true; + + script.onload = () => { + resolve(); + script.remove(); + }; + + script.onerror = () => { + const errorMsg = `Failed to load script: ${scriptUrl}`; + console.error(`[ScriptLoader] ${errorMsg}`); + reject(new Error(errorMsg)); + }; + + document.body.appendChild(script); + }); +} + +// Initialize app +async function initializeApp() { + try { + await Promise.all([ + loadScript("/js/layout.js") + ]); + createPage(); + await Promise.all([ + loadScript("/js/i18n.js"), + loadCSSAsPromise("/css/layout.css"), + loadCSSAsPromise("/css/install.css"), + loadCSSAsPromise("/font-awesome/css/all.min.css") + ]) + welcome(); + } catch (error) { + console.error('App initialization error:', error); + } +} + document.addEventListener('DOMContentLoaded', () => { - install(); + initializeApp(); }); \ No newline at end of file diff --git a/internal/handler/web/js/layout.js b/internal/handler/web/js/layout.js new file mode 100644 index 0000000..12fe863 --- /dev/null +++ b/internal/handler/web/js/layout.js @@ -0,0 +1,75 @@ +function loadCSS(url, callback, errorCallback, attributes = {}) { + const existingLinks = document.querySelectorAll('link[rel="stylesheet"]'); + for (let i = 0; i < existingLinks.length; i++) { + if (existingLinks[i].href === url || existingLinks[i].href.endsWith(url)) { + if (typeof callback === 'function') { + callback(); + } + return existingLinks[i]; + } + } + + const link = document.createElement('link'); + + link.rel = attributes.rel || 'stylesheet'; + link.type = attributes.type || 'text/css'; + link.href = url; + + if (attributes.media) link.media = attributes.media; + if (attributes.id) link.id = attributes.id; + if (attributes.crossOrigin) link.crossOrigin = attributes.crossOrigin; + + link.onload = function () { + if (typeof callback === 'function') { + callback(); + } + }; + + link.onerror = function (err) { + if (typeof errorCallback === 'function') { + errorCallback(err); + } else { + console.error(`Failed to load CSS: ${url}`, err); + } + }; + + const head = document.head || document.getElementsByTagName('head')[0]; + head.appendChild(link); + + return link; +} + +function loadCSSAsPromise(url, attributes = {}) { + return new Promise((resolve, reject) => { + loadCSS( + url, + resolve, + reject, + attributes + ); + }); +} + +function loadMD5Script() { + return new Promise((resolve, reject) => { + if (typeof md5 !== 'undefined') { + resolve(); // MD5 library already loaded + return; + } + const script = document.createElement('script'); + script.src = '/js/md5.min.js'; + script.async = true; + script.onload = () => resolve(); + script.onerror = () => { + console.error('Failed to load MD5 library'); + reject(new Error('MD5 library load failed')); + }; + document.head.appendChild(script); + }); +} + +function createPage() { + const main = document.createElement('div'); + main.id = "page"; + document.body.appendChild(main); +} \ No newline at end of file diff --git a/internal/handler/web/js/md5.min.js b/internal/handler/web/js/md5.min.js new file mode 100644 index 0000000..3777aed --- /dev/null +++ b/internal/handler/web/js/md5.min.js @@ -0,0 +1 @@ +!function (n) { "use strict"; function d(n, t) { var r = (65535 & n) + (65535 & t); return (n >> 16) + (t >> 16) + (r >> 16) << 16 | 65535 & r } function f(n, t, r, e, o, u) { return d((u = d(d(t, n), d(e, u))) << o | u >>> 32 - o, r) } function l(n, t, r, e, o, u, c) { return f(t & r | ~t & e, n, t, o, u, c) } function g(n, t, r, e, o, u, c) { return f(t & e | r & ~e, n, t, o, u, c) } function v(n, t, r, e, o, u, c) { return f(t ^ r ^ e, n, t, o, u, c) } function m(n, t, r, e, o, u, c) { return f(r ^ (t | ~e), n, t, o, u, c) } function c(n, t) { var r, e, o, u; n[t >> 5] |= 128 << t % 32, n[14 + (t + 64 >>> 9 << 4)] = t; for (var c = 1732584193, f = -271733879, i = -1732584194, a = 271733878, h = 0; h < n.length; h += 16)c = l(r = c, e = f, o = i, u = a, n[h], 7, -680876936), a = l(a, c, f, i, n[h + 1], 12, -389564586), i = l(i, a, c, f, n[h + 2], 17, 606105819), f = l(f, i, a, c, n[h + 3], 22, -1044525330), c = l(c, f, i, a, n[h + 4], 7, -176418897), a = l(a, c, f, i, n[h + 5], 12, 1200080426), i = l(i, a, c, f, n[h + 6], 17, -1473231341), f = l(f, i, a, c, n[h + 7], 22, -45705983), c = l(c, f, i, a, n[h + 8], 7, 1770035416), a = l(a, c, f, i, n[h + 9], 12, -1958414417), i = l(i, a, c, f, n[h + 10], 17, -42063), f = l(f, i, a, c, n[h + 11], 22, -1990404162), c = l(c, f, i, a, n[h + 12], 7, 1804603682), a = l(a, c, f, i, n[h + 13], 12, -40341101), i = l(i, a, c, f, n[h + 14], 17, -1502002290), c = g(c, f = l(f, i, a, c, n[h + 15], 22, 1236535329), i, a, n[h + 1], 5, -165796510), a = g(a, c, f, i, n[h + 6], 9, -1069501632), i = g(i, a, c, f, n[h + 11], 14, 643717713), f = g(f, i, a, c, n[h], 20, -373897302), c = g(c, f, i, a, n[h + 5], 5, -701558691), a = g(a, c, f, i, n[h + 10], 9, 38016083), i = g(i, a, c, f, n[h + 15], 14, -660478335), f = g(f, i, a, c, n[h + 4], 20, -405537848), c = g(c, f, i, a, n[h + 9], 5, 568446438), a = g(a, c, f, i, n[h + 14], 9, -1019803690), i = g(i, a, c, f, n[h + 3], 14, -187363961), f = g(f, i, a, c, n[h + 8], 20, 1163531501), c = g(c, f, i, a, n[h + 13], 5, -1444681467), a = g(a, c, f, i, n[h + 2], 9, -51403784), i = g(i, a, c, f, n[h + 7], 14, 1735328473), c = v(c, f = g(f, i, a, c, n[h + 12], 20, -1926607734), i, a, n[h + 5], 4, -378558), a = v(a, c, f, i, n[h + 8], 11, -2022574463), i = v(i, a, c, f, n[h + 11], 16, 1839030562), f = v(f, i, a, c, n[h + 14], 23, -35309556), c = v(c, f, i, a, n[h + 1], 4, -1530992060), a = v(a, c, f, i, n[h + 4], 11, 1272893353), i = v(i, a, c, f, n[h + 7], 16, -155497632), f = v(f, i, a, c, n[h + 10], 23, -1094730640), c = v(c, f, i, a, n[h + 13], 4, 681279174), a = v(a, c, f, i, n[h], 11, -358537222), i = v(i, a, c, f, n[h + 3], 16, -722521979), f = v(f, i, a, c, n[h + 6], 23, 76029189), c = v(c, f, i, a, n[h + 9], 4, -640364487), a = v(a, c, f, i, n[h + 12], 11, -421815835), i = v(i, a, c, f, n[h + 15], 16, 530742520), c = m(c, f = v(f, i, a, c, n[h + 2], 23, -995338651), i, a, n[h], 6, -198630844), a = m(a, c, f, i, n[h + 7], 10, 1126891415), i = m(i, a, c, f, n[h + 14], 15, -1416354905), f = m(f, i, a, c, n[h + 5], 21, -57434055), c = m(c, f, i, a, n[h + 12], 6, 1700485571), a = m(a, c, f, i, n[h + 3], 10, -1894986606), i = m(i, a, c, f, n[h + 10], 15, -1051523), f = m(f, i, a, c, n[h + 1], 21, -2054922799), c = m(c, f, i, a, n[h + 8], 6, 1873313359), a = m(a, c, f, i, n[h + 15], 10, -30611744), i = m(i, a, c, f, n[h + 6], 15, -1560198380), f = m(f, i, a, c, n[h + 13], 21, 1309151649), c = m(c, f, i, a, n[h + 4], 6, -145523070), a = m(a, c, f, i, n[h + 11], 10, -1120210379), i = m(i, a, c, f, n[h + 2], 15, 718787259), f = m(f, i, a, c, n[h + 9], 21, -343485551), c = d(c, r), f = d(f, e), i = d(i, o), a = d(a, u); return [c, f, i, a] } function i(n) { for (var t = "", r = 32 * n.length, e = 0; e < r; e += 8)t += String.fromCharCode(n[e >> 5] >>> e % 32 & 255); return t } function a(n) { var t = []; for (t[(n.length >> 2) - 1] = void 0, e = 0; e < t.length; e += 1)t[e] = 0; for (var r = 8 * n.length, e = 0; e < r; e += 8)t[e >> 5] |= (255 & n.charCodeAt(e / 8)) << e % 32; return t } function e(n) { for (var t, r = "0123456789abcdef", e = "", o = 0; o < n.length; o += 1)t = n.charCodeAt(o), e += r.charAt(t >>> 4 & 15) + r.charAt(15 & t); return e } function r(n) { return unescape(encodeURIComponent(n)) } function o(n) { return i(c(a(n = r(n)), 8 * n.length)) } function u(n, t) { return function (n, t) { var r, e = a(n), o = [], u = []; for (o[15] = u[15] = void 0, 16 < e.length && (e = c(e, 8 * n.length)), r = 0; r < 16; r += 1)o[r] = 909522486 ^ e[r], u[r] = 1549556828 ^ e[r]; return t = c(o.concat(a(t)), 512 + 8 * t.length), i(c(u.concat(t), 640)) }(r(n), r(t)) } function t(n, t, r) { return t ? r ? u(t, n) : e(u(t, n)) : r ? o(n) : e(o(n)) } "function" == typeof define && define.amd ? define(function () { return t }) : "object" == typeof module && module.exports ? module.exports = t : n.md5 = t }(this); \ No newline at end of file diff --git a/internal/service/struct.go b/internal/service/struct.go index 6bde49e..08229b5 100644 --- a/internal/service/struct.go +++ b/internal/service/struct.go @@ -3,9 +3,10 @@ package service // Config represents the full configuration structure type Config struct { Server struct { - Host string `json:"host"` - Port int `json:"port"` - Tls struct { + Host string `json:"host"` + Port int `json:"port"` + ShowServerVersion bool `json:"show_server_version"` + Tls struct { Enabled bool `json:"enabled"` Cert string `json:"cert_file"` Key string `json:"key_file"` diff --git a/internal/service/version.go b/internal/service/version.go index a2da8bb..ebec85e 100644 --- a/internal/service/version.go +++ b/internal/service/version.go @@ -1,5 +1,5 @@ package service func GetVersion() string { - return "0.1.0 dev 2025-11-22" + return "v0.1.0 dev 2025-11-24" }