Skip to content

Instantly share code, notes, and snippets.

@urandom
Created July 16, 2015 11:39
Show Gist options
  • Save urandom/5aa718f653b7b898b73c to your computer and use it in GitHub Desktop.
Save urandom/5aa718f653b7b898b73c to your computer and use it in GitHub Desktop.
Simple static file and cgi server in go
package main
import (
"bytes"
"flag"
"fmt"
"io/ioutil"
"net/http"
"net/http/cgi"
"os"
"path"
"path/filepath"
"sort"
"strings"
"text/template"
"time"
)
var (
port int
autoindex bool
cgiURL, cgiDir, htdocsDir string
staticTmpl *template.Template
)
type CGIHandler struct {
http.Handler
cgiURL, cgiDir, htdocsDir, wd string
}
type fileStats []os.FileInfo
type fileList struct {
CurDir string
Stats fileStats
}
func (h *CGIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rPath := filepath.Clean(filepath.FromSlash(r.URL.Path))
if len(rPath) > 0 && os.IsPathSeparator(rPath[len(rPath)-1]) {
rPath = rPath[:len(rPath)-1]
}
var isCGI bool
var scriptPath string
if strings.HasPrefix(path.Clean(r.URL.Path), h.cgiURL) {
isCGI = true
}
if isCGI {
scriptPath = r.URL.Path[len(h.cgiURL):]
if i := strings.Index(scriptPath, "/"); i > -1 {
scriptPath = scriptPath[:i]
rPath = rPath[len(h.cgiURL):]
rPath = rPath[:strings.Index(rPath, "/")]
rPath = filepath.Join(h.cgiDir, rPath)
} else {
rPath = filepath.Join(h.cgiDir, rPath[len(h.cgiURL):])
}
scriptPath = path.Join(h.cgiURL, scriptPath)
} else {
rPath = filepath.Join(h.htdocsDir, rPath)
}
st, err := os.Stat(rPath)
if err != nil {
http.NotFound(w, r)
fmt.Fprintf(os.Stderr, "Error stat-ing '%s': %v\n", rPath, err)
return
}
if isCGI {
var cgih cgi.Handler
cgih = cgi.Handler{
Path: rPath,
Root: scriptPath,
Env: []string{"DOCUMENT_ROOT=" + h.htdocsDir},
}
cgih.ServeHTTP(w, r)
} else {
if st.IsDir() {
iPath := filepath.Join(rPath, "index.html")
st, err = os.Stat(iPath)
if err != nil {
if autoindex {
if !strings.HasSuffix(r.URL.Path, "/") {
http.Redirect(w, r, r.URL.Path+"/", http.StatusFound)
return
}
stats, err := ioutil.ReadDir(rPath)
if err == nil {
sort.Sort(fileStats(stats))
fileList := &fileList{CurDir: path.Base(rPath), Stats: stats}
var buf bytes.Buffer
if err := staticTmpl.Execute(&buf, fileList); err != nil {
http.NotFound(w, r)
fmt.Fprintf(os.Stderr, "Error executing template: %v\n", err)
return
}
if _, err := buf.WriteTo(w); err != nil {
http.NotFound(w, r)
fmt.Fprintf(os.Stderr, "Error writing buffer: %v\n", err)
return
}
} else {
http.NotFound(w, r)
fmt.Fprintf(os.Stderr, "Error reading '%s': %v\n", iPath, err)
}
} else {
http.NotFound(w, r)
}
return
}
}
if !strings.HasPrefix(rPath, h.htdocsDir) {
http.NotFound(w, r)
return
}
http.ServeFile(w, r, rPath)
}
}
func (fs fileStats) Len() int { return len(fs) }
func (fs fileStats) Swap(i, j int) { fs[i], fs[j] = fs[j], fs[i] }
func (fs fileStats) Less(i, j int) bool { return fs[i].Name() < fs[j].Name() }
func main() {
wd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "Error getting current working dir: %v\n", err)
os.Exit(0)
}
cgiURL = path.Clean(cgiURL)
if len(cgiURL) == 0 {
fmt.Fprintf(os.Stderr, "No cgi url given\n")
os.Exit(0)
}
if cgiURL[0] != '/' {
cgiURL = "/" + cgiURL
}
if !strings.HasSuffix(cgiURL, "/") {
cgiURL = cgiURL + "/"
}
h := &CGIHandler{
cgiURL: cgiURL,
cgiDir: filepath.Join(wd, cgiDir),
htdocsDir: filepath.Join(wd, htdocsDir),
wd: wd,
}
http.ListenAndServe(fmt.Sprintf(":%d", port), h)
}
func init() {
flag.IntVar(&port, "port", 8080, "port")
flag.BoolVar(&autoindex, "autoindex", true, "generate a directory index")
flag.StringVar(&cgiURL, "cgi-url", "/cgi-bin", "cgi url")
flag.StringVar(&cgiDir, "cgi-dir", "cgi-bin", "cgi directory")
flag.StringVar(&htdocsDir, "htdocs-dir", "htdocs", "htdocs directory")
flag.Parse()
if autoindex {
staticTmpl = template.Must(template.New("autoindex").Funcs(template.FuncMap{
"formatdate": func(t time.Time) string {
return t.Format(dateFormat)
},
}).Parse(fileListTemplate))
}
}
const (
dateFormat = "Jan 2, 2006 at 3:04pm (MST)"
fileListTemplate = `
<!doctype html>
<html>
<head>
<title>{{ .CurDir }}</title>
</head>
<body>
<table>
<tbody>
<tr>
<td><a href="../">../</a></td>
<td colspan="2"></td>
</tr>
{{ range .Stats }}
<tr>
<td>
{{ if .IsDir }}
<a href="{{ .Name }}/">{{ .Name }}/</a>
{{ else }}
<a href="{{ .Name }}">{{ .Name }}</a>
{{ end }}
</td>
<td>
{{ .ModTime | formatdate }}
</td>
<td>
{{ .Size }}
</td>
</tr>
{{ end }}
</body>
</html>
`
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment