Skip to content

Instantly share code, notes, and snippets.

@bmcculley
Forked from alexisrobert/webserver.go
Created January 9, 2023 20:42
Show Gist options
  • Save bmcculley/22f2211a71bf5d6e8434901886b02b1a to your computer and use it in GitHub Desktop.
Save bmcculley/22f2211a71bf5d6e8434901886b02b1a to your computer and use it in GitHub Desktop.
Tiny web server in Go for sharing a folder
/* Tiny web server in Golang for sharing a folder
Copyright (c) 2010 Alexis ROBERT <alexis.robert@gmail.com>
Contains some code from Golang's http.ServeFile method, and
uses lighttpd's directory listing HTML template. */
package main
import (
"compress/gzip"
"compress/zlib"
"container/list"
"flag"
"fmt"
"html/template"
"io"
"io/fs"
"mime"
"net/http"
"net/url"
"os"
"path"
"strconv"
"strings"
"time"
)
var (
root_folder *string // TODO: Find a way to be cleaner !
uses_gzip *bool
)
const (
serverUA = "Alexis/0.1"
fs_maxbufsize = 4096 // 4096 bits = default page size on OSX
)
/* Go is the first programming language with a templating engine embeddeed
* but with no min function. */
func min(x int64, y int64) int64 {
if x < y {
return x
}
return y
}
func main() {
// Get current working directory to get the file from it
cwd, err := os.Getwd()
if err != nil {
fmt.Printf("Error while getting current directory.")
return
}
// Command line parsing
bind := flag.String("bind", ":8080", "Bind address")
root_folder = flag.String("root", cwd, "Root folder")
// TODO: investigate why gzip isn't working properly
// setting this to false
uses_gzip = flag.Bool("gzip", false, "Enables gzip/zlib compression")
flag.Parse()
http.Handle("/", http.HandlerFunc(handleFile))
fmt.Printf("Sharing %s on %s ...\n", *root_folder, *bind)
http.ListenAndServe((*bind), nil)
}
// Manages directory listings
type DirListing struct {
Name string
Children_dir []string
Children_files []string
ServerUA string
}
func copyToArray(src *list.List) []string {
dst := make([]string, src.Len())
i := 0
for e := src.Front(); e != nil; e = e.Next() {
dst[i] = e.Value.(string)
i = i + 1
}
return dst
}
func handleDirectory(f *os.File, w http.ResponseWriter, req *http.Request) {
names, _ := f.Readdir(-1)
// First, check if there is any index in this folder.
for _, val := range names {
if val.Name() == "index.html" {
serveFile(path.Join(f.Name(), "index.html"), w, req)
return
}
}
// Otherwise, generate folder content.
children_dir_tmp := list.New()
children_files_tmp := list.New()
for _, val := range names {
if val.Name()[0] == '.' {
continue
} // Remove hidden files from listing
if val.IsDir() {
children_dir_tmp.PushBack(val.Name())
} else {
children_files_tmp.PushBack(val.Name())
}
}
// And transfer the content to the final array structure
children_dir := copyToArray(children_dir_tmp)
children_files := copyToArray(children_files_tmp)
tmpl := template.Must(template.New("tpl").Parse(dirlisting_tpl))
data := DirListing{
Name: req.URL.Path,
Children_dir: children_dir,
Children_files: children_files,
ServerUA: serverUA,
}
err := tmpl.Execute(w, data)
if err != nil {
http.Error(w, "500 Internal Error: Error executing the template.", 500)
}
}
func serveFile(filepath string, w http.ResponseWriter, req *http.Request) {
// Opening the file handle
f, err := os.Open(filepath)
if err != nil {
http.Error(w, "404 Not Found : Error while opening the file.", 404)
return
}
defer f.Close()
// Checking if the opened handle is really a file
statinfo, err := f.Stat()
if err != nil {
http.Error(w, "500 Internal Error : stat() failure.", 500)
return
}
if statinfo.IsDir() { // If it's a directory, open it !
handleDirectory(f, w, req)
return
}
if statinfo.Mode().Type() == fs.ModeSocket { // If it's a socket, forbid it !
http.Error(w, "403 Forbidden : you can't access this resource.", 403)
return
}
// Manages If-Modified-Since and add Last-Modified (taken from Golang code)
if t, _ := time.Parse(http.TimeFormat, req.Header.Get("If-Modified-Since")); !t.IsZero() && t.Equal(time.Unix(0, 0)) {
w.WriteHeader(http.StatusNotModified)
return
}
w.Header().Set("Last-Modified", statinfo.ModTime().Format(http.TimeFormat))
// Content-Type handling
query, err := url.ParseQuery(req.URL.RawQuery)
if err == nil && len(query["dl"]) > 0 { // The user explicitedly wanted to download the file (Dropbox style!)
w.Header().Set("Content-Type", "application/octet-stream")
} else {
// Fetching file's mimetype and giving it to the browser
if mimetype := mime.TypeByExtension(path.Ext(filepath)); mimetype != "" {
w.Header().Set("Content-Type", mimetype)
} else {
w.Header().Set("Content-Type", "application/octet-stream")
}
}
// Add Content-Length
w.Header().Set("Content-Length", strconv.FormatInt(statinfo.Size(), 10))
// Manage Content-Range (TODO: Manage end byte and multiple Content-Range)
if req.Header.Get("Range") != "" {
start_byte := parseRange(req.Header.Get("Range"))
if start_byte < statinfo.Size() {
f.Seek(start_byte, 0)
} else {
start_byte = 0
}
w.Header().Set("Content-Range",
fmt.Sprintf("bytes %d-%d/%d", start_byte, statinfo.Size()-1, statinfo.Size()))
}
// Manage gzip/zlib compression
output_writer := w.(io.Writer)
if (*uses_gzip) == true && req.Header.Get("Accept-Encoding") != "" {
encodings := parseCSV(req.Header.Get("Accept-Encoding"))
for _, val := range encodings {
if val == "gzip" {
w.Header().Set("Accept-Encoding", "gzip")
output_writer, _ = gzip.NewWriterLevel(w, gzip.BestSpeed)
break
} else if val == "deflate" {
w.Header().Set("Accept-Encoding", "deflate")
output_writer, _ = zlib.NewWriterLevel(w, zlib.BestSpeed)
break
}
}
}
// Stream data out !
buf := make([]byte, min(fs_maxbufsize, statinfo.Size()))
n := 0
for err == nil {
n, err = f.Read(buf)
output_writer.Write(buf[0:n])
}
// Closes current compressors
switch output_writer.(type) {
case *gzip.Writer:
output_writer.(*gzip.Writer).Close()
case io.WriteCloser:
output_writer.(io.WriteCloser).Close()
}
f.Close()
}
func handleFile(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Server", serverUA)
filepath := path.Join((*root_folder), path.Clean(req.URL.Path))
serveFile(filepath, w, req)
fmt.Printf("\"%s %s %s\"\n",
req.Method,
req.URL,
req.Proto)
/*,
req.Referer,
req.UserAgent) // TODO: Improve this crappy logging
*/
}
func parseCSV(data string) []string {
splitted := strings.Split(data, ",")
data_tmp := make([]string, len(splitted))
for i, val := range splitted {
data_tmp[i] = strings.TrimSpace(val)
}
return data_tmp
}
func parseRange(data string) int64 {
stop := (int64)(0)
part := 0
for i := 0; i < len(data) && part < 2; i = i + 1 {
if part == 0 { // part = 0 <=> equal isn't met.
if data[i] == '=' {
part = 1
}
continue
}
if part == 1 { // part = 1 <=> we've met the equal, parse beginning
if data[i] == ',' || data[i] == '-' {
part = 2 // part = 2 <=> OK DUDE.
} else {
if 48 <= data[i] && data[i] <= 57 { // If it's a digit ...
// ... convert the char to integer and add it!
stop = (stop * 10) + (((int64)(data[i])) - 48)
} else {
part = 2 // Parsing error! No error needed : 0 = from start.
}
}
}
}
return stop
}
var dirlisting_tpl string = `
<title>Index of {{.Name}}</title>
<style type="text/css">
a, a:active {text-decoration: none; color: blue;}
a:visited {color: #48468F;}
a:hover, a:focus {text-decoration: underline; color: red;}
body {background-color: #F5F5F5;}
h2 {margin-bottom: 12px;}
table {margin-left: 12px;}
th, td { font: 14px monospace; text-align: left;}
th { font-weight: bold; padding-right: 14px; padding-bottom: 3px;}
td {padding-right: 14px;}
td.s, th.s {text-align: right;}
div.list { background-color: white; border-top: 1px solid #646464; border-bottom: 1px solid #646464; padding-top: 10px; padding-bottom: 14px;}
div.foot { font: 14px monospace; color: #787878; padding-top: 4px;}
</style>
</head>
<body>
<h2>Index of {{.Name}}</h2>
<div class="list">
<table summary="Directory Listing" cellpadding="0" cellspacing="0">
<thead><tr><th class="n">Name</th><th class="t">Type</th><th class="dl">Options</th></tr></thead>
<tbody>
<tr><td class="n"><a href="../">Parent Directory</a>/</td><td class="t">Directory</td><td class="dl"></td></tr>
{{range .Children_dir}}
<tr><td class="n"><a href="{{.}}/">{{.}}/</a></td><td class="t">Directory</td><td class="dl"></td></tr>
{{end}}
{{range .Children_files}}
<tr><td class="n"><a href="{{.}}">{{.}}</a></td><td class="t">&nbsp;</td><td class="dl"><a href="{{.}}?dl">Download</a></td></tr>
{{end}}
</tbody>
</table>
</div>
<div class="foot">{{.ServerUA}}</div>
</body>
</html>
`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment