├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── fileserver.go ├── http ├── request.go ├── response.go ├── server.go └── server_test.go ├── main.go ├── run-tests.bash ├── test-fixtures ├── 100.txt ├── a b c (d) │ ├── e f g [h] │ │ └── test.txt │ └── یک.txt ├── date.txt └── results.txt └── tests.bash /.gitignore: -------------------------------------------------------------------------------- 1 | gofile 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.7 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2016-07-21 2 | 3 | - **fileserver** fileserver tries to open the following files before responding 4 | with 404: 5 | 6 | * $uri 7 | * $uri.html 8 | * $uri.htm 9 | * $uri/index.html 10 | * $uri/index.htm 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gofile 2 | 3 | [![GoDoc](https://godoc.org/github.com/siadat/gofile/http?status.svg)](https://godoc.org/github.com/siadat/gofile/http) 4 | [![Build Status](https://travis-ci.org/siadat/gofile.svg?branch=master)](https://travis-ci.org/siadat/gofile) 5 | 6 | A non-blocking directory listing and file server. 7 | It implementats HTTP/1.1 keepalive, chunked transfer, and byte range. 8 | 9 | The HTTP server implementation provides a channel for writing chunked response. It could be used as a library. [Read the API](https://godoc.org/github.com/siadat/gofile/http). 10 | 11 | ![gofile](/../screenshots/screenshot-0.1.0.png?raw=true "gofile") 12 | 13 | ### Usage 14 | 15 | Usage: gofile port [dir] 16 | 17 | Examples: 18 | 19 | gofile 8080 20 | gofile 8080 ~/public 21 | 22 | ### Install 23 | 24 | go get -u github.com/siadat/gofile 25 | 26 | ### HTTP/1.1 implementation checklist 27 | 28 | - [x] GET and HEAD methods 29 | - [x] Support keep-alive connections 30 | - [x] Support chunked transfer encoding 31 | - [x] Requests must include a `Host` header 32 | - [x] Requests with `Connection: close` should be closed 33 | - [x] Support for requests with absolute URLs 34 | - [x] If-Modified-Since support 35 | - [x] Byte range support 36 | - [ ] Transparent response compression 37 | 38 | ### Hacking 39 | 40 | Submit an issue or send a pull request. 41 | Make sure you `./run-tests.bash` to test your patch. 42 | 43 | ### Thanks 44 | 45 | Thanks @valyala for his feature suggestions. Thanks @maruel for reviewing the http package. 46 | -------------------------------------------------------------------------------- /fileserver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io/ioutil" 7 | "mime" 8 | neturl "net/url" 9 | "os" 10 | "path" 11 | fp "path/filepath" 12 | "runtime" 13 | "strings" 14 | "time" 15 | 16 | http "github.com/siadat/gofile/http" 17 | ) 18 | 19 | var ( 20 | rootDir = "" 21 | startTime = time.Now() 22 | history []string 23 | ) 24 | 25 | func htmlLayoutTop(title string) string { 26 | return fmt.Sprintf(` 27 | 28 | 29 | %s | Gofile 30 | 31 | 39 | 40 | `, title) 41 | } 42 | func htmlLayoutBottom() string { 43 | return `` 44 | } 45 | 46 | func htmlLink(href, text string) string { 47 | return fmt.Sprintf("%s", href, text) 48 | } 49 | 50 | func filesLis(fileInfos []os.FileInfo, url string, urlUnescaped string, bodyChan chan []byte) { 51 | bodyChan <- []byte("") 68 | return 69 | } 70 | 71 | func listDirChan(url string, urlUnescaped string, filepath string, res *http.Response) (err error) { 72 | res.Body <- []byte(htmlLayoutTop(url)) 73 | res.Body <- []byte(fmt.Sprintf("

Directory Listing for %s

", urlUnescaped)) 74 | if false { 75 | res.Body <- []byte(fmt.Sprintf("

Uptime:%s Goroutines:%d Requests:%d

", 76 | time.Since(startTime), 77 | runtime.NumGoroutine(), 78 | len(history), 79 | )) 80 | } 81 | 82 | fileInfos, status, errMsg := getFilesInDir(filepath) 83 | filesLis(fileInfos, url, urlUnescaped, res.Body) 84 | res.Body <- []byte(htmlLayoutBottom()) 85 | 86 | if status != 200 { 87 | err = errors.New(errMsg) 88 | // content = "" 89 | } 90 | 91 | return 92 | } 93 | 94 | func getRootDir(optRoot string) (root string) { 95 | var wd string 96 | if len(optRoot) == 0 { 97 | wd, _ = os.Getwd() 98 | } else { 99 | wd = optRoot 100 | } 101 | 102 | root, err := fp.Abs(wd) 103 | 104 | if err != nil { 105 | panic(err) 106 | } 107 | return 108 | } 109 | 110 | func tryFilepaths(filepath string) (filepathRet string, file os.FileInfo, err error) { 111 | filepaths := []string{ 112 | filepath, 113 | fmt.Sprintf("%s.html", filepath), 114 | fmt.Sprintf("%s.htm", filepath), 115 | fmt.Sprintf("%s/index.html", filepath), 116 | fmt.Sprintf("%s/index.htm", filepath), 117 | } 118 | 119 | for _, filepathRet = range filepaths { 120 | file, err = os.Stat(filepathRet) 121 | if err == nil { 122 | return filepathRet, file, nil 123 | } 124 | } 125 | 126 | return filepathRet, file, err 127 | } 128 | 129 | func urlToFilepath(requestURI string) (requestedFilepath string, retErr error) { 130 | // Note: path.Join removes '..', so the HasPrefix check is safe for paths 131 | // that try to traverse parent directory using '..'. 132 | if len(rootDir) == 0 { 133 | rootDir = getRootDir("") 134 | } 135 | requestedFilepath = path.Join(rootDir, requestURI) 136 | 137 | if !strings.HasPrefix(requestedFilepath, rootDir) { 138 | retErr = errors.New("Requested URI is not allowed") 139 | requestedFilepath = "" 140 | return 141 | } 142 | return 143 | } 144 | 145 | func getFilesInDir(requestedFilepath string) (fileInfos []os.FileInfo, status int, errMsg string) { 146 | status = 200 147 | fileInfos, err := ioutil.ReadDir(requestedFilepath) 148 | 149 | if err != nil { 150 | errMsg = "Requested URI was not found." 151 | status = 404 152 | } 153 | 154 | return 155 | } 156 | 157 | func getFilesize(filepath string) (fileSize int64) { 158 | f, err := os.Open(filepath) 159 | if err != nil { 160 | return 161 | } 162 | defer f.Close() 163 | 164 | if fi, err := f.Stat(); err == nil { 165 | fileSize = fi.Size() 166 | } 167 | return 168 | } 169 | 170 | func downloadFileChan(filepath string, ranges []http.ByteRange, res *http.Response) (err error) { 171 | // NOTE at the moment we are respecting the first range only 172 | rangeFrom := ranges[0].Start 173 | rangeTo := ranges[0].End 174 | 175 | f, err := os.Open(filepath) 176 | if err != nil { 177 | return 178 | } 179 | defer f.Close() 180 | 181 | var fileSize int64 182 | if fi, err := f.Stat(); err == nil { 183 | fileSize = fi.Size() 184 | } 185 | 186 | if rangeTo < 0 { 187 | rangeTo = fileSize + rangeTo 188 | } 189 | 190 | if rangeFrom < 0 { 191 | rangeFrom = fileSize + rangeFrom 192 | } 193 | 194 | rangeFromNew, err := f.Seek(rangeFrom, 0) 195 | if err != nil { 196 | f.Close() 197 | return 198 | } 199 | 200 | if rangeTo < rangeFromNew { 201 | rangeTo = rangeFromNew 202 | } 203 | 204 | var maxFileReadLen int64 = 1024 * 1024 205 | for cursorFrom := rangeFromNew; cursorFrom <= rangeTo; cursorFrom += maxFileReadLen { 206 | cursorTo := cursorFrom + maxFileReadLen - 1 207 | if cursorTo > rangeTo { 208 | cursorTo = rangeTo 209 | } 210 | buff := make([]byte, cursorTo-cursorFrom+1) 211 | 212 | _, err := f.Seek(cursorFrom, 0) 213 | if err != nil { 214 | break 215 | } 216 | 217 | readN, readErr := f.Read(buff) 218 | if readErr != nil { 219 | break 220 | } 221 | if readN == 0 { 222 | break 223 | } 224 | 225 | res.Body <- buff[:readN] 226 | // time.Sleep(time.Millisecond * 10) 227 | } 228 | 229 | return 230 | } 231 | 232 | func fileServerHandleRequestGen(optRoot string) func(http.Request, *http.Response) { 233 | rootDir = getRootDir(optRoot) 234 | return fileServerHandleRequest 235 | } 236 | 237 | func fileServerHandleRequest(req http.Request, res *http.Response) { 238 | history = append(history, req.URL.Path) 239 | 240 | unescapedURL, _ := neturl.QueryUnescape(req.URL.Path) 241 | filepath, err := urlToFilepath(unescapedURL) 242 | if err != nil { 243 | res.Status = 401 244 | defer close(res.Body) 245 | res.Body <- []byte(err.Error() + "\n") 246 | return 247 | } 248 | 249 | filepath, file, err := tryFilepaths(filepath) 250 | if err != nil { 251 | res.Status = 404 252 | defer close(res.Body) 253 | res.Body <- []byte("") 254 | return 255 | } 256 | 257 | if file.IsDir() { 258 | res.Status = 200 259 | res.ContentType = "text/html" 260 | defer close(res.Body) 261 | err = listDirChan(req.URL.Path, unescapedURL, filepath, res) 262 | // if err != nil { res.Status = 400 } 263 | return 264 | } 265 | 266 | res.Status = 200 267 | fileIsModified := true 268 | if len(req.Headers["If-Modified-Since"]) > 0 { 269 | ifModifiedSince := http.ParseHTTPDate(req.Headers["If-Modified-Since"]) 270 | if !file.ModTime().After(ifModifiedSince) { 271 | fileIsModified = false 272 | } 273 | } 274 | 275 | if fileIsModified { 276 | res.ContentType = mime.TypeByExtension(fp.Ext(filepath)) 277 | res.ContentLength = getFilesize(filepath) 278 | err = downloadFileChan(filepath, req.Ranges, res) 279 | } else { 280 | res.Status = 304 281 | } 282 | 283 | defer close(res.Body) 284 | 285 | if err != nil { 286 | res.Status = 400 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /http/request.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | type Request struct { 13 | // Method is the HTTP/1.1 method name. 14 | Method string 15 | 16 | // URL is the URL requested by the client. 17 | URL *url.URL 18 | 19 | // Headers contains pairs of the header key-values. 20 | Headers map[string]string 21 | 22 | // Ranges contains all the requested byte-ranges. 23 | Ranges []ByteRange 24 | 25 | isRanged bool 26 | } 27 | 28 | // ByteRange is the parsed ranges requested by the client. 29 | // For example, the client could request the following byte range: 30 | // 31 | // Range: bytes=100- 32 | // 33 | // This header is parsed with 100 and -1 (unspecified) being the start and end of the range. 34 | type ByteRange struct { 35 | Start int64 36 | End int64 37 | } 38 | 39 | // Length is the total number of bytes included in the range. 40 | func (br ByteRange) Length() int64 { 41 | return br.End - br.Start + 1 42 | } 43 | 44 | func parseByteRangeHeader(headerValue string) (byteRanges []ByteRange, explicit bool) { 45 | rangePrefix := "bytes=" 46 | byteRanges = make([]ByteRange, 0) 47 | 48 | if !strings.HasPrefix(headerValue, rangePrefix) { 49 | byteRanges = append(byteRanges, ByteRange{Start: 0, End: -1}) 50 | return 51 | } 52 | 53 | explicit = true 54 | 55 | headerValue = headerValue[len(rangePrefix):] 56 | for _, value := range strings.Split(headerValue, ",") { 57 | 58 | // Let's say we have 10 bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 59 | // -10 => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 60 | // -7 => [3, 4, 5, 6, 7, 8, 9] 61 | // -2 => [8, 9] 62 | // -1 => [9] 63 | if val, err := strconv.ParseInt(value, 10, 0); err == nil && val < 0 { 64 | byteRanges = append(byteRanges, ByteRange{Start: val, End: -1}) 65 | continue 66 | } 67 | 68 | // 0- => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] 69 | // 3- => [3, 4, 5, 6, 7, 8, 9] 70 | if strings.HasSuffix(value, "-") { 71 | if val, err := strconv.ParseInt(value[:len(value)-1], 10, 0); err == nil { 72 | byteRanges = append(byteRanges, ByteRange{Start: val, End: -1}) 73 | } 74 | continue 75 | } 76 | 77 | // 1-1 => [1, 1] 78 | // 3-6 => [3, 4, 5, 6] 79 | rangeVals := strings.Split(value, "-") 80 | val1, err1 := strconv.ParseInt(rangeVals[0], 10, 0) 81 | val2, err2 := strconv.ParseInt(rangeVals[1], 10, 0) 82 | if err1 == nil && err2 == nil { 83 | byteRanges = append(byteRanges, ByteRange{Start: val1, End: val2}) 84 | } 85 | } 86 | return 87 | } 88 | 89 | // ParseHTTPDate is a helper for parsing HTTP-dates. 90 | func ParseHTTPDate(date string) (t time.Time) { 91 | t, err := time.Parse(httpTimeFormat, date) 92 | if err != nil { 93 | fmt.Println("error parsing", err) 94 | } 95 | return 96 | } 97 | 98 | func (req *Request) parseInitialLine(line string) (err error) { 99 | words := strings.SplitN(line, " ", 3) 100 | 101 | if len(words) < 3 { 102 | return errors.New("Invalid initial request line.") 103 | } 104 | 105 | if words[2] != "HTTP/1.1" { 106 | return errors.New("Invalid initial request line.") 107 | } 108 | 109 | req.Method = words[0] 110 | req.URL, _ = url.Parse(words[1]) 111 | 112 | return 113 | } 114 | 115 | func (req *Request) parseHeaders(headerLines []string) { 116 | for _, headerLine := range headerLines { 117 | headerPair := strings.SplitN(headerLine, ": ", 2) 118 | if len(headerPair) == 2 { 119 | req.Headers[headerPair[0]] = headerPair[1] 120 | } 121 | } 122 | req.Ranges, req.isRanged = parseByteRangeHeader(req.Headers["Range"]) 123 | } 124 | -------------------------------------------------------------------------------- /http/response.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "runtime" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | type Response struct { 13 | // Status is the response status code sent to the client. 14 | Status int 15 | 16 | // Body is the channel used by the server to write the content of the 17 | // response body. Each received array of byte is sent to the client as a HTTP 18 | // body chunk. 19 | Body chan []byte 20 | 21 | // ContentType is the MIME type of the response. The default ContentType is 22 | // text/plain. 23 | ContentType string 24 | 25 | // ContentLength is the length of the response. 26 | ContentLength int64 27 | 28 | conn net.Conn 29 | connID uint32 30 | } 31 | 32 | const ( 33 | crlf = "\r\n" 34 | httpTimeFormat = "Mon, 02 Jan 2006 15:04:05 MST" 35 | chunkLen = 1024 36 | version = "0.3.0" 37 | ) 38 | 39 | var responsePhrases = map[int]string{ 40 | 100: "Continue", 41 | 101: "Switching Protocols", 42 | 200: "OK", 43 | 201: "Created", 44 | 202: "Accepted", 45 | 203: "Non-Authoritative Information", 46 | 204: "No Content", 47 | 205: "Reset Content", 48 | 206: "Partial Content", 49 | 300: "Multiple Choices", 50 | 301: "Moved Permanently", 51 | 302: "Found", 52 | 303: "See Other", 53 | 304: "Not Modified", 54 | 305: "Use Proxy", 55 | 307: "Temporary Redirect", 56 | 400: "Bad Request", 57 | 401: "Unauthorized", 58 | 402: "Payment Required", 59 | 403: "Forbidden", 60 | 404: "Not Found", 61 | 405: "Method Not Allowed", 62 | 406: "Not Acceptable", 63 | 407: "Proxy Authentication Required", 64 | 408: "Request Time-out", 65 | 409: "Conflict", 66 | 410: "Gone", 67 | 411: "Length Required", 68 | 412: "Precondition Failed", 69 | 413: "Request Entity Too Large", 70 | 414: "Request-URI Too Large", 71 | 415: "Unsupported Media Type", 72 | 416: "Requested range not satisfiable", 73 | 417: "Expectation Failed", 74 | 500: "Internal Server Error", 75 | 501: "Not Implemented", 76 | 502: "Bad Gateway", 77 | 503: "Service Unavailable", 78 | 504: "Gateway Time-out", 79 | 505: "HTTP Version not supported", 80 | } 81 | 82 | func (res *Response) respondOther(req Request) { 83 | respond(req, res) 84 | } 85 | 86 | func respondHead(req Request, res *Response) { 87 | var headers []string 88 | 89 | if res.Status == 0 { 90 | res.Status = 200 91 | } 92 | 93 | if req.isRanged && res.Status == 200 { 94 | res.Status = 206 95 | } 96 | 97 | r := req.Ranges[0] 98 | if res.ContentLength > 0 { 99 | if r.End < 0 { 100 | r.End = res.ContentLength + r.End 101 | } 102 | if r.Start < 0 { 103 | r.Start = res.ContentLength + r.Start 104 | } 105 | if r.Start > r.End { 106 | res.Status = 416 107 | } 108 | } 109 | 110 | headers = append(headers, fmt.Sprintf("HTTP/1.1 %d %s", res.Status, responsePhrases[res.Status])) 111 | 112 | if req.isRanged && res.ContentLength > 0 { 113 | headers = append(headers, fmt.Sprintf("Content-Range: %s-%s/%d", 114 | fmt.Sprintf("%d", r.Start), 115 | fmt.Sprintf("%d", r.End), 116 | res.ContentLength)) 117 | } 118 | 119 | if len(res.ContentType) == 0 { 120 | res.ContentType = "text/plain" 121 | } 122 | 123 | headers = append(headers, 124 | "Connection: keep-alive", 125 | "Accept-Ranges: byte", 126 | fmt.Sprintf("Content-Type: %s", res.ContentType), 127 | fmt.Sprintf("Server: Gofile/%s %s", version, runtime.Version()), 128 | fmt.Sprintf("Date: %s", time.Now().UTC().Format(httpTimeFormat)), 129 | ) 130 | 131 | headers = append(headers, fmt.Sprintf("Transfer-Encoding: %s", "chunked")) 132 | 133 | if res.ContentLength > 0 { 134 | headers = append(headers, fmt.Sprintf("Content-Length: %d", r.Length())) 135 | } 136 | 137 | if verbose { 138 | log.Println(strings.Join(headers, crlf) + crlf + crlf) 139 | } 140 | res.conn.Write(([]byte)(strings.Join(headers, crlf) + crlf + crlf)) 141 | } 142 | 143 | func respond(req Request, res *Response) { 144 | from := 0 145 | var chunkBuff []byte 146 | noWriteYet := true 147 | 148 | for content := range res.Body { 149 | if noWriteYet { 150 | noWriteYet = false 151 | respondHead(req, res) 152 | switch res.Status { 153 | case 304, 501: 154 | break 155 | } 156 | } 157 | 158 | if len(chunkBuff)+len(content) > chunkLen && len(chunkBuff) > 0 { 159 | to := from + len(chunkBuff) 160 | err := writeToConn(res.conn, chunkBuff, from, to) 161 | if err != nil { 162 | fmt.Println("Socket Write Error > ", err) 163 | break 164 | } 165 | from = 0 166 | chunkBuff = []byte{} 167 | } 168 | chunkBuff = append(chunkBuff, content...) 169 | } 170 | 171 | if len(chunkBuff) > 0 { 172 | writeToConn(res.conn, chunkBuff, 0, len(chunkBuff)) 173 | } 174 | 175 | if noWriteYet { 176 | respondHead(req, res) 177 | } else { 178 | res.conn.Write(([]byte)(fmt.Sprintf("%d%s%s", 0, crlf, crlf))) 179 | } 180 | 181 | if req.Headers["Connection"] == "close" { 182 | res.conn.Close() 183 | } 184 | } 185 | 186 | func writeToConn(conn net.Conn, content []byte, from int, to int) (err error) { 187 | written := []byte(fmt.Sprintf("%x%s", to-from, crlf)) 188 | written = append(written, content...) 189 | written = append(written, []byte(fmt.Sprintf("%s", crlf))...) 190 | _, err = conn.Write(written) 191 | return 192 | } 193 | -------------------------------------------------------------------------------- /http/server.go: -------------------------------------------------------------------------------- 1 | // Package http implements an HTTP/1.1 server. 2 | package http 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "log" 8 | "math/rand" 9 | "net" 10 | "strings" 11 | ) 12 | 13 | const ( 14 | reqBuffLen = 2 * 1024 15 | reqMaxBuffLen = 64 * 1024 16 | ) 17 | 18 | var ( 19 | socketCounter = 0 20 | verbose = false 21 | ) 22 | 23 | // Server defines the Handler used by Serve. 24 | type Server struct { 25 | Handler func(Request, *Response) 26 | } 27 | 28 | // Serve starts the HTTP server listening on port. For each request, handle is 29 | // called with the parsed request and response in their own goroutine. 30 | func (s Server) Serve(ln net.Listener) error { 31 | r := rand.New(rand.NewSource(99)) 32 | 33 | for { 34 | conn, err := ln.Accept() 35 | if err != nil { 36 | log.Println("Error while accepting new connection", err) 37 | continue 38 | } 39 | 40 | socketCounter++ 41 | if verbose { 42 | log.Println("handleConnection #", socketCounter) 43 | } 44 | req := Request{Headers: make(map[string]string)} 45 | res := Response{conn: conn, connID: r.Uint32()} 46 | go handleConnection(req, &res, s.Handler) 47 | } 48 | return nil 49 | } 50 | 51 | func readRequest(req Request, res *Response) (requestBuff []byte, err error) { 52 | requestBuff = make([]byte, 0, 8*1024) 53 | var reqLen int 54 | 55 | for { 56 | buff := make([]byte, reqBuffLen) 57 | reqLen, err = res.conn.Read(buff) 58 | requestBuff = append(requestBuff, buff[:reqLen]...) 59 | 60 | if len(requestBuff) > reqMaxBuffLen { 61 | log.Println("Request is too big, ignoring the rest.") 62 | break 63 | } 64 | 65 | if err != nil && err != io.EOF { 66 | log.Println("Connection error:", err) 67 | break 68 | } 69 | 70 | if err == io.EOF || reqLen < reqBuffLen { 71 | break 72 | } 73 | } 74 | return 75 | } 76 | 77 | func handleConnection(req Request, res *Response, handle func(Request, *Response)) { 78 | defer func() { 79 | socketCounter-- 80 | if verbose { 81 | log.Println(fmt.Sprintf("Closing socket:%d. Total connections:%d", res.connID, socketCounter)) 82 | } 83 | }() 84 | 85 | for { 86 | requestBuff, err := readRequest(req, res) 87 | 88 | if len(requestBuff) == 0 { 89 | return 90 | } 91 | 92 | if err != nil && err != io.EOF { 93 | log.Println("Error while reading socket:", err) 94 | return 95 | } 96 | 97 | if verbose { 98 | log.Println(string(requestBuff[0:])) 99 | } 100 | 101 | requestLines := strings.Split(string(requestBuff[0:]), crlf) 102 | req.parseHeaders(requestLines[1:]) 103 | err = req.parseInitialLine(requestLines[0]) 104 | 105 | res.Body = make(chan []byte) 106 | go res.respondOther(req) 107 | 108 | if err != nil { 109 | res.Status = 400 110 | res.ContentType = "text/plain" 111 | 112 | res.Body <- []byte(err.Error() + "\n") 113 | close(res.Body) 114 | 115 | continue 116 | } 117 | 118 | requestIsValid := true 119 | log.Println(fmt.Sprintf("%s", requestLines[0])) 120 | 121 | if len(req.Headers["Host"]) == 0 { 122 | res.ContentType = "text/plain" 123 | res.Status = 400 124 | close(res.Body) 125 | requestIsValid = false 126 | } 127 | 128 | switch req.Method { 129 | case "GET", "HEAD": 130 | default: 131 | res.ContentType = "text/plain" 132 | res.Status = 501 133 | close(res.Body) 134 | requestIsValid = false 135 | } 136 | 137 | if requestIsValid { 138 | if req.Method == "HEAD" { 139 | close(res.Body) 140 | } else { 141 | handle(req, res) 142 | } 143 | } 144 | 145 | if req.Headers["Connection"] == "close" { 146 | break 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /http/server_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "testing" 8 | ) 9 | 10 | func ExampleServe(t *testing.T) { 11 | server := Server{Handler: func(req Request, res *Response) { 12 | defer close(res.Body) 13 | res.Body <- []byte(fmt.Sprintf("You requested %v", req.URL)) 14 | }} 15 | ln, err := net.Listen("tcp", ":8080") 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | server.Serve(ln) 20 | } 21 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net" 8 | "strconv" 9 | 10 | http "github.com/siadat/gofile/http" 11 | ) 12 | 13 | const usage = `Usage: gofile port [dir]` 14 | 15 | var optRoot = "" 16 | 17 | func main() { 18 | flag.Usage = func() { 19 | log.Fatal(usage) 20 | } 21 | 22 | flag.Parse() 23 | 24 | if flag.NArg() < 1 || flag.NArg() > 2 { 25 | flag.Usage() 26 | } 27 | 28 | port, err := strconv.Atoi(flag.Args()[0]) 29 | if err != nil { 30 | log.Fatal("Bad port number:", flag.Args()[0]) 31 | } 32 | 33 | if flag.NArg() == 2 { 34 | optRoot = flag.Args()[1] 35 | } 36 | 37 | ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | 42 | server := http.Server{Handler: fileServerHandleRequestGen(optRoot)} 43 | log.Println("Starting server on port", port) 44 | server.Serve(ln) 45 | } 46 | -------------------------------------------------------------------------------- /run-tests.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | file=$(mktemp) 3 | trap "rm $file" EXIT 4 | 5 | bash tests.bash > "$file" 6 | vimdiff <(cat "$file" \ 7 | | perl -pe 's/^Date: \w{3}, \d{2} \w{3} \d{4} \d\d:\d\d:\d\d UTC/Date: [FILTERED BY TEST SCRIPT]/' \ 8 | ) test-fixtures/results.txt 9 | -------------------------------------------------------------------------------- /test-fixtures/100.txt: -------------------------------------------------------------------------------- 1 | A123456789B123456789C123456789D123456789E123456789F123456789G123456789H123456789I123456789J123456789 -------------------------------------------------------------------------------- /test-fixtures/a b c (d)/e f g [h]/test.txt: -------------------------------------------------------------------------------- 1 | world 2 | -------------------------------------------------------------------------------- /test-fixtures/a b c (d)/یک.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /test-fixtures/date.txt: -------------------------------------------------------------------------------- 1 | Tue Mar 1 10:13:47 2016 2 | -------------------------------------------------------------------------------- /test-fixtures/results.txt: -------------------------------------------------------------------------------- 1 | 2 | Valid: relative url => 200 3 | ==================================== 4 | 5 | GET / HTTP/1.1 6 | Host: localhost 7 | Connection: close 8 | 9 | HTTP/1.1 200 OK 10 | Connection: keep-alive 11 | Accept-Ranges: byte 12 | Content-Type: text/html 13 | Server: Gofile/0.3.0 go1.7rc1 14 | Date: [FILTERED BY TEST SCRIPT] 15 | Transfer-Encoding: chunked 16 | 17 | 3d5 18 | 19 | 20 | 21 | Gofile 22 | 23 | 31 | 32 |

Directory Listing for /

46 | 0 47 | 48 | 49 | HEAD / HTTP/1.1 50 | Host: localhost 51 | Connection: close 52 | 53 | HTTP/1.1 200 OK 54 | Connection: keep-alive 55 | Accept-Ranges: byte 56 | Content-Type: text/plain 57 | Server: Gofile/0.3.0 go1.7rc1 58 | Date: [FILTERED BY TEST SCRIPT] 59 | Transfer-Encoding: chunked 60 | 61 | 62 | Valid: modified, normal response => 200 63 | ==================================== 64 | 65 | GET /test-fixtures/date.txt HTTP/1.1 66 | Host: localhost 67 | If-Modified-Since: Sun, 09 Sep 2001 06:16:40 IRDT 68 | Connection: close 69 | 70 | HTTP/1.1 200 OK 71 | Connection: keep-alive 72 | Accept-Ranges: byte 73 | Content-Type: text/plain; charset=utf-8 74 | Server: Gofile/0.3.0 go1.7rc1 75 | Date: [FILTERED BY TEST SCRIPT] 76 | Transfer-Encoding: chunked 77 | Content-Length: 24 78 | 79 | 18 80 | Tue Mar 1 10:13:47 2016 81 | 82 | 0 83 | 84 | 85 | Valid: not modified, body should be empty => 304 86 | ==================================== 87 | 88 | GET /test-fixtures/date.txt HTTP/1.1 89 | Host: localhost 90 | If-Modified-Since: Wed, 14 Mar 2255 19:30:00 IRST 91 | Connection: close 92 | 93 | HTTP/1.1 304 Not Modified 94 | Connection: keep-alive 95 | Accept-Ranges: byte 96 | Content-Type: text/plain 97 | Server: Gofile/0.3.0 go1.7rc1 98 | Date: [FILTERED BY TEST SCRIPT] 99 | Transfer-Encoding: chunked 100 | 101 | 102 | Valid: range header => 200 103 | ==================================== 104 | 105 | GET /test-fixtures/100.txt HTTP/1.1 106 | Host: localhost 107 | Range: bytes=-10 108 | Connection: close 109 | 110 | HTTP/1.1 206 Partial Content 111 | Content-Range: 90-99/100 112 | Connection: keep-alive 113 | Accept-Ranges: byte 114 | Content-Type: text/plain; charset=utf-8 115 | Server: Gofile/0.3.0 go1.7rc1 116 | Date: [FILTERED BY TEST SCRIPT] 117 | Transfer-Encoding: chunked 118 | Content-Length: 10 119 | 120 | a 121 | J123456789 122 | 0 123 | 124 | 125 | GET /test-fixtures/100.txt HTTP/1.1 126 | Host: localhost 127 | Range: bytes=90- 128 | Connection: close 129 | 130 | HTTP/1.1 206 Partial Content 131 | Content-Range: 90-99/100 132 | Connection: keep-alive 133 | Accept-Ranges: byte 134 | Content-Type: text/plain; charset=utf-8 135 | Server: Gofile/0.3.0 go1.7rc1 136 | Date: [FILTERED BY TEST SCRIPT] 137 | Transfer-Encoding: chunked 138 | Content-Length: 10 139 | 140 | a 141 | J123456789 142 | 0 143 | 144 | 145 | GET /test-fixtures/100.txt HTTP/1.1 146 | Host: localhost 147 | Range: bytes=10-10 148 | Connection: close 149 | 150 | HTTP/1.1 206 Partial Content 151 | Content-Range: 10-10/100 152 | Connection: keep-alive 153 | Accept-Ranges: byte 154 | Content-Type: text/plain; charset=utf-8 155 | Server: Gofile/0.3.0 go1.7rc1 156 | Date: [FILTERED BY TEST SCRIPT] 157 | Transfer-Encoding: chunked 158 | Content-Length: 1 159 | 160 | 1 161 | B 162 | 0 163 | 164 | 165 | GET /test-fixtures/100.txt HTTP/1.1 166 | Host: localhost 167 | Range: bytes=10-20 168 | Connection: close 169 | 170 | HTTP/1.1 206 Partial Content 171 | Content-Range: 10-20/100 172 | Connection: keep-alive 173 | Accept-Ranges: byte 174 | Content-Type: text/plain; charset=utf-8 175 | Server: Gofile/0.3.0 go1.7rc1 176 | Date: [FILTERED BY TEST SCRIPT] 177 | Transfer-Encoding: chunked 178 | Content-Length: 11 179 | 180 | b 181 | B123456789C 182 | 0 183 | 184 | 185 | GET /test-fixtures/100.txt HTTP/1.1 186 | Host: localhost 187 | Range: bytes=-1 188 | Connection: close 189 | 190 | HTTP/1.1 206 Partial Content 191 | Content-Range: 99-99/100 192 | Connection: keep-alive 193 | Accept-Ranges: byte 194 | Content-Type: text/plain; charset=utf-8 195 | Server: Gofile/0.3.0 go1.7rc1 196 | Date: [FILTERED BY TEST SCRIPT] 197 | Transfer-Encoding: chunked 198 | Content-Length: 1 199 | 200 | 1 201 | 9 202 | 0 203 | 204 | 205 | GET /test-fixtures/100.txt HTTP/1.1 206 | Host: localhost 207 | Range: bytes=0-10,20- 208 | Connection: close 209 | 210 | HTTP/1.1 206 Partial Content 211 | Content-Range: 0-10/100 212 | Connection: keep-alive 213 | Accept-Ranges: byte 214 | Content-Type: text/plain; charset=utf-8 215 | Server: Gofile/0.3.0 go1.7rc1 216 | Date: [FILTERED BY TEST SCRIPT] 217 | Transfer-Encoding: chunked 218 | Content-Length: 11 219 | 220 | b 221 | A123456789B 222 | 0 223 | 224 | 225 | Valid: absolute url => 200 226 | ==================================== 227 | 228 | GET http://localhost:8080/ HTTP/1.1 229 | Host: localhost 230 | Connection: close 231 | 232 | HTTP/1.1 200 OK 233 | Connection: keep-alive 234 | Accept-Ranges: byte 235 | Content-Type: text/html 236 | Server: Gofile/0.3.0 go1.7rc1 237 | Date: [FILTERED BY TEST SCRIPT] 238 | Transfer-Encoding: chunked 239 | 240 | 3d5 241 | 242 | 243 | 244 | Gofile 245 | 246 | 254 | 255 |

Directory Listing for /

269 | 0 270 | 271 | 272 | Valid: should not be chunked, no Transfer-Encoding header, must have Content-Length => 200 273 | ==================================== 274 | 275 | GET /test-fixtures/date.txt HTTP/1.1 276 | Host: localhost 277 | Connection: close 278 | 279 | HTTP/1.1 200 OK 280 | Connection: keep-alive 281 | Accept-Ranges: byte 282 | Content-Type: text/plain; charset=utf-8 283 | Server: Gofile/0.3.0 go1.7rc1 284 | Date: [FILTERED BY TEST SCRIPT] 285 | Transfer-Encoding: chunked 286 | Content-Length: 24 287 | 288 | 18 289 | Tue Mar 1 10:13:47 2016 290 | 291 | 0 292 | 293 | 294 | Valid: special characters => 200 295 | ==================================== 296 | 297 | GET /test-fixtures/a+b+c+(d)/یک.txt HTTP/1.1 298 | Host: localhost 299 | Connection: close 300 | 301 | HTTP/1.1 200 OK 302 | Connection: keep-alive 303 | Accept-Ranges: byte 304 | Content-Type: text/plain; charset=utf-8 305 | Server: Gofile/0.3.0 go1.7rc1 306 | Date: [FILTERED BY TEST SCRIPT] 307 | Transfer-Encoding: chunked 308 | Content-Length: 6 309 | 310 | 6 311 | hello 312 | 313 | 0 314 | 315 | 316 | GET /test-fixtures/a+b+c+(d)/e+f+g+[h]/test.txt HTTP/1.1 317 | Host: localhost 318 | Connection: close 319 | 320 | HTTP/1.1 200 OK 321 | Connection: keep-alive 322 | Accept-Ranges: byte 323 | Content-Type: text/plain; charset=utf-8 324 | Server: Gofile/0.3.0 go1.7rc1 325 | Date: [FILTERED BY TEST SCRIPT] 326 | Transfer-Encoding: chunked 327 | Content-Length: 6 328 | 329 | 6 330 | world 331 | 332 | 0 333 | 334 | 335 | Valid: should not found => 404 336 | ==================================== 337 | 338 | GET /foo HTTP/1.1 339 | Host: localhost 340 | Connection: close 341 | 342 | HTTP/1.1 404 Not Found 343 | Connection: keep-alive 344 | Accept-Ranges: byte 345 | Content-Type: text/plain 346 | Server: Gofile/0.3.0 go1.7rc1 347 | Date: [FILTERED BY TEST SCRIPT] 348 | Transfer-Encoding: chunked 349 | 350 | 0 351 | 352 | 353 | Invalid: no 'Host' header => 400 354 | ==================================== 355 | 356 | GET / HTTP/1.1 357 | Connection: close 358 | 359 | HTTP/1.1 400 Bad Request 360 | Connection: keep-alive 361 | Accept-Ranges: byte 362 | Content-Type: text/plain 363 | Server: Gofile/0.3.0 go1.7rc1 364 | Date: [FILTERED BY TEST SCRIPT] 365 | Transfer-Encoding: chunked 366 | 367 | 368 | Invalid: bad paths => 401 369 | ==================================== 370 | 371 | GET ../ HTTP/1.1 372 | Host: localhost 373 | Connection: close 374 | 375 | HTTP/1.1 401 Unauthorized 376 | Connection: keep-alive 377 | Accept-Ranges: byte 378 | Content-Type: text/plain 379 | Server: Gofile/0.3.0 go1.7rc1 380 | Date: [FILTERED BY TEST SCRIPT] 381 | Transfer-Encoding: chunked 382 | 383 | 1d 384 | Requested URI is not allowed 385 | 386 | 0 387 | 388 | 389 | GET /.. HTTP/1.1 390 | Host: localhost 391 | Connection: close 392 | 393 | HTTP/1.1 401 Unauthorized 394 | Connection: keep-alive 395 | Accept-Ranges: byte 396 | Content-Type: text/plain 397 | Server: Gofile/0.3.0 go1.7rc1 398 | Date: [FILTERED BY TEST SCRIPT] 399 | Transfer-Encoding: chunked 400 | 401 | 1d 402 | Requested URI is not allowed 403 | 404 | 0 405 | 406 | 407 | GET http://localhost:8080/../ HTTP/1.1 408 | Host: localhost 409 | Connection: close 410 | 411 | HTTP/1.1 401 Unauthorized 412 | Connection: keep-alive 413 | Accept-Ranges: byte 414 | Content-Type: text/plain 415 | Server: Gofile/0.3.0 go1.7rc1 416 | Date: [FILTERED BY TEST SCRIPT] 417 | Transfer-Encoding: chunked 418 | 419 | 1d 420 | Requested URI is not allowed 421 | 422 | 0 423 | 424 | 425 | Invalid: bad method => 501 426 | ==================================== 427 | 428 | POST / HTTP/1.1 429 | Host: localhost 430 | Connection: close 431 | 432 | HTTP/1.1 501 Not Implemented 433 | Connection: keep-alive 434 | Accept-Ranges: byte 435 | Content-Type: text/plain 436 | Server: Gofile/0.3.0 go1.7rc1 437 | Date: [FILTERED BY TEST SCRIPT] 438 | Transfer-Encoding: chunked 439 | 440 | 441 | Invalid: bad protocol 442 | ==================================== 443 | 444 | GET / HTTP/BAD 445 | Host: localhost 446 | Connection: close 447 | 448 | HTTP/1.1 400 Bad Request 449 | Connection: keep-alive 450 | Accept-Ranges: byte 451 | Content-Type: text/plain 452 | Server: Gofile/0.3.0 go1.7rc1 453 | Date: [FILTERED BY TEST SCRIPT] 454 | Transfer-Encoding: chunked 455 | 456 | 1e 457 | Invalid initial request line. 458 | 459 | 0 460 | 461 | 462 | A file larger than the response chunk size (1M) should be identical to the original file 463 | ==================================== 464 | Files 2m.file and test-fixtures/2m.file are identical 465 | 466 | Valid: keepalive should not disconnect, because no 'Connection: close\r\n' header is present 467 | ==================================== 468 | 469 | GET / HTTP/1.1 470 | Host: localhost 471 | 472 | HTTP/1.1 200 OK 473 | Connection: keep-alive 474 | Accept-Ranges: byte 475 | Content-Type: text/html 476 | Server: Gofile/0.3.0 go1.7rc1 477 | Date: [FILTERED BY TEST SCRIPT] 478 | Transfer-Encoding: chunked 479 | 480 | 3d5 481 | 482 | 483 | 484 | Gofile 485 | 486 | 494 | 495 |

Directory Listing for /

509 | 0 510 | 511 | -------------------------------------------------------------------------------- /tests.bash: -------------------------------------------------------------------------------- 1 | host=127.0.0.1 2 | port=8080 3 | testdir="test-fixtures" 4 | 5 | CRLF="\r\n" 6 | HTTP11="HTTP/1.1${CRLF}" 7 | HTTPBAD="HTTP/BAD${CRLF}" 8 | HConn="Connection: close${CRLF}" 9 | HHost="Host: localhost${CRLF}" 10 | HIfModifiedPast="If-Modified-Since: $(date +"%a, %d %b %Y %T %Z" --date='@1000000000')${CRLF}" 11 | HIfModifiedFuture="If-Modified-Since: $(date +"%a, %d %b %Y %T %Z" --date='@9000000000')${CRLF}" 12 | HByteRange1="Range: bytes=-10${CRLF}" 13 | HByteRange2="Range: bytes=90-${CRLF}" 14 | HByteRange3="Range: bytes=10-10${CRLF}" 15 | HByteRange4="Range: bytes=10-20${CRLF}" 16 | HByteRange5="Range: bytes=-1${CRLF}" 17 | HByteRange6="Range: bytes=0-10,20-${CRLF}" # Not supported 18 | 19 | sendreq() { 20 | local ColorReq="" # \e[94m" 21 | local ColorNon="" # \e[0m" 22 | echo # "------------------------------------" 23 | echo -ne "${ColorReq}" 24 | echo -e "$1" 25 | echo -ne "${ColorNon}" 26 | echo -e "$1" | nc $host $port # | head -n 15 27 | } 28 | 29 | hl() { 30 | echo 31 | echo "$1" 32 | echo "====================================" 33 | } 34 | 35 | hl "Valid: relative url => 200" 36 | sendreq "GET / ${HTTP11}${HHost}${HConn}" 37 | sendreq "HEAD / ${HTTP11}${HHost}${HConn}" 38 | 39 | hl "Valid: modified, normal response => 200" 40 | sendreq "GET /${testdir}/date.txt ${HTTP11}${HHost}${HIfModifiedPast}${HConn}" 41 | 42 | hl "Valid: not modified, body should be empty => 304" 43 | sendreq "GET /${testdir}/date.txt ${HTTP11}${HHost}${HIfModifiedFuture}${HConn}" 44 | 45 | hl "Valid: range header => 200" 46 | sendreq "GET /${testdir}/100.txt ${HTTP11}${HHost}${HByteRange1}${HConn}" 47 | sendreq "GET /${testdir}/100.txt ${HTTP11}${HHost}${HByteRange2}${HConn}" 48 | sendreq "GET /${testdir}/100.txt ${HTTP11}${HHost}${HByteRange3}${HConn}" 49 | sendreq "GET /${testdir}/100.txt ${HTTP11}${HHost}${HByteRange4}${HConn}" 50 | sendreq "GET /${testdir}/100.txt ${HTTP11}${HHost}${HByteRange5}${HConn}" 51 | sendreq "GET /${testdir}/100.txt ${HTTP11}${HHost}${HByteRange6}${HConn}" 52 | 53 | hl "Valid: absolute url => 200" 54 | sendreq "GET http://localhost:8080/ ${HTTP11}${HHost}${HConn}" 55 | 56 | hl "Valid: should not be chunked, no Transfer-Encoding header, must have Content-Length => 200" 57 | sendreq "GET /${testdir}/date.txt ${HTTP11}${HHost}${HConn}" 58 | 59 | hl "Valid: special characters => 200" 60 | sendreq "GET /${testdir}/a+b+c+(d)/یک.txt ${HTTP11}${HHost}${HConn}" 61 | sendreq "GET /${testdir}/a+b+c+(d)/e+f+g+[h]/test.txt ${HTTP11}${HHost}${HConn}" 62 | 63 | hl "Valid: should not found => 404" 64 | sendreq "GET /foo ${HTTP11}${HHost}${HConn}" 65 | 66 | hl "Invalid: no 'Host' header => 400" 67 | sendreq "GET / ${HTTP11}${HConn}" 68 | 69 | hl "Invalid: bad paths => 401" 70 | sendreq "GET ../ ${HTTP11}${HHost}${HConn}" 71 | sendreq "GET /.. ${HTTP11}${HHost}${HConn}" 72 | sendreq "GET http://localhost:8080/../ ${HTTP11}${HHost}${HConn}" 73 | 74 | hl "Invalid: bad method => 501" 75 | sendreq "POST / ${HTTP11}${HHost}${HConn}" 76 | 77 | hl "Invalid: bad protocol" 78 | sendreq "GET / ${HTTPBAD}${HHost}${HConn}" 79 | 80 | hl "A file larger than the response chunk size (1M) should be identical to the original file" 81 | largefile="2m.file" 82 | dd if=/dev/zero of=${testdir}/"$largefile" bs=2048 count=1024 83 | wget --quiet -O "$largefile" "http://${host}:${port}/${testdir}/${largefile}" 84 | diff -s "$largefile" ${testdir}/"$largefile" 85 | rm "$largefile" ${testdir}/"$largefile" 86 | 87 | hl "Valid: keepalive should not disconnect, because no '${HConn}' header is present" 88 | sendreq "GET / ${HTTP11}${HHost}" & 89 | sleep 0.5 90 | kill %1 91 | wait 92 | --------------------------------------------------------------------------------