├── LICENSE ├── README.md ├── bin-linux └── mypfs ├── bin-osx └── mypfs ├── bin-win └── mypfs.exe ├── build.sh ├── command.go ├── fs.go ├── http.go ├── main.go └── random.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jon Carlson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mypfs 2 | An personal fileserver for quickly sharing files with (and receiving files from) other computers on your network. More specifically, it is a small web server that exposes the files in the current directory and/or allows uploads to the same directory. 3 | 4 | ### why? 5 | * Me: Sarah, can you send me that 100MB zip of those log files? 6 | * Sarah: Sure, I'll attach it to an email 7 | * Me: Um, our email system doesn't allow attachments over 10MB 8 | * Sarah: OK, do you have a flash drive? 9 | * Me: No, that's such a pain. 10 | * (I start mypfs) 11 | * Me: Here, open your browser to `http://:8080`, enter this username when requested, and upload the file 12 | * (30 seconds later) 13 | * Sarah: That was easy. 14 | 15 | ### features 16 | 1. mypfs provides an HTTP web interface to upload/download files to/from the current directory 17 | 1. access to the web interface requires a username that is randomly generated during server startup (a password is not needed) 18 | 1. runs on the command-line 19 | 1. during startup you can specify upload, download, or accept the default of both 20 | 1. server will run for 10 minutes (default), then exit -- (this is a security feature which gives enough time to exchange files and yet protect you if you forget to shut it off) 21 | 22 | ### examples 23 | * `mypfs --timeout=5 --port=8888` (both upload and download) 24 | * `mypfs -t5 -p8888 download` (just download) 25 | * `mypfs upload` (just upload) 26 | * `mypfs --insecure` (wide open to anyone) 27 | 28 | ### how to install and run 29 | 1. download executable for your platform ( [windows](https://github.com/joncrlsn/mypfs/raw/master/bin-win/mypfs.exe "Windows"), [osx](https://github.com/joncrlsn/mypfs/raw/master/bin-osx/mypfs "OSX"), [linux](https://github.com/joncrlsn/mypfs/raw/master/bin-linux/mypfs "Linux") ) 30 | 1. place executable somewhere in your path 31 | 1. navigate to the directory with files you want to share 32 | 1. run `mypfs` 33 | 1. share URL and generated username with person you need to exchange files with. 34 | 35 | ### safe use 36 | 1. mypfs will work over the internet only if your computer has a public IP address or you have port-forwarding setup on your router. 37 | 1. run mypfs in a small directory, never the root or home directory 38 | 1. avoid use on a public network (like a coffeeshop) until more security features are added 39 | 1. avoid extending the timeout unless you totally trust your network 40 | 1. shut it down after you have exchanged files 41 | 42 | ### version history 43 | 1. 0.9.4 listens on IPV4 addresses (no IPV6... couldn't get both); startup displays URL to share 44 | 1. 0.9.3 secret username is now optional with the --insecure (-k) flag 45 | 1. 0.9.2 a secret username is generated -- required to access the site 46 | 1. 0.9.1 log to standard out when someone downloads or uploads a file 47 | 1. 0.9.0 first release 48 | 49 | ### cool things that could be added 50 | 1. add parameter with directory to be served i.e. `mypfs upload /tmp/share` 51 | 1. on the web pages, show time remaining until server shuts down 52 | 1. instead of exiting after timeout, show a "timeout" page 53 | 1. support https (easy with GoLang, just not sure it will be used) 54 | 1. limit uploads to a configurable amount 55 | -------------------------------------------------------------------------------- /bin-linux/mypfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joncrlsn/mypfs/8657ab7fb55933f6c32a9e0542b3c99d3db41b78/bin-linux/mypfs -------------------------------------------------------------------------------- /bin-osx/mypfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joncrlsn/mypfs/8657ab7fb55933f6c32a9e0542b3c99d3db41b78/bin-osx/mypfs -------------------------------------------------------------------------------- /bin-win/mypfs.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joncrlsn/mypfs/8657ab7fb55933f6c32a9e0542b3c99d3db41b78/bin-win/mypfs.exe -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | appname=mypfs 4 | 5 | if [[ -d bin-linux ]]; then 6 | GOOS=linux GOARCH=386 go build -o bin-linux/${appname} 7 | echo "Built linux32." 8 | else 9 | echo "Skipping linux32. No bin-linux directory." 10 | fi 11 | 12 | if [[ -d bin-linux64 ]]; then 13 | GOOS=linux GOARCH=amd64 go build -o bin-linux64/${appname} 14 | echo "Built linux64." 15 | else 16 | echo "Skipping linux64. No bin-linux64 directory." 17 | fi 18 | 19 | if [[ -d bin-osx ]]; then 20 | GOOS=darwin GOARCH=386 go build -o bin-osx/${appname} 21 | echo "Built osx32." 22 | else 23 | echo "Skipping osx32. No bin-osx directory." 24 | fi 25 | 26 | if [[ -d bin-osx64 ]]; then 27 | GOOS=darwin GOARCH=amd64 go build -o bin-osx64/${appname} 28 | echo "Built osx64." 29 | else 30 | echo "Skipping osx64. No bin-osx64 directory." 31 | fi 32 | 33 | if [[ -d bin-win ]]; then 34 | GOOS=windows GOARCH=386 go build -o bin-win/${appname}.exe 35 | echo "Built win32." 36 | else 37 | echo "Skipping win32. No bin-win directory." 38 | fi 39 | 40 | if [[ -d bin-win64 ]]; then 41 | GOOS=windows GOARCH=amd64 go build -o bin-win64/${appname}.exe 42 | echo "Built win64." 43 | else 44 | echo "Skipping win64. No bin-win64 directory." 45 | fi 46 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | var myPfsCmd = &cobra.Command{ 9 | Use: "mypfs", 10 | Short: "My personal file server", 11 | Long: `A personal file server for sharing files with and receiving files 12 | from people on your network. 13 | Complete documentation is available at http://github.com/joncrlsn/mypfs`, 14 | Run: func(cmd *cobra.Command, args []string) { 15 | action = "up/down" 16 | }, 17 | } 18 | 19 | var uploadCmd = &cobra.Command{ 20 | Use: "upload", 21 | Short: "Allow file uploads to the current directory", 22 | Long: `Starts web server that only allows uploading of files to 23 | the current directory. No downloading is allowed`, 24 | Run: func(cmd *cobra.Command, args []string) { 25 | action = "up" 26 | }, 27 | } 28 | 29 | var downloadCmd = &cobra.Command{ 30 | Use: "download", 31 | Short: "Allow file downloads from the current directory", 32 | Long: `Starts web server that only allows downloading of files from 33 | the current directory. No uploading is allowed`, 34 | Run: func(cmd *cobra.Command, args []string) { 35 | action = "down" 36 | }, 37 | } 38 | 39 | var versionCmd = &cobra.Command{ 40 | Use: "version", 41 | Short: "Prints the version number", 42 | Long: `Displays the version number for mypfs`, 43 | Run: func(cmd *cobra.Command, args []string) { 44 | fmt.Println("mypfs (My Personal File Server) version", version) 45 | action = "version" 46 | }, 47 | } 48 | 49 | func init() { 50 | myPfsCmd.AddCommand(uploadCmd) 51 | myPfsCmd.AddCommand(downloadCmd) 52 | myPfsCmd.AddCommand(versionCmd) 53 | myPfsCmd.PersistentFlags().IntVarP(&port, "port", "p", 8080, "port number to listen on") 54 | myPfsCmd.PersistentFlags().Int64VarP(&timeoutMinutes, "timeout", "t", 10, "number of minutes to leave this running") 55 | myPfsCmd.PersistentFlags().BoolVarP(&insecure, "insecure", "k", false, "true = do not require secret username") 56 | } 57 | -------------------------------------------------------------------------------- /fs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // HTTP file system request handler 6 | 7 | // This is a modified version of the Go 1.5 net/http/fs.go file. 8 | // The only significant change is in the 3rd line of the dirList() function 9 | 10 | package main 11 | 12 | import ( 13 | "errors" 14 | "fmt" 15 | "io" 16 | "log" 17 | "mime" 18 | "mime/multipart" 19 | "net/http" 20 | "net/textproto" 21 | "net/url" 22 | "os" 23 | "path" 24 | "path/filepath" 25 | "strconv" 26 | "strings" 27 | "time" 28 | ) 29 | 30 | // from http/sniff.go 31 | const sniffLen = 512 32 | 33 | // from http/server.go 34 | var htmlReplacer = strings.NewReplacer( 35 | "&", "&", 36 | "<", "<", 37 | ">", ">", 38 | // """ is shorter than """. 39 | `"`, """, 40 | // "'" is shorter than "'" and apos was not in HTML until HTML5. 41 | "'", "'", 42 | ) 43 | 44 | // A Dir implements FileSystem using the native file system restricted to a 45 | // specific directory tree. 46 | // 47 | // While the FileSystem.Open method takes '/'-separated paths, a Dir's string 48 | // value is a filename on the native file system, not a URL, so it is separated 49 | // by filepath.Separator, which isn't necessarily '/'. 50 | // 51 | // An empty Dir is treated as ".". 52 | type Dir string 53 | 54 | func (d Dir) Open(name string) (File, error) { 55 | if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 || 56 | strings.Contains(name, "\x00") { 57 | return nil, errors.New("http: invalid character in file path") 58 | } 59 | dir := string(d) 60 | if dir == "" { 61 | dir = "." 62 | } 63 | f, err := os.Open(filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name)))) 64 | if err != nil { 65 | return nil, err 66 | } 67 | return f, nil 68 | } 69 | 70 | // A FileSystem implements access to a collection of named files. 71 | // The elements in a file path are separated by slash ('/', U+002F) 72 | // characters, regardless of host operating system convention. 73 | type FileSystem interface { 74 | Open(name string) (File, error) 75 | } 76 | 77 | // A File is returned by a FileSystem's Open method and can be 78 | // served by the FileServer implementation. 79 | // 80 | // The methods should behave the same as those on an *os.File. 81 | type File interface { 82 | io.Closer 83 | io.Reader 84 | Readdir(count int) ([]os.FileInfo, error) 85 | Seek(offset int64, whence int) (int64, error) 86 | Stat() (os.FileInfo, error) 87 | } 88 | 89 | func dirList(w http.ResponseWriter, f File, addUploadHeader bool) { 90 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 91 | // This is the only significant change from the Go 1.5 original 92 | if addUploadHeader { 93 | fmt.Fprintf(w, `Upload
`) 94 | } 95 | fmt.Fprintf(w, "
")
 96 | 	for {
 97 | 		dirs, err := f.Readdir(100)
 98 | 		if err != nil || len(dirs) == 0 {
 99 | 			break
100 | 		}
101 | 		for _, d := range dirs {
102 | 			name := d.Name()
103 | 			if d.IsDir() {
104 | 				name += "/"
105 | 			}
106 | 			// name may contain '?' or '#', which must be escaped to remain
107 | 			// part of the URL path, and not indicate the start of a query
108 | 			// string or fragment.
109 | 			url := url.URL{Path: name}
110 | 			fmt.Fprintf(w, "%s\n", url.String(), htmlReplacer.Replace(name))
111 | 		}
112 | 	}
113 | 	fmt.Fprintf(w, "
\n") 114 | } 115 | 116 | // ServeContent replies to the request using the content in the 117 | // provided ReadSeeker. The main benefit of ServeContent over io.Copy 118 | // is that it handles Range requests properly, sets the MIME type, and 119 | // handles If-Modified-Since requests. 120 | // 121 | // If the response's Content-Type header is not set, ServeContent 122 | // first tries to deduce the type from name's file extension and, 123 | // if that fails, falls back to reading the first block of the content 124 | // and passing it to DetectContentType. 125 | // The name is otherwise unused; in particular it can be empty and is 126 | // never sent in the response. 127 | // 128 | // If modtime is not the zero time or Unix epoch, ServeContent 129 | // includes it in a Last-Modified header in the response. If the 130 | // request includes an If-Modified-Since header, ServeContent uses 131 | // modtime to decide whether the content needs to be sent at all. 132 | // 133 | // The content's Seek method must work: ServeContent uses 134 | // a seek to the end of the content to determine its size. 135 | // 136 | // If the caller has set w's ETag header, ServeContent uses it to 137 | // handle requests using If-Range and If-None-Match. 138 | // 139 | // Note that *os.File implements the io.ReadSeeker interface. 140 | func ServeContent(w http.ResponseWriter, req *http.Request, name string, modtime time.Time, content io.ReadSeeker) { 141 | sizeFunc := func() (int64, error) { 142 | size, err := content.Seek(0, os.SEEK_END) 143 | if err != nil { 144 | return 0, errSeeker 145 | } 146 | _, err = content.Seek(0, os.SEEK_SET) 147 | if err != nil { 148 | return 0, errSeeker 149 | } 150 | return size, nil 151 | } 152 | serveContent(w, req, name, modtime, sizeFunc, content) 153 | } 154 | 155 | // errSeeker is returned by ServeContent's sizeFunc when the content 156 | // doesn't seek properly. The underlying Seeker's error text isn't 157 | // included in the sizeFunc reply so it's not sent over HTTP to end 158 | // users. 159 | var errSeeker = errors.New("seeker can't seek") 160 | 161 | // if name is empty, filename is unknown. (used for mime type, before sniffing) 162 | // if modtime.IsZero(), modtime is unknown. 163 | // content must be seeked to the beginning of the file. 164 | // The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response. 165 | func serveContent(w http.ResponseWriter, r *http.Request, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) { 166 | if checkLastModified(w, r, modtime) { 167 | return 168 | } 169 | rangeReq, done := checkETag(w, r, modtime) 170 | if done { 171 | return 172 | } 173 | 174 | code := http.StatusOK 175 | 176 | // If Content-Type isn't set, use the file's extension to find it, but 177 | // if the Content-Type is unset explicitly, do not sniff the type. 178 | ctypes, haveType := w.Header()["Content-Type"] 179 | var ctype string 180 | if !haveType { 181 | ctype = mime.TypeByExtension(filepath.Ext(name)) 182 | if ctype == "" { 183 | // read a chunk to decide between utf-8 text and binary 184 | var buf [sniffLen]byte 185 | n, _ := io.ReadFull(content, buf[:]) 186 | ctype = http.DetectContentType(buf[:n]) 187 | _, err := content.Seek(0, os.SEEK_SET) // rewind to output whole file 188 | if err != nil { 189 | http.Error(w, "seeker can't seek", http.StatusInternalServerError) 190 | return 191 | } 192 | } 193 | w.Header().Set("Content-Type", ctype) 194 | } else if len(ctypes) > 0 { 195 | ctype = ctypes[0] 196 | } 197 | 198 | size, err := sizeFunc() 199 | if err != nil { 200 | http.Error(w, err.Error(), http.StatusInternalServerError) 201 | return 202 | } 203 | 204 | // handle Content-Range header. 205 | sendSize := size 206 | var sendContent io.Reader = content 207 | if size >= 0 { 208 | ranges, err := parseRange(rangeReq, size) 209 | if err != nil { 210 | http.Error(w, err.Error(), http.StatusRequestedRangeNotSatisfiable) 211 | return 212 | } 213 | if sumRangesSize(ranges) > size { 214 | // The total number of bytes in all the ranges 215 | // is larger than the size of the file by 216 | // itself, so this is probably an attack, or a 217 | // dumb client. Ignore the range request. 218 | ranges = nil 219 | } 220 | switch { 221 | case len(ranges) == 1: 222 | // RFC 2616, Section 14.16: 223 | // "When an HTTP message includes the content of a single 224 | // range (for example, a response to a request for a 225 | // single range, or to a request for a set of ranges 226 | // that overlap without any holes), this content is 227 | // transmitted with a Content-Range header, and a 228 | // Content-Length header showing the number of bytes 229 | // actually transferred. 230 | // ... 231 | // A response to a request for a single range MUST NOT 232 | // be sent using the multipart/byteranges media type." 233 | ra := ranges[0] 234 | if _, err := content.Seek(ra.start, os.SEEK_SET); err != nil { 235 | http.Error(w, err.Error(), http.StatusRequestedRangeNotSatisfiable) 236 | return 237 | } 238 | sendSize = ra.length 239 | code = http.StatusPartialContent 240 | w.Header().Set("Content-Range", ra.contentRange(size)) 241 | case len(ranges) > 1: 242 | sendSize = rangesMIMESize(ranges, ctype, size) 243 | code = http.StatusPartialContent 244 | 245 | pr, pw := io.Pipe() 246 | mw := multipart.NewWriter(pw) 247 | w.Header().Set("Content-Type", "multipart/byteranges; boundary="+mw.Boundary()) 248 | sendContent = pr 249 | defer pr.Close() // cause writing goroutine to fail and exit if CopyN doesn't finish. 250 | go func() { 251 | for _, ra := range ranges { 252 | part, err := mw.CreatePart(ra.mimeHeader(ctype, size)) 253 | if err != nil { 254 | pw.CloseWithError(err) 255 | return 256 | } 257 | if _, err := content.Seek(ra.start, os.SEEK_SET); err != nil { 258 | pw.CloseWithError(err) 259 | return 260 | } 261 | if _, err := io.CopyN(part, content, ra.length); err != nil { 262 | pw.CloseWithError(err) 263 | return 264 | } 265 | } 266 | mw.Close() 267 | pw.Close() 268 | }() 269 | } 270 | 271 | w.Header().Set("Accept-Ranges", "bytes") 272 | if w.Header().Get("Content-Encoding") == "" { 273 | w.Header().Set("Content-Length", strconv.FormatInt(sendSize, 10)) 274 | } 275 | } 276 | 277 | w.WriteHeader(code) 278 | 279 | if r.Method != "HEAD" { 280 | io.CopyN(w, sendContent, sendSize) 281 | log.Println("File sent:", name) 282 | } 283 | } 284 | 285 | var unixEpochTime = time.Unix(0, 0) 286 | 287 | // modtime is the modification time of the resource to be served, or IsZero(). 288 | // return value is whether this request is now complete. 289 | func checkLastModified(w http.ResponseWriter, r *http.Request, modtime time.Time) bool { 290 | if modtime.IsZero() || modtime.Equal(unixEpochTime) { 291 | // If the file doesn't have a modtime (IsZero), or the modtime 292 | // is obviously garbage (Unix time == 0), then ignore modtimes 293 | // and don't process the If-Modified-Since header. 294 | return false 295 | } 296 | 297 | // The Date-Modified header truncates sub-second precision, so 298 | // use mtime < t+1s instead of mtime <= t to check for unmodified. 299 | if t, err := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")); err == nil && modtime.Before(t.Add(1*time.Second)) { 300 | h := w.Header() 301 | delete(h, "Content-Type") 302 | delete(h, "Content-Length") 303 | w.WriteHeader(http.StatusNotModified) 304 | return true 305 | } 306 | w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat)) 307 | return false 308 | } 309 | 310 | // checkETag implements If-None-Match and If-Range checks. 311 | // 312 | // The ETag or modtime must have been previously set in the 313 | // http.ResponseWriter's headers. The modtime is only compared at second 314 | // granularity and may be the zero value to mean unknown. 315 | // 316 | // The return value is the effective request "Range" header to use and 317 | // whether this request is now considered done. 318 | func checkETag(w http.ResponseWriter, r *http.Request, modtime time.Time) (rangeReq string, done bool) { 319 | etag := w.Header().Get("Etag") 320 | rangeReq = r.Header.Get("Range") 321 | 322 | // Invalidate the range request if the entity doesn't match the one 323 | // the client was expecting. 324 | // "If-Range: version" means "ignore the Range: header unless version matches the 325 | // current file." 326 | // We only support ETag versions. 327 | // The caller must have set the ETag on the response already. 328 | if ir := r.Header.Get("If-Range"); ir != "" && ir != etag { 329 | // The If-Range value is typically the ETag value, but it may also be 330 | // the modtime date. See golang.org/issue/8367. 331 | timeMatches := false 332 | if !modtime.IsZero() { 333 | if t, err := http.ParseTime(ir); err == nil && t.Unix() == modtime.Unix() { 334 | timeMatches = true 335 | } 336 | } 337 | if !timeMatches { 338 | rangeReq = "" 339 | } 340 | } 341 | 342 | if inm := r.Header.Get("If-None-Match"); inm != "" { 343 | // Must know ETag. 344 | if etag == "" { 345 | return rangeReq, false 346 | } 347 | 348 | // TODO(bradfitz): non-GET/HEAD requests require more work: 349 | // sending a different status code on matches, and 350 | // also can't use weak cache validators (those with a "W/ 351 | // prefix). But most users of ServeContent will be using 352 | // it on GET or HEAD, so only support those for now. 353 | if r.Method != "GET" && r.Method != "HEAD" { 354 | return rangeReq, false 355 | } 356 | 357 | // TODO(bradfitz): deal with comma-separated or multiple-valued 358 | // list of If-None-match values. For now just handle the common 359 | // case of a single item. 360 | if inm == etag || inm == "*" { 361 | h := w.Header() 362 | delete(h, "Content-Type") 363 | delete(h, "Content-Length") 364 | w.WriteHeader(http.StatusNotModified) 365 | return "", true 366 | } 367 | } 368 | return rangeReq, false 369 | } 370 | 371 | // name is '/'-separated, not filepath.Separator. 372 | func serveFile(w http.ResponseWriter, r *http.Request, fs FileSystem, name string, redirect bool, addUploadHeader bool) { 373 | const indexPage = "/index.html" 374 | 375 | // redirect .../index.html to .../ 376 | // can't use Redirect() because that would make the path absolute, 377 | // which would be a problem running under StripPrefix 378 | if strings.HasSuffix(r.URL.Path, indexPage) { 379 | localRedirect(w, r, "./") 380 | return 381 | } 382 | 383 | f, err := fs.Open(name) 384 | if err != nil { 385 | msg, code := toHTTPError(err) 386 | http.Error(w, msg, code) 387 | return 388 | } 389 | defer f.Close() 390 | 391 | d, err1 := f.Stat() 392 | if err1 != nil { 393 | msg, code := toHTTPError(err) 394 | http.Error(w, msg, code) 395 | return 396 | } 397 | 398 | if redirect { 399 | // redirect to canonical path: / at end of directory url 400 | // r.URL.Path always begins with / 401 | url := r.URL.Path 402 | if d.IsDir() { 403 | if url[len(url)-1] != '/' { 404 | localRedirect(w, r, path.Base(url)+"/") 405 | return 406 | } 407 | } else { 408 | if url[len(url)-1] == '/' { 409 | localRedirect(w, r, "../"+path.Base(url)) 410 | return 411 | } 412 | } 413 | } 414 | 415 | // use contents of index.html for directory, if present 416 | if d.IsDir() { 417 | index := strings.TrimSuffix(name, "/") + indexPage 418 | ff, err := fs.Open(index) 419 | if err == nil { 420 | defer ff.Close() 421 | dd, err := ff.Stat() 422 | if err == nil { 423 | name = index 424 | d = dd 425 | f = ff 426 | } 427 | } 428 | } 429 | 430 | // Still a directory? (we didn't find an index.html file) 431 | if d.IsDir() { 432 | if checkLastModified(w, r, d.ModTime()) { 433 | return 434 | } 435 | dirList(w, f, addUploadHeader) 436 | return 437 | } 438 | 439 | // serveContent will check modification time 440 | sizeFunc := func() (int64, error) { return d.Size(), nil } 441 | serveContent(w, r, d.Name(), d.ModTime(), sizeFunc, f) 442 | } 443 | 444 | // toHTTPError returns a non-specific HTTP error message and status code 445 | // for a given non-nil error value. It's important that toHTTPError does not 446 | // actually return err.Error(), since msg and httpStatus are returned to users, 447 | // and historically Go's ServeContent always returned just "404 Not Found" for 448 | // all errors. We don't want to start leaking information in error messages. 449 | func toHTTPError(err error) (msg string, httpStatus int) { 450 | if os.IsNotExist(err) { 451 | return "404 page not found", http.StatusNotFound 452 | } 453 | if os.IsPermission(err) { 454 | return "403 Forbidden", http.StatusForbidden 455 | } 456 | // Default: 457 | return "500 Internal Server Error", http.StatusInternalServerError 458 | } 459 | 460 | // localRedirect gives a Moved Permanently response. 461 | // It does not convert relative paths to absolute paths like Redirect does. 462 | func localRedirect(w http.ResponseWriter, r *http.Request, newPath string) { 463 | if q := r.URL.RawQuery; q != "" { 464 | newPath += "?" + q 465 | } 466 | w.Header().Set("Location", newPath) 467 | w.WriteHeader(http.StatusMovedPermanently) 468 | } 469 | 470 | // ServeFile replies to the request with the contents of the named 471 | // file or directory. 472 | // 473 | // As a special case, ServeFile redirects any request where r.URL.Path 474 | // ends in "/index.html" to the same path, without the final 475 | // "index.html". To avoid such redirects either modify the path or 476 | // use ServeContent. 477 | func ServeFile(w http.ResponseWriter, r *http.Request, name string) { 478 | dir, file := filepath.Split(name) 479 | serveFile(w, r, Dir(dir), file, false, false) 480 | } 481 | 482 | type fileHandler struct { 483 | root FileSystem 484 | addUploadHeader bool 485 | } 486 | 487 | // FileServer returns a handler that serves HTTP requests 488 | // with the contents of the file system rooted at root. 489 | // 490 | // To use the operating system's file system implementation, 491 | // use http.Dir: 492 | // 493 | // http.Handle("/", http.FileServer(http.Dir("/tmp"))) 494 | // 495 | // As a special case, the returned file server redirects any request 496 | // ending in "/index.html" to the same path, without the final 497 | // "index.html". 498 | func FileServer(root FileSystem, addUploadHeader bool) http.Handler { 499 | return &fileHandler{root, addUploadHeader} 500 | } 501 | 502 | func (f *fileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 503 | upath := r.URL.Path 504 | if !strings.HasPrefix(upath, "/") { 505 | upath = "/" + upath 506 | r.URL.Path = upath 507 | } 508 | serveFile(w, r, f.root, path.Clean(upath), true, f.addUploadHeader) 509 | } 510 | 511 | // httpRange specifies the byte range to be sent to the client. 512 | type httpRange struct { 513 | start, length int64 514 | } 515 | 516 | func (r httpRange) contentRange(size int64) string { 517 | return fmt.Sprintf("bytes %d-%d/%d", r.start, r.start+r.length-1, size) 518 | } 519 | 520 | func (r httpRange) mimeHeader(contentType string, size int64) textproto.MIMEHeader { 521 | return textproto.MIMEHeader{ 522 | "Content-Range": {r.contentRange(size)}, 523 | "Content-Type": {contentType}, 524 | } 525 | } 526 | 527 | // parseRange parses a Range header string as per RFC 2616. 528 | func parseRange(s string, size int64) ([]httpRange, error) { 529 | if s == "" { 530 | return nil, nil // header not present 531 | } 532 | const b = "bytes=" 533 | if !strings.HasPrefix(s, b) { 534 | return nil, errors.New("invalid range") 535 | } 536 | var ranges []httpRange 537 | for _, ra := range strings.Split(s[len(b):], ",") { 538 | ra = strings.TrimSpace(ra) 539 | if ra == "" { 540 | continue 541 | } 542 | i := strings.Index(ra, "-") 543 | if i < 0 { 544 | return nil, errors.New("invalid range") 545 | } 546 | start, end := strings.TrimSpace(ra[:i]), strings.TrimSpace(ra[i+1:]) 547 | var r httpRange 548 | if start == "" { 549 | // If no start is specified, end specifies the 550 | // range start relative to the end of the file. 551 | i, err := strconv.ParseInt(end, 10, 64) 552 | if err != nil { 553 | return nil, errors.New("invalid range") 554 | } 555 | if i > size { 556 | i = size 557 | } 558 | r.start = size - i 559 | r.length = size - r.start 560 | } else { 561 | i, err := strconv.ParseInt(start, 10, 64) 562 | if err != nil || i >= size || i < 0 { 563 | return nil, errors.New("invalid range") 564 | } 565 | r.start = i 566 | if end == "" { 567 | // If no end is specified, range extends to end of the file. 568 | r.length = size - r.start 569 | } else { 570 | i, err := strconv.ParseInt(end, 10, 64) 571 | if err != nil || r.start > i { 572 | return nil, errors.New("invalid range") 573 | } 574 | if i >= size { 575 | i = size - 1 576 | } 577 | r.length = i - r.start + 1 578 | } 579 | } 580 | ranges = append(ranges, r) 581 | } 582 | return ranges, nil 583 | } 584 | 585 | // countingWriter counts how many bytes have been written to it. 586 | type countingWriter int64 587 | 588 | func (w *countingWriter) Write(p []byte) (n int, err error) { 589 | *w += countingWriter(len(p)) 590 | return len(p), nil 591 | } 592 | 593 | // rangesMIMESize returns the number of bytes it takes to encode the 594 | // provided ranges as a multipart response. 595 | func rangesMIMESize(ranges []httpRange, contentType string, contentSize int64) (encSize int64) { 596 | var w countingWriter 597 | mw := multipart.NewWriter(&w) 598 | for _, ra := range ranges { 599 | mw.CreatePart(ra.mimeHeader(contentType, contentSize)) 600 | encSize += ra.length 601 | } 602 | mw.Close() 603 | encSize += int64(w) 604 | return 605 | } 606 | 607 | func sumRangesSize(ranges []httpRange) (size int64) { 608 | for _, ra := range ranges { 609 | size += ra.length 610 | } 611 | return 612 | } 613 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | // 2 | // Sets up the URL routing and starts the web server 3 | // 4 | // Copyright (c) 2015 Jon Carlson. All rights reserved. 5 | // Use of this source code is governed by the MIT 6 | // license that can be found in the LICENSE file. 7 | // 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "log" 13 | "net/http" 14 | "strconv" 15 | "strings" 16 | ) 17 | 18 | // Taken from http://blog.golang.org/error-handling-and-go 19 | // errorHandler adds a ServeHttp method to every errorHandler function 20 | type errorHandler func(http.ResponseWriter, *http.Request) error 21 | 22 | // Adds a ServeHttp method to every errorHandler function 23 | func (fn errorHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 24 | if err := fn(w, req); err != nil { 25 | log.Println("Error handling request", err) 26 | http.Error(w, "Internal Server Error. Check logs for actual error", 500) 27 | } 28 | } 29 | 30 | // errorableHandler converts an http.Handler into an errorHandler function 31 | //func errorableHandler(handler http.Handler) func(w http.ResponseWriter, req *http.Request) error { 32 | func errorableHandler(handler http.Handler) errorHandler { 33 | return func(w http.ResponseWriter, req *http.Request) error { 34 | handler.ServeHTTP(w, req) 35 | return nil 36 | } 37 | } 38 | 39 | // authBasic wraps a request handler (that returns an error: AKA errorHandler) 40 | // with another one that requires BASIC HTTP authentication 41 | func authBasic(handler errorHandler) errorHandler { 42 | return func(w http.ResponseWriter, req *http.Request) error { 43 | 44 | if insecure { 45 | return handler(w, req) 46 | } 47 | 48 | // 49 | // Ensure request has an "Authorization" header 50 | // (needed for "Basic" authentication) 51 | // 52 | username, _, ok := req.BasicAuth() 53 | if !ok { 54 | // Request the "Authorization" header 55 | w.Header().Set("WWW-Authenticate", `Basic realm="go-example-web"`) 56 | http.Error(w, "Access Denied", http.StatusUnauthorized) 57 | return nil 58 | } 59 | 60 | if username != secretUsername { 61 | // User authentication failed 62 | w.Header().Set("WWW-Authenticate", `Basic realm="Enter token for username"`) 63 | http.Error(w, "Access Denied", http.StatusUnauthorized) 64 | return nil 65 | } 66 | 67 | // 68 | // The credentials match, so run the wrapped handler 69 | // 70 | return handler(w, req) 71 | } 72 | } 73 | 74 | // redirectToHttps returns a function that redirects anything on the http 75 | // port over to the https port. We have to wrap the function in a function 76 | // so we can dynamically provide the http and https ports. 77 | func redirectToHttps(httpPort int, httpsPort int) func(w http.ResponseWriter, req *http.Request) { 78 | return func(w http.ResponseWriter, req *http.Request) { 79 | newHost := strings.Replace(req.Host, strconv.Itoa(httpPort), strconv.Itoa(httpsPort), 1) 80 | newUrl := fmt.Sprintf("https://%s/%s", newHost, req.RequestURI) 81 | http.Redirect(w, req, newUrl, http.StatusMovedPermanently) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2015 Jon Carlson. All rights reserved. 3 | // Use of this source code is governed by the MIT 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | // 10 | // Runs an HTTP static file server and file upload receiver from/to 11 | // the directory that this is executed from. 12 | // 13 | 14 | import ( 15 | "fmt" 16 | "io" 17 | "log" 18 | "math/rand" 19 | "net" 20 | "net/http" 21 | "os" 22 | "strconv" 23 | "time" 24 | ) 25 | 26 | var ( 27 | action string = "help" 28 | port int = 8080 29 | timeoutMinutes int64 = 10 30 | insecure bool = false 31 | version string = "0.9.4" 32 | secretUsername string // Required for admittance to site 33 | ) 34 | 35 | // uploadHandler returns an HTML upload form 36 | func uploadHandler(w http.ResponseWriter, r *http.Request) error { 37 | if r.Method == "GET" { 38 | fmt.Fprintf(w, ` 39 | 40 | GoLang HTTP Fileserver 41 | 47 | 48 | 49 | 50 | 51 |

