Created
July 15, 2026 06:45
-
-
Save russianryebread/decbfa694067cfeba8a59a5536cf2180 to your computer and use it in GitHub Desktop.
Simple HTTP Server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "fmt" | |
| "log" | |
| "net" | |
| "net/http" | |
| "os" | |
| "path/filepath" | |
| "sort" | |
| "strconv" | |
| "strings" | |
| "time" | |
| ) | |
| // filteredFS wraps an http.Dir but refuses to serve hidden files. | |
| type filteredFS struct { | |
| root http.Dir | |
| } | |
| func (f filteredFS) Open(name string) (http.File, error) { | |
| // name is the cleaned path relative to the root (http.Dir applies Clean). | |
| // Check every component for a leading dot — reject if any is hidden. | |
| if hasHiddenComponent(name) { | |
| return nil, os.ErrNotExist | |
| } | |
| return f.root.Open(name) | |
| } | |
| func hasHiddenComponent(path string) bool { | |
| for _, part := range strings.Split(path, string(os.PathSeparator)) { | |
| if part != "" && part[0] == '.' { | |
| return true | |
| } | |
| // Also reject __MACOSX / DS_Store on principle (dir starts with non-dot | |
| // but we also reject well-known "hidden" entries). | |
| if part == "__MACOSX" { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| // responseWriter wraps http.ResponseWriter to capture the status code. | |
| type responseWriter struct { | |
| http.ResponseWriter | |
| status int | |
| } | |
| func (rw *responseWriter) WriteHeader(code int) { | |
| rw.status = code | |
| rw.ResponseWriter.WriteHeader(code) | |
| } | |
| // loggingMiddleware wraps an http.Handler and logs each request to stdout. | |
| func loggingMiddleware(next http.Handler) http.Handler { | |
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| start := time.Now() | |
| rw := &responseWriter{ResponseWriter: w, status: http.StatusOK} | |
| next.ServeHTTP(rw, r) | |
| log.Printf("%s %s %d %s", r.Method, r.URL.Path, rw.status, time.Since(start)) | |
| }) | |
| } | |
| func main() { | |
| dir := "." // default directory | |
| port := 8080 // default port | |
| args := os.Args[1:] | |
| // Parse flags — very simple flag loop. | |
| for i := 0; i < len(args); i++ { | |
| if args[i] == "-d" && i+1 < len(args) { | |
| dir = args[i+1] | |
| i++ // skip the value | |
| } else { | |
| // Try to parse as port | |
| if p, err := strconv.Atoi(args[i]); err == nil && p >= 0 && p <= 65535 { | |
| port = p | |
| } | |
| // Silently ignore unknown flags | |
| } | |
| } | |
| absDir, err := filepath.Abs(dir) | |
| if err != nil { | |
| fmt.Fprintf(os.Stderr, "bad directory %q: %v\n", dir, err) | |
| os.Exit(1) | |
| } | |
| if fi, err := os.Stat(absDir); err != nil || !fi.IsDir() { | |
| fmt.Fprintf(os.Stderr, "%q is not a valid directory\n", absDir) | |
| os.Exit(1) | |
| } | |
| fs := filteredFS{root: http.Dir(absDir)} | |
| // Use a custom file server that generates a directory listing. | |
| handler := loggingMiddleware(customFileServer(fs)) | |
| ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) | |
| if err != nil { | |
| fmt.Fprintf(os.Stderr, "cannot listen on port %d: %v\n", port, err) | |
| os.Exit(1) | |
| } | |
| fmt.Printf("Serving %s on http://localhost:%d\n", absDir, port) | |
| if err := http.Serve(ln, handler); err != nil { | |
| fmt.Fprintf(os.Stderr, "server error: %v\n", err) | |
| os.Exit(1) | |
| } | |
| } | |
| // customFileServer returns a handler that serves files, hides hidden paths, | |
| // and renders a simple file-browser HTML page for directories without an | |
| // index.html / index.htm. | |
| func customFileServer(fs filteredFS) http.Handler { | |
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| // Clean the path to prevent directory traversal. | |
| cleaned := filepath.Clean(r.URL.Path) | |
| if cleaned != r.URL.Path { | |
| // Redirect so the browser shows the canonical path. | |
| http.Redirect(w, r, cleaned, http.StatusMovedPermanently) | |
| return | |
| } | |
| // Reject hidden paths. | |
| if hasHiddenComponent(cleaned) { | |
| http.NotFound(w, r) | |
| return | |
| } | |
| // Build the real filesystem path. | |
| realPath := filepath.Join(string(fs.root), cleaned) | |
| fi, err := os.Stat(realPath) | |
| if err != nil { | |
| http.NotFound(w, r) | |
| return | |
| } | |
| if !fi.IsDir() { | |
| // Serve a regular file. | |
| http.ServeFile(w, r, realPath) | |
| return | |
| } | |
| // It's a directory. Check for index.html / index.htm. | |
| for _, idx := range []string{"index.html", "index.htm"} { | |
| idxPath := filepath.Join(realPath, idx) | |
| if fi2, err := os.Stat(idxPath); err == nil && !fi2.IsDir() { | |
| http.ServeFile(w, r, idxPath) | |
| return | |
| } | |
| } | |
| // No index file — render a simple directory listing. | |
| dirListing(w, r, realPath, cleaned, fs) | |
| }) | |
| } | |
| func dirListing(w http.ResponseWriter, r *http.Request, realPath, urlPath string, fs filteredFS) { | |
| entries, err := os.ReadDir(realPath) | |
| if err != nil { | |
| http.Error(w, "Error reading directory", http.StatusInternalServerError) | |
| return | |
| } | |
| // Filter out hidden entries. | |
| var visible []os.DirEntry | |
| for _, e := range entries { | |
| if !isHidden(e) { | |
| visible = append(visible, e) | |
| } | |
| } | |
| sort.Slice(visible, func(i, j int) bool { | |
| return strings.ToLower(visible[i].Name()) < strings.ToLower(visible[j].Name()) | |
| }) | |
| w.Header().Set("Content-Type", "text/html; charset=utf-8") | |
| fmt.Fprintf(w, `<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Index of %s</title> | |
| <style> | |
| body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; max-width: 800px; margin: 2em auto; padding: 0 1em; color: #333; } | |
| h1 { font-size: 1.3em; border-bottom: 1px solid #ddd; padding-bottom: .3em; } | |
| ul { list-style: none; padding: 0; } | |
| li { padding: .3em .5em; } | |
| li:nth-child(even) { background: #f6f6f6; } | |
| a { text-decoration: none; color: #0366d6; } | |
| a:hover { text-decoration: underline; } | |
| .size { color: #666; font-size: .85em; margin-left: .6em; } | |
| .dir { font-weight: 500; } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Index of %s</h1> | |
| <ul> | |
| <li><a href="../">../</a></li> | |
| `, htmlEscape(urlPath), htmlEscape(urlPath)) | |
| // Dirs first, then files. | |
| dirs := make([]os.DirEntry, 0) | |
| files := make([]os.DirEntry, 0) | |
| for _, e := range visible { | |
| if e.IsDir() { | |
| dirs = append(dirs, e) | |
| } else { | |
| files = append(files, e) | |
| } | |
| } | |
| for _, e := range dirs { | |
| name := e.Name() | |
| fmt.Fprintf(w, `<li><a href="%s/" class="dir">%s/</a></li> | |
| `, htmlEscape(name), htmlEscape(name)) | |
| } | |
| for _, e := range files { | |
| name := e.Name() | |
| info, _ := e.Info() | |
| size := "" | |
| if info != nil { | |
| size = fmt.Sprintf(`<span class="size">(%s)</span>`, formatSize(info.Size())) | |
| } | |
| fmt.Fprintf(w, `<li><a href="%s">%s</a>%s</li> | |
| `, htmlEscape(name), htmlEscape(name), size) | |
| } | |
| fmt.Fprintf(w, `</ul> | |
| <div style="color: #666; font-size: .85em; text-align: center;">Made in Colorado <a href="https://github.com/russianryebread" target="_blank">🏔</a></div> | |
| </body> | |
| </html>`) | |
| } | |
| func isHidden(e os.DirEntry) bool { | |
| name := e.Name() | |
| if name == "" { | |
| return false | |
| } | |
| // Hide dotfiles and well-known metadata dirs. | |
| if name[0] == '.' { | |
| return true | |
| } | |
| if name == "__MACOSX" { | |
| return true | |
| } | |
| return false | |
| } | |
| func formatSize(n int64) string { | |
| switch { | |
| case n < 1024: | |
| return fmt.Sprintf("%d B", n) | |
| case n < 1024*1024: | |
| return fmt.Sprintf("%.1f KB", float64(n)/1024) | |
| default: | |
| return fmt.Sprintf("%.1f MB", float64(n)/(1024*1024)) | |
| } | |
| } | |
| func htmlEscape(s string) string { | |
| s = strings.ReplaceAll(s, "&", "&") | |
| s = strings.ReplaceAll(s, "<", "<") | |
| s = strings.ReplaceAll(s, ">", ">") | |
| s = strings.ReplaceAll(s, `"`, """) | |
| return s | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment