├── .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 | [](https://godoc.org/github.com/siadat/gofile/http)
4 | [](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 | 
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("")
52 | for _, fi := range fileInfos {
53 | filename := fi.Name()
54 | class := ""
55 | if fi.IsDir() {
56 | class = " class='dir' "
57 | filename = filename + "/"
58 | }
59 |
60 | fullPath := strings.Join([]string{url, neturl.QueryEscape(fi.Name())}, "/")
61 | // url could end with a "/" or with no "/", so when joined with
62 | // something else using "/" there could be a double slash ie "//"
63 | fullPath = strings.Replace(fullPath, "//", "/", 1)
64 |
65 | bodyChan <- []byte(fmt.Sprintf("- %s
\n", class, htmlLink(fullPath, filename)))
66 | }
67 | 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 |
--------------------------------------------------------------------------------