-
-
Save bmcculley/22f2211a71bf5d6e8434901886b02b1a to your computer and use it in GitHub Desktop.
Tiny web server in Go for sharing a folder
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
/* Tiny web server in Golang for sharing a folder | |
Copyright (c) 2010 Alexis ROBERT <[email protected]> | |
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"> </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