diff options
Diffstat (limited to 'website.go')
| -rw-r--r-- | website.go | 126 |
1 files changed, 120 insertions, 6 deletions
@@ -11,10 +11,12 @@ import ( "html/template" "io/fs" "log" + "maps" "net/http" "os" "regexp" "slices" + "sort" "strings" "sync" "time" @@ -401,12 +403,101 @@ func (externalLinks) Transform(doc *ast.Document, reader text.Reader, pc parser. }) } +type ShellResult struct { + Output templ.Component // nil renders nothing + Clear bool + Vars map[string]string + Cookies []*http.Cookie +} + +type shellCommand func(r *http.Request, args []string) ShellResult + +var shellCommands map[string]shellCommand + +func init() { + shellCommands = map[string]shellCommand{ + "fastfetch": func(*http.Request, []string) ShellResult { return ShellResult{Output: Fastfetch(fetchRows)} }, + "clear": func(*http.Request, []string) ShellResult { return ShellResult{Clear: true} }, + "help": func(*http.Request, []string) ShellResult { return ShellResult{Output: ShellHelp(shellNames())} }, + "export": func(r *http.Request, args []string) ShellResult { + if len(args) == 0 { + return ShellResult{Output: ShellText("usage: export KEY=value")} + } + res := ShellResult{Vars: map[string]string{}} + var lines []string + for _, arg := range args { + key, value, ok := strings.Cut(arg, "=") + if !ok { + lines = append(lines, "export: expected KEY=value, got "+arg) + continue + } + value = strings.Trim(value, `"'`) + vars, cookie, err := setEnvVar(key, value) + if err != nil { + lines = append(lines, "export: "+err.Error()) + continue + } + maps.Copy(res.Vars, vars) + res.Cookies = append(res.Cookies, cookie) + lines = append(lines, key+"="+value) + } + res.Output = ShellLines(lines) + return res + }, + } +} + +func setEnvVar(key, value string) (map[string]string, *http.Cookie, error) { + switch key { + case "THEME": + theme, ok := GetTheme(value) + if !ok { + return nil, nil, fmt.Errorf("unknown theme: %s", value) + } + return theme.Vars(), envCookie("theme", value), nil + case "FONT_PACK": + pack, ok := GetFontPack(value) + if !ok { + return nil, nil, fmt.Errorf("unknown font pack: %s", value) + } + return pack.Vars(), envCookie("font_pack", value), nil + default: + return nil, nil, fmt.Errorf("unknown variable: %s", key) + } +} + +func envCookie(name, value string) *http.Cookie { + return &http.Cookie{Name: name, Value: value, Path: "/", MaxAge: 31536000, SameSite: http.SameSiteLaxMode} +} + +func shellNames() []string { + names := make([]string, 0, len(shellCommands)) + for name := range shellCommands { + names = append(names, name) + } + sort.Strings(names) + return names +} + +func runShell(r *http.Request, line string) ShellResult { + fields := strings.Fields(line) + if len(fields) == 0 { + return ShellResult{} + } + run, ok := shellCommands[fields[0]] + if !ok { + return ShellResult{Output: ShellText(fields[0] + ": command not found")} + } + + return run(r, fields[1:]) +} + var projects = []Project{ { Slug: "krypton", Title: "Krypton", Category: "Programming Language Compiler", - Desc: "", + Desc: "Krypton is a simple, high-performance, general-purpose language with C-like control designed for good programmers.", // Icon: IconCpu("text-[#F5A670] self-center"), Icon: IconKrypton(), Tags: []ProjectTag{ @@ -556,7 +647,7 @@ func GetMyUptime() string { defer myUptime.mu.RUnlock() // If no ping received in 3 minutes, treat as offline - if time.Since(myUptime.LastUpdate) > 1*time.Minute { + if time.Since(myUptime.LastUpdate) > 3*time.Minute { return "Asleep" } @@ -691,10 +782,6 @@ func main() { HomePage(currentStyle(r), latestBlog(getBlogs())).Render(r.Context(), w) }) - mux.HandleFunc("GET /info", func(w http.ResponseWriter, r *http.Request) { - InfoPage(currentStyle(r)).Render(r.Context(), w) - }) - mux.HandleFunc("GET /projects", func(w http.ResponseWriter, r *http.Request) { ProjectsPage(currentStyle(r), projects, 2).Render(r.Context(), w) }) @@ -712,6 +799,33 @@ func main() { BlogPage(currentStyle(r), *blog, prev, next).Render(r.Context(), w) }) + mux.HandleFunc("GET /shell", func(w http.ResponseWriter, r *http.Request) { + ShellPage(currentStyle(r)).Render(r.Context(), w) + }) + + mux.HandleFunc("POST /shell/run", func(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, 1<<10) + line := strings.TrimSpace(r.FormValue("cmd")) + if len(line) > 200 { + line = line[:200] + } + res := runShell(r, line) + for _, c := range res.Cookies { + http.SetCookie(w, c) + } + + var buf bytes.Buffer + if res.Output != nil { + if err := ShellEntry(line, res.Output).Render(r.Context(), &buf); err != nil { + http.Error(w, "render failed", http.StatusInternalServerError) + return + } + } + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store") + json.NewEncoder(w).Encode(map[string]any{"clear": res.Clear, "html": buf.String(), "vars": res.Vars}) + }) + mux.HandleFunc("GET /theme/{name}", styleHandler("theme", func(n string) (map[string]string, bool) { theme, ok := GetTheme(n) return theme.Vars(), ok |