Choose a file to upload

52 | 53 |
54 | 55 |

56 | 57 |
58 | 59 | 60 | `) 61 | } 62 | return nil 63 | } 64 | 65 | // receiveHandler accepts the file and saves it to the current working directory 66 | func receiveHandler(w http.ResponseWriter, r *http.Request) error { 67 | 68 | // the FormFile function takes in the POST input id file 69 | file, header, err := r.FormFile("file") 70 | if err != nil { 71 | fmt.Fprintln(w, err) 72 | return err 73 | } 74 | 75 | defer file.Close() 76 | 77 | out, err := os.Create(header.Filename) 78 | if err != nil { 79 | fmt.Fprintf(w, "Unable to create the file for writing. Check your write access privilege") 80 | return err 81 | } 82 | 83 | defer out.Close() 84 | 85 | // write the content from POST to the file 86 | _, err = io.Copy(out, file) 87 | if err != nil { 88 | fmt.Fprintln(w, err) 89 | return err 90 | } 91 | 92 | log.Println("File received:", header.Filename) 93 | 94 | fmt.Fprintf(w, ` 95 | File uploaded successfully: %s 96 |

Back

97 | `, header.Filename) 98 | return nil 99 | } 100 | 101 | func init() { 102 | err := myPfsCmd.Execute() 103 | if err != nil { 104 | os.Exit(1) 105 | } 106 | 107 | if insecure { 108 | // skip the random username generation 109 | } else { 110 | // Generate the token required to access this server via HTTP 111 | rand.Seed(time.Now().UTC().UnixNano()) 112 | secretUsername = randomString(8) 113 | } 114 | } 115 | 116 | func main() { 117 | 118 | var portStr = ":" + strconv.Itoa(port) 119 | 120 | dir, err := os.Getwd() 121 | if err != nil { 122 | fmt.Println("err=", err) 123 | os.Exit(1) 124 | } 125 | 126 | if action == "help" { 127 | os.Exit(0) 128 | } else if action == "version" { 129 | os.Exit(0) 130 | } 131 | 132 | if action == "up" { 133 | printAddressAndPort() 134 | log.Printf("Allowing uploads to the current directory for %v minutes on port %v\n", timeoutMinutes, port) 135 | 136 | // Show the upload form 137 | http.Handle("/", errorHandler(authBasic(uploadHandler))) 138 | // Handle the incoming file 139 | http.Handle("/fs-receive", errorHandler(authBasic(receiveHandler))) 140 | 141 | } else if action == "down" { 142 | printAddressAndPort() 143 | log.Printf("Allowing downloads from the current directory for %v minutes on port %v\n", timeoutMinutes, port) 144 | 145 | // Show the download page using a customized FileServer with no 146 | // added Upload Header (because we are not allowing uploads) 147 | http.Handle("/", errorHandler(authBasic(errorableHandler(FileServer(Dir(dir), false /*addUploadHeader*/))))) 148 | 149 | } else if action == "up/down" { 150 | printAddressAndPort() 151 | log.Printf("Allowing downloads from (and uploads to) the current directory for %v minutes on port %v\n", timeoutMinutes, port) 152 | 153 | // Display the upload form 154 | http.Handle("/fs-upload", errorHandler(authBasic(uploadHandler))) 155 | // Handle the incoming file 156 | http.Handle("/fs-receive", errorHandler(authBasic(receiveHandler))) 157 | 158 | // Show the download page using a customized FileServer 159 | // copied from net/http/fs.go. This version adds a header 160 | // to the top when we list a directory (in dirList() func) 161 | http.Handle("/", errorHandler(authBasic(errorableHandler(FileServer(Dir(dir), true /*addUploadHeader*/))))) 162 | } 163 | 164 | go func() { 165 | time.Sleep(time.Duration(timeoutMinutes) * time.Minute) 166 | log.Println("Fileserver timed out. Exiting.") 167 | os.Exit(0) 168 | }() 169 | 170 | l, err := net.Listen("tcp4", portStr) 171 | if err != nil { 172 | fmt.Println(err) 173 | os.Exit(1) 174 | } 175 | log.Fatal(http.Serve(l, nil)) 176 | } 177 | 178 | func printAddressAndPort() { 179 | addrs, err := net.InterfaceAddrs() 180 | 181 | if err != nil { 182 | log.Fatal("Error getting local address", err) 183 | } 184 | 185 | var localAddress string = "" 186 | for _, address := range addrs { 187 | // check the address type and if it is not a loopback then display it 188 | if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 189 | if ipnet.IP.To4() != nil { 190 | localAddress = ipnet.IP.String() 191 | } 192 | } 193 | } 194 | 195 | fmt.Println() 196 | if insecure { 197 | fmt.Printf("Use this address: http://%s:%v\n (No secret username required)", localAddress, port) 198 | } else { 199 | fmt.Printf("Use this address: http://%s:%v\n (Enter %s for username when requested. Ignore password) \n", localAddress, port, secretUsername) 200 | } 201 | fmt.Println() 202 | 203 | } 204 | -------------------------------------------------------------------------------- /random.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | ) 6 | 7 | // randomString returns a random string of letters of the given length 8 | func randomString(l int) string { 9 | bytes := make([]byte, l) 10 | for i := 0; i < l; i++ { 11 | rint := randomInt(65, 117) 12 | if rint > 90 { 13 | rint = rint + 6 14 | } 15 | bytes[i] = byte(rint) 16 | } 17 | return string(bytes) 18 | } 19 | 20 | // randomInt returns a random integer between the two numbers. 21 | // min is inclusive, max is exclusive 22 | func randomInt(min int, max int) int { 23 | return min + rand.Intn(max-min) 24 | } 25 | --------------------------------------------------------------------------------