Skip to content

Instantly share code, notes, and snippets.

@EntropyWorks
Forked from alexisrobert/webserver.go
Created August 8, 2017 02:13
Show Gist options
  • Select an option

  • Save EntropyWorks/4b10cc0de0d8f566a91a46b93a65f858 to your computer and use it in GitHub Desktop.

Select an option

Save EntropyWorks/4b10cc0de0d8f566a91a46b93a65f858 to your computer and use it in GitHub Desktop.

Revisions

  1. @alexisrobert alexisrobert revised this gist Jul 24, 2014. 1 changed file with 61 additions and 49 deletions.
    110 changes: 61 additions & 49 deletions webserver.go
    Original file line number Diff line number Diff line change
    @@ -1,12 +1,13 @@
    /* Tiny web server in Golang for sharing a folder
    Copyright (c) 2010 Alexis ROBERT <[email protected]>
    Copyright (c) 2010-2014 Alexis ROBERT <[email protected]>
    Contains some code from Golang's http.ServeFile method, and
    uses lighttpd's directory listing HTML template. */

    package main

    import "http"
    import "net/http"
    import "net/url"
    import "io"
    import "os"
    import "mime"
    @@ -15,7 +16,7 @@ import "fmt"
    import "flag"
    import "strings"
    import "strconv"
    import "template"
    import "text/template"
    import "container/list"
    import "compress/gzip"
    import "compress/zlib"
    @@ -24,7 +25,7 @@ import "time"
    var root_folder *string // TODO: Find a way to be cleaner !
    var uses_gzip *bool

    const serverUA = "Alexis/0.1"
    const serverUA = "Alexis/0.2"
    const fs_maxbufsize = 4096 // 4096 bits = default page size on OSX

    /* Go is the first programming language with a templating engine embeddeed
    @@ -82,7 +83,7 @@ func handleDirectory(f *os.File, w http.ResponseWriter, req *http.Request) {

    // First, check if there is any index in this folder.
    for _, val := range names {
    if val.Name == "index.html" {
    if val.Name() == "index.html" {
    serveFile(path.Join(f.Name(), "index.html"), w, req)
    return
    }
    @@ -93,33 +94,35 @@ func handleDirectory(f *os.File, w http.ResponseWriter, req *http.Request) {
    children_files_tmp := list.New()

    for _, val := range names {
    if val.Name[0] == '.' {
    if val.Name()[0] == '.' {
    continue
    } // Remove hidden files from listing

    if val.IsDirectory() {
    children_dir_tmp.PushBack(val.Name)
    if val.IsDir() {
    children_dir_tmp.PushBack(val.Name())
    } else {
    children_files_tmp.PushBack(val.Name)
    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)
    tpl, err := template.New("tpl").Parse(dirlisting_tpl)
    if err != nil {
    http.Error(w, "500 Internal Error : Error while generating directory listing.", 500)
    fmt.Println(err)
    return
    }

    data := dirlisting{Name: req.URL.Path, ServerUA: serverUA,
    Children_dir: children_dir, Children_files: children_files}

    tpl.Execute(w, data)
    err = tpl.Execute(w, data)
    if err != nil {
    fmt.Println(err)
    }
    }

    func serveFile(filepath string, w http.ResponseWriter, req *http.Request) {
    @@ -139,25 +142,25 @@ func serveFile(filepath string, w http.ResponseWriter, req *http.Request) {
    return
    }

    if statinfo.IsDirectory() { // If it's a directory, open it !
    if statinfo.IsDir() { // If it's a directory, open it !
    handleDirectory(f, w, req)
    return
    }

    if statinfo.IsSocket() { // If it's a socket, forbid it !
    if (statinfo.Mode() &^ 07777) == os.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 != nil && statinfo.Mtime_ns/1e9 <= t.Seconds() {
    if t, err := time.Parse(http.TimeFormat, req.Header.Get("If-Modified-Since")); err == nil && statinfo.ModTime().Unix() <= t.Unix() {
    w.WriteHeader(http.StatusNotModified)
    return
    }
    w.Header().Set("Last-Modified", time.SecondsToUTC(statinfo.Mtime_ns/1e9).Format(http.TimeFormat))
    w.Header().Set("Last-Modified", statinfo.ModTime().Format(http.TimeFormat))

    // Content-Type handling
    query, err := http.ParseQuery(req.URL.RawQuery)
    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")
    @@ -170,59 +173,68 @@ func serveFile(filepath string, w http.ResponseWriter, req *http.Request) {
    }
    }

    // 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 {
    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))
    fmt.Sprintf("bytes %d-%d/%d", start_byte, statinfo.Size()-1, statinfo.Size()))
    }

    // Manage gzip/zlib compression
    output_writer := w.(io.Writer)

    is_compressed_reply := false

    if (*uses_gzip) == true && req.Header.Get("Accept-Encoding") != "" {
    encodings := parseCSV(req.Header.Get("Accept-Encoding"))

    for _, val := range(encodings) {
    for _, val := range encodings {
    if val == "gzip" {
    w.Header().Set("Accept-Encoding", "gzip")
    output_writer,_ = gzip.NewWriterLevel(w, gzip.BestSpeed)
    w.Header().Set("Content-Encoding", "gzip")
    output_writer = gzip.NewWriter(w)

    is_compressed_reply = true

    break
    } else if val == "deflate" {
    w.Header().Set("Accept-Encoding", "deflate")
    output_writer,_ = zlib.NewWriterLevel(w, zlib.BestSpeed)
    w.Header().Set("Content-Encoding", "deflate")
    output_writer = zlib.NewWriter(w)

    is_compressed_reply = true

    break
    }
    }
    }

    if !is_compressed_reply {
    // Add Content-Length
    w.Header().Set("Content-Length", strconv.FormatInt(statinfo.Size(), 10))
    }

    // Stream data out !
    buf := make([]byte, min(fs_maxbufsize, statinfo.Size))
    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()
    switch output_writer.(type) {
    case *gzip.Writer:
    output_writer.(*gzip.Writer).Close()
    case *zlib.Writer:
    output_writer.(*zlib.Writer).Close()
    }

    //w.Flush()
    f.Close()
    }

    @@ -234,18 +246,18 @@ func handleFile(w http.ResponseWriter, req *http.Request) {

    fmt.Printf("\"%s %s %s\" \"%s\" \"%s\"\n",
    req.Method,
    req.URL.Raw,
    req.URL.String(),
    req.Proto,
    req.Referer,
    req.UserAgent) // TODO: Improve this crappy logging
    req.Referer(),
    req.UserAgent()) // TODO: Improve this crappy logging
    }

    func parseCSV(data string) []string {
    splitted := strings.Split(data, ",", -1)
    splitted := strings.SplitN(data, ",", -1)

    data_tmp := make([]string, len(splitted))

    for i, val := range(splitted) {
    for i, val := range splitted {
    data_tmp[i] = strings.TrimSpace(val)
    }

    @@ -284,9 +296,9 @@ func parseRange(data string) int64 {
    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 --!>
    <!-- Modified from lighttpd directory listing -->
    <head>
    <title>Index of [Name]</title>
    <title>Index of {{.Name}}</title>
    <style type="text/css">
    a, a:active {text-decoration: none; color: blue;}
    a:visited {color: #48468F;}
    @@ -303,21 +315,21 @@ div.foot { font: 90% monospace; color: #787878; padding-top: 4px;}
    </style>
    </head>
    <body>
    <h2>Index of [Name]</h2>
    <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">&nbsp;</td><td class="dl"><a href="[@]?dl">Download</a></td></tr>
    [.end]
    {{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>
    <div class="foot">{{.ServerUA}}</div>
    </body>
    </html>`
    </html>`
  2. @alexisrobert alexisrobert created this gist May 20, 2011.
    323 changes: 323 additions & 0 deletions webserver.go
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,323 @@
    /* 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 "http"
    import "io"
    import "os"
    import "mime"
    import "path"
    import "fmt"
    import "flag"
    import "strings"
    import "strconv"
    import "template"
    import "container/list"
    import "compress/gzip"
    import "compress/zlib"
    import "time"

    var root_folder *string // TODO: Find a way to be cleaner !
    var uses_gzip *bool

    const serverUA = "Alexis/0.1"
    const 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())

    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.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) == 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 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 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
    }

    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">&nbsp;</td><td class="dl"><a href="[@]?dl">Download</a></td></tr>
    [.end]
    </tbody>
    </table>
    </div>
    <div class="foot">[ServerUA]</div>
    </body>
    </html>`