This commit is contained in:
214
client/app.go
214
client/app.go
@@ -16,6 +16,7 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -154,6 +155,7 @@ type App struct {
|
||||
wifiRejected bool
|
||||
quitDialogShown bool
|
||||
monitoringTornDown bool
|
||||
localBrowserActive bool // trình duyệt local — chỉ cho phép localhost
|
||||
chatUnread int
|
||||
replyStaffID uint
|
||||
fetchAppsMu sync.Mutex
|
||||
@@ -276,6 +278,9 @@ func (a *App) startup(ctx context.Context) {
|
||||
|
||||
// Khởi chạy vòng lặp đọc local storage của trang Rikkei Portal khi chưa đăng nhập
|
||||
go a.authStorageScanner()
|
||||
|
||||
// Chặn thoát localhost khi đang dùng trình duyệt local
|
||||
go a.localBrowserGuardLoop()
|
||||
}
|
||||
|
||||
func (a *App) handleGuardViolation(kind, reason string) {
|
||||
@@ -588,6 +593,7 @@ func (a *App) startLocalServer() {
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
return
|
||||
}
|
||||
a.setLocalBrowserActive(false)
|
||||
go func() {
|
||||
guard.SuppressFor(5 * time.Second)
|
||||
runtime.WindowReloadApp(a.ctx)
|
||||
@@ -604,6 +610,46 @@ func (a *App) startLocalServer() {
|
||||
go a.purgeWebViewDiskCache()
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
mux.HandleFunc("/local-browser", func(w http.ResponseWriter, r *http.Request) {
|
||||
blocked := strings.TrimSpace(r.URL.Query().Get("blocked"))
|
||||
from := strings.TrimSpace(r.URL.Query().Get("from"))
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
notice := ""
|
||||
if blocked == "1" {
|
||||
notice = `<div class="notice">Đã chặn liên kết ngoài mạng. Trình duyệt này chỉ mở <strong>localhost</strong> / <strong>127.0.0.1</strong>.</div>`
|
||||
if from != "" {
|
||||
notice += `<div class="notice-sub">Đã chặn: ` + html.EscapeString(from) + `</div>`
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, localBrowserHTML, notice)
|
||||
})
|
||||
mux.HandleFunc("/local-browser-go", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
if r.Method == "OPTIONS" {
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
return
|
||||
}
|
||||
raw := strings.TrimSpace(r.URL.Query().Get("url"))
|
||||
if raw == "" {
|
||||
http.Error(w, "missing url", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !strings.Contains(raw, "://") {
|
||||
raw = "http://" + raw
|
||||
}
|
||||
if !isLoopbackURL(raw) {
|
||||
http.Error(w, "only localhost allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
a.mu.Lock()
|
||||
a.localBrowserActive = true
|
||||
a.mu.Unlock()
|
||||
go func() {
|
||||
guard.SuppressFor(3 * time.Second)
|
||||
runtime.WindowExecJS(a.ctx, fmt.Sprintf("window.location.href = %q", raw))
|
||||
}()
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
|
||||
go func() {
|
||||
_ = server.ListenAndServe()
|
||||
@@ -690,6 +736,94 @@ document.addEventListener('keydown', function(e){
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
const localBrowserHTML = `<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Trình duyệt Local — Simple Care</title>
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:Segoe UI,system-ui,sans-serif;background:#f5f7fa;color:#1a2332;min-height:100vh;display:flex;flex-direction:column}
|
||||
.bar{display:flex;align-items:center;gap:8px;padding:10px 14px;background:#fff;border-bottom:1px solid #e4e9f0;flex-wrap:wrap}
|
||||
.bar button{border-radius:8px;padding:8px 12px;font-size:13px;cursor:pointer;font-weight:600;white-space:nowrap}
|
||||
.btn-back{background:#fff;color:#bb2126;border:1px solid #bb2126}
|
||||
.btn-back:hover{background:#fde8e9}
|
||||
.btn-go{background:#bb2126;color:#fff;border:1px solid #bb2126}
|
||||
.btn-go:hover{background:#9e1c22}
|
||||
.addr{flex:1;min-width:220px;padding:8px 12px;border:1px solid #e4e9f0;border-radius:8px;font-size:13px}
|
||||
.addr:focus{outline:2px solid rgba(187,33,38,.25);border-color:#bb2126}
|
||||
.main{flex:1;padding:28px 20px;max-width:720px;margin:0 auto;width:100%%}
|
||||
h1{font-size:1.35rem;margin-bottom:8px;color:#bb2126}
|
||||
.lead{color:#5a6a7e;font-size:0.95rem;margin-bottom:18px;line-height:1.5}
|
||||
.notice{background:#fde8e9;color:#9e1c22;border:1px solid #f5c2c4;border-radius:10px;padding:12px 14px;margin-bottom:10px;font-size:0.9rem}
|
||||
.notice-sub{font-size:0.8rem;color:#8b99a8;margin-bottom:14px;word-break:break-all}
|
||||
.chips{display:flex;flex-wrap:wrap;gap:8px;margin-top:16px}
|
||||
.chip{background:#fff;border:1px solid #e4e9f0;border-radius:999px;padding:6px 12px;font-size:12px;cursor:pointer;color:#bb2126;font-weight:600}
|
||||
.chip:hover{background:#fde8e9;border-color:#bb2126}
|
||||
.hint{margin-top:22px;font-size:12px;color:#8b99a8;line-height:1.5}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bar">
|
||||
<button type="button" class="btn-back" onclick="goHome()">← Về trang chính</button>
|
||||
<input class="addr" id="addr" type="url" placeholder="http://localhost:3000" spellcheck="false" />
|
||||
<button type="button" class="btn-go" onclick="goLocal()">Mở</button>
|
||||
</div>
|
||||
<div class="main">
|
||||
%s
|
||||
<h1>Trình duyệt Local</h1>
|
||||
<p class="lead">Chỉ dùng để xem bài làm chạy trên máy bạn (<strong>localhost</strong> / <strong>127.0.0.1</strong>). Mọi link online (GitHub, Google, …) đều bị chặn tại đây.</p>
|
||||
<div class="chips">
|
||||
<button type="button" class="chip" onclick="fill('http://localhost:3000')">localhost:3000</button>
|
||||
<button type="button" class="chip" onclick="fill('http://localhost:5173')">localhost:5173</button>
|
||||
<button type="button" class="chip" onclick="fill('http://127.0.0.1:8080')">127.0.0.1:8080</button>
|
||||
<button type="button" class="chip" onclick="fill('http://localhost:5500')">localhost:5500</button>
|
||||
</div>
|
||||
<p class="hint">Mẹo: chạy dự án (npm run dev / Live Server), dán URL vào ô trên rồi bấm Mở. Về trang chính: nút ← hoặc menu Simple Care → Ctrl+H.</p>
|
||||
</div>
|
||||
<script>
|
||||
function fill(u){ document.getElementById('addr').value = u; }
|
||||
function goHome(){
|
||||
fetch('http://127.0.0.1:34115/exam-close').catch(function(){});
|
||||
}
|
||||
function isLoopback(raw){
|
||||
try {
|
||||
if (!raw) return false;
|
||||
if (raw.indexOf('://') < 0) raw = 'http://' + raw;
|
||||
var u = new URL(raw);
|
||||
if (u.protocol !== 'http:' && u.protocol !== 'https:') return false;
|
||||
var h = (u.hostname || '').toLowerCase();
|
||||
if (h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '[::1]') return true;
|
||||
if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
|
||||
return false;
|
||||
} catch (e) { return false; }
|
||||
}
|
||||
async function goLocal(){
|
||||
var raw = (document.getElementById('addr').value || '').trim();
|
||||
if (!raw) { alert('Nhập URL localhost'); return; }
|
||||
if (!isLoopback(raw)) {
|
||||
alert('Chỉ được mở localhost / 127.0.0.1');
|
||||
return;
|
||||
}
|
||||
if (raw.indexOf('://') < 0) raw = 'http://' + raw;
|
||||
try {
|
||||
var res = await fetch('http://127.0.0.1:34115/local-browser-go?url=' + encodeURIComponent(raw));
|
||||
if (!res.ok) {
|
||||
alert('Không mở được — chỉ localhost');
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
location.href = raw;
|
||||
}
|
||||
}
|
||||
document.getElementById('addr').addEventListener('keydown', function(e){
|
||||
if (e.key === 'Enter') { e.preventDefault(); goLocal(); }
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
// loadSession nạp session từ file cục bộ
|
||||
func (a *App) loadSession() {
|
||||
a.mu.Lock()
|
||||
@@ -808,6 +942,7 @@ func (a *App) GetStudentInfo() map[string]any {
|
||||
|
||||
// NavigateToLogin chuyển WebView đến trang đăng nhập Rikkei Portal
|
||||
func (a *App) NavigateToLogin() {
|
||||
a.setLocalBrowserActive(false)
|
||||
a.mu.Lock()
|
||||
a.expectingLogin = true
|
||||
a.mu.Unlock()
|
||||
@@ -1844,10 +1979,88 @@ func (a *App) examStudentContext() (int64, *StudentExamSnapshot, error) {
|
||||
|
||||
// ReturnToDashboard quay lại giao diện chính của app (giữ cookie WebView2).
|
||||
func (a *App) ReturnToDashboard() {
|
||||
a.setLocalBrowserActive(false)
|
||||
guard.SuppressFor(5 * time.Second)
|
||||
runtime.WindowReloadApp(a.ctx)
|
||||
}
|
||||
|
||||
func (a *App) setLocalBrowserActive(on bool) {
|
||||
a.mu.Lock()
|
||||
a.localBrowserActive = on
|
||||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
func isLoopbackURL(raw string) bool {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return false
|
||||
}
|
||||
if !strings.Contains(raw, "://") {
|
||||
raw = "http://" + raw
|
||||
}
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil || u.Host == "" {
|
||||
return false
|
||||
}
|
||||
scheme := strings.ToLower(u.Scheme)
|
||||
if scheme != "http" && scheme != "https" {
|
||||
return false
|
||||
}
|
||||
host := strings.ToLower(u.Hostname())
|
||||
if host == "localhost" {
|
||||
return true
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
return ip != nil && ip.IsLoopback()
|
||||
}
|
||||
|
||||
func (a *App) localBrowserGuardLoop() {
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
a.mu.Lock()
|
||||
active := a.localBrowserActive && a.ctx != nil && !a.monitoringTornDown
|
||||
ctx := a.ctx
|
||||
a.mu.Unlock()
|
||||
if !active {
|
||||
continue
|
||||
}
|
||||
// Nếu hostname không phải loopback → đá về trang local-browser
|
||||
runtime.WindowExecJS(ctx, `
|
||||
(function(){
|
||||
try {
|
||||
var h = (location.hostname || '').toLowerCase();
|
||||
var ok = h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '[::1]' || /^127\.\d+\.\d+\.\d+$/.test(h);
|
||||
if (!ok) {
|
||||
location.href = 'http://127.0.0.1:34115/local-browser?blocked=1&from=' + encodeURIComponent(location.href);
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
// OpenGitHub mở github.com trong WebView (về trang chính: Ctrl+H).
|
||||
func (a *App) OpenGitHub() {
|
||||
a.setLocalBrowserActive(false)
|
||||
guard.SuppressFor(5 * time.Second)
|
||||
runtime.WindowExecJS(a.ctx, `window.location.href = 'https://github.com'`)
|
||||
}
|
||||
|
||||
// OpenGoogleTranslate mở Google Dịch trong WebView.
|
||||
func (a *App) OpenGoogleTranslate() {
|
||||
a.setLocalBrowserActive(false)
|
||||
guard.SuppressFor(5 * time.Second)
|
||||
runtime.WindowExecJS(a.ctx, `window.location.href = 'https://translate.google.com/?sl=auto&tl=vi'`)
|
||||
}
|
||||
|
||||
// OpenLocalBrowser mở trình duyệt chỉ cho phép localhost (test bài làm local).
|
||||
func (a *App) OpenLocalBrowser() {
|
||||
a.setLocalBrowserActive(true)
|
||||
guard.SuppressFor(5 * time.Second)
|
||||
runtime.WindowExecJS(a.ctx, `window.location.href = 'http://127.0.0.1:34115/local-browser'`)
|
||||
}
|
||||
|
||||
// ReloadExamPage — F5: làm mới trang / iframe đang mở (trắc nghiệm).
|
||||
func (a *App) ReloadExamPage() {
|
||||
guard.SuppressFor(3 * time.Second)
|
||||
@@ -1928,6 +2141,7 @@ func (a *App) openExamWebView(targetURL, title string) error {
|
||||
if targetURL == "" {
|
||||
return errors.New("không có nội dung để mở")
|
||||
}
|
||||
a.setLocalBrowserActive(false)
|
||||
// URL local (PDF/resource): khung exam-view. URL ngoài (quiz): mở top-level giữ first-party cookies/session.
|
||||
if isLocalExamURL(targetURL) {
|
||||
wrapper := fmt.Sprintf(
|
||||
|
||||
Reference in New Issue
Block a user