-
-
Save elimisteve/5226735 to your computer and use it in GitHub Desktop.
This file contains 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" | |
"http" | |
"io" | |
"mime" | |
"os" | |
"path" | |
"strconv" | |
"strings" | |
"template" | |
"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", ":1718", "Bind address") | |
root_folder = flag.String("root", cwd, "Root folder") | |
uses_gzip = flag.Bool("gzip", true, "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()) | |
for i, e := 0, src.Front(); e != nil; i, e = i+1, e.Next() { | |
dst[i] = e.Value.(string) | |
} | |
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.IsDirectory() { | |
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) | |
tpl := template.New(nil) | |
tpl.SetDelims("[", "]") | |
err := tpl.Parse(dirlisting_tpl) | |
if err != nil { | |
http.Error(w, "500 Internal Error : Error while generating directory listing.", 500) | |
return | |
} | |
data := dirlisting{Name: req.URL.Path, ServerUA: serverUA, | |
Children_dir: children_dir, Children_files: children_files} | |
tpl.Execute(w, data) | |
} | |
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.IsDirectory() { // If it's a directory, open it ! | |
handleDirectory(f, w, req) | |
return | |
} | |
if statinfo.IsSocket() { // 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 != nil && statinfo.Mtime_ns/1e9 <= t.Seconds() { | |
w.WriteHeader(http.StatusNotModified) | |
return | |
} | |
w.Header().Set("Last-Modified", time.SecondsToUTC(statinfo.Mtime_ns/1e9).Format(http.TimeFormat)) | |
// Content-Type handling | |
query, err := http.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.Itoa64(statinfo.Size)) | |
// 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 && 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 t := output_writer.(type) { | |
case *gzip.Compressor: | |
output_writer.(*gzip.Compressor).Close() | |
case io.WriteCloser: | |
output_writer.(io.WriteCloser).Close() | |
} | |
//w.Flush() | |
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" "%s" "%s"` + "\n", | |
req.Method, | |
req.URL.Raw, | |
req.Proto, | |
req.Referer, | |
req.UserAgent) // TODO: Improve this crappy logging | |
} | |
func parseCSV(data string) []string { | |
splitted := strings.Split(data, ",", -1) | |
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 '0' <= data[i] && data[i] <= '9' { // 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 | |
} | |
const dirlisting_tpl = `<?xml version="1.0" encoding="iso-8859-1"?> | |
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> | |
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> | |
<!-- Modified from lighttpd directory listing --!> | |
<head> | |
<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: 90% 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: 90% 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> | |
[.repeated section Children_dir] | |
<tr><td class="n"><a href="[@]/">[@]/</a></td><td class="t">Directory</td><td class="dl"></td></tr> | |
[.end] | |
[.repeated section 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