Creating a basic filesharing service

I began using Dropbox when I was in high school. This site allowed me to upload school assignments from a computer on the school's network to a cloud location where I could later download it and work on it at home. Since then, I've used MEGA and other similar sites to upload and share files. I wanted to learn more about file I/O and secure sessions in Go and this simple project was a practical way to do so. It turns out creating a filesharing webserver in Go is extremely straightforward, as the Go standard library abstracts most of the file I/O logic, and a trusted third-party package is available that makes session configuration very easy.



  • I've defined handler functions that will be mapped to endpoints on the server.
  • indexHandler() reads all of the files present in the server's local uploads> directory into a slice of type io.DirEntry. This type exposes functions that will allow a Go program to understand what it may find inside of a directory, and the documentation provided by IntelliSense is shown below:

    image of type documentation

  • Once the uploads> directory is read, its contents will be used to render the index.html page on the webserver - the end result is that a list of all uploaded files will be displayed on the server's homepage.
  • The uploadHandler function expects to receive a request posting a file. The HTML code defining the file key is shown below.
  • This function will create a new filesystem entry in the uploads directory and then copy the uploaded file there.
  • If this is successful the visitor will be redirected to the homepage where they can see their uploaded file listed, as described above.
  • The fileHandler function is responsible for serving a file on the server to a client - usually a web browser.
  • After some trial and error I found it was necessary to escape URL encoding as this would cause a mismatch between the file's actual name and the path to that file on the server which was necessarily encoded.
  • The action of serving the file is entirely abstracted by the http.ServeFile() function.
package main

import (
    "io"
    "net/http"
    "net/url"
    "os"
    "path/filepath"
)

func indexHandler(w http.ResponseWriter, r *http.Request) {
    files, err := os.ReadDir("uploads")
    if err != nil {
        http.Error(w, "Error finding available files", http.StatusInternalServerError)
        return
    }

    data := struct {
        Files []os.DirEntry
    }{
        Files: files,
    }

    templates.ExecuteTemplate(w, "index.html", data)
}

func uploadHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method == "POST" {
        file, handler, err := r.FormFile("file")
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        defer file.Close()

        localFile, err := os.Create(filepath.Join("uploads", handler.Filename))
        if err != nil {
            http.Error(w, "Unable to create a copy of the file on the server", http.StatusInternalServerError)
            return
        }
        defer localFile.Close()

        if _, err := io.Copy(localFile, file); err != nil {
            http.Error(w, "Unable to copy file to server", http.StatusInternalServerError)
            return
        }

        http.Redirect(w, r, "/", http.StatusSeeOther)
    } else {
        http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
    }
}

func fileHandler(w http.ResponseWriter, r *http.Request) {
    encodedFilename := r.URL.Path[len("/download/"):]

    filename, err := url.QueryUnescape(encodedFilename)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    filepath := filepath.Join("uploads", filename)
    http.ServeFile(w, r, filepath)
}                            
                        

This code may make more sense when examining the HTML code below:



index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>File Sharing</title>
</head>
<body>
    <h3><a href="/logout">Log Out</a></h3>
    <h1>Upload a file</h1>
    <form action="/new" method="post" enctype="multipart/form-data">
        <input type="file" name="file">
        <input type="submit" value="Upload">
    </form>

    <h2>Uploaded files:</h2>
    <ul>
        {{range .Files}}
            <li><a href="/download/{{.Name}}">{{.Name}}</a></li>
        {{else}}
            <li>No files uploaded yet.</li>
        {{end}}
    </ul>
</body>
</html>
                            

login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login</title>
</head>
<body>
    <h1>Login</h1>
    <form action="/login" method="post">
        <label for="username">Username:</label>
        <input type="text" id="username" name="username"><br>
        <label for="password">Password:</label>
        <input type="password" id="password" name="password"><br>
        <input type="submit" value="Login">
    </form>
</body>
</html>
                    

Next I defined logic to ensure users authenticate into a secure session, thereby preventing visitors without a valid username and password from accessing the service:



  • I am using the sessions module to create a cookie store that assigns values to the cookie named session. I've also added some configuration to make this cookie adhere to OWASP best security practices:

    cookie with value and properties shown

  • This cookie is set if a user successfully authenticates with a username and password.
  • The logoutHandler invalidates the cookie by setting the authenticated key's value to false.
  • The isAuthenticated function checks the status of the cookie. This is a helper method for the authMiddleware function which will wrap all of the sensitive endpoints on the server: those allowing users to view uploaded files, download these files, or upload new files.
package main

import (
	"net/http"

	"github.com/gorilla/sessions"
)

var store = sessions.NewCookieStore([]byte(session_key))

func loginHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method == "GET" {
		templates.ExecuteTemplate(w, "login.html", nil)
		return
	}

	if r.Method == "POST" {
		r.ParseForm()
		user := r.FormValue("username")
		pass := r.FormValue("password")

		if user == username && pass == password {
			session, err := store.Get(r, "session")
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			session.Values["authenticated"] = true
			session.Save(r, w)
			http.Redirect(w, r, "/", http.StatusSeeOther)
		} else {
			http.Error(w, "Invalid credentials", http.StatusUnauthorized)
		}
	}
}

func logoutHandler(w http.ResponseWriter, r *http.Request) {
	session, err := store.Get(r, "session")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	session.Values["authenticated"] = false
	session.Save(r, w)
	http.Redirect(w, r, "/login", http.StatusSeeOther)
}

func isAuthenticated(r *http.Request) bool {
	session, _ := store.Get(r, "session")
	auth, ok := session.Values["authenticated"].(bool)
	return ok && auth
}

func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if !isAuthenticated(r) {
			http.Redirect(w, r, "/login", http.StatusSeeOther)
			return
		}
		next.ServeHTTP(w, r)
	}
}

                        

Putting it all together

  • The main() function is an entry point for this Go program.
  • The HTML files shown earlier are retrieved from the templates directory.
  • A function is defined that will set OWASP recommended security headers to all of the server's endpoints.
  • HSTS> is enforced for a period of two years, preventing the connection from being downgraded to the insecure HTTP protocol.
  • X-Content-Type-Options is set to a value preventing content sniffing. This forces clients to respect the Content-Type header when uploading and downloading files which will prevent some XSS attacks.
  • This doesn't eliminate the risks associated with allowing file uploads on my server. These risks are explained here and here and are extensive. For this reason I am not going to run this webserver exposed to the public internet. This is a hobby project and the source code should never be used for anything serious.
  • The X-Frame-Options header is set to DENY preventing the content from being displayed in a frame, an important component of Clickjacking attacks.
  • The Content-Security-Policy header is set such that only local scripts, stylesheets, fonts, images, and other resources can be loaded. This prevents XSS and other attacks where remote resources could be used against the server.
  • Session configuration is applied that will set cookies to expire after ten minutes.
  • The HttpOnly cookie flag is set to true in order to prevent the cookie from being retrieved using Javascript in XSS attacks
  • The Secure cookie flag is set to true preventing the cookies value from being transmitted from the client to the server or vice versa as plaintext.
  • The SameSite cookie flag is set to true which will prevent the cookie from being sent to other websites.
  • Lastly, I am using the more secure HTTPS protocol which prevents threat actors from eavesdropping on the content sent to and from this service and also prevents many forms of Man-in-the-middle attacks.
  • package main
    
    import (
        "fmt"
        "html/template"
        "net/http"
    
        "github.com/gorilla/sessions"
    )
    
    var templates = template.Must(template.ParseGlob("templates/*.html"))
    
    func addSecurityHeaders(h http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
            w.Header().Set("X-Content-Type-Options", "nosniff")
            w.Header().Set("X-Frame-Options", "DENY")
            w.Header().Set("Content-Security-Policy", "default-src 'self'")
    
            h.ServeHTTP(w, r)
        })
    }
    
    func main() {
        store.Options = &sessions.Options{
            Path:     "/",
            MaxAge:   60 * 10, // in seconds
            HttpOnly: true,
            Secure:   true, // set HTTPS-only cookies
            SameSite: http.SameSiteStrictMode,
        }
    
        mux := http.NewServeMux()
    
        mux.HandleFunc("/", authMiddleware(indexHandler))
        mux.HandleFunc("/login", loginHandler)
        mux.HandleFunc("/logout", logoutHandler)
        mux.HandleFunc("/new", authMiddleware(uploadHandler))
        mux.HandleFunc("/download/", authMiddleware(fileHandler))
    
        http.Handle("/", addSecurityHeaders(mux))
    
        fmt.Println("Starting server at https://localhost:8443")
        err := http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", nil)
        if err != nil {
            fmt.Printf("Failed to start server: %v\n", err)
        }
    }                            
                            

    Supporting HTTPS

    I've used a self-signed certificate generated using the OpenSSL command openssl x509 -req -days 365 -in localhost.csr -signkey localhost.key -out localhost.crt. For any production environment it would be necessary to obtain this certificate from a trusted certificate authority. This is because web browsers will display a warning when a certificate is presented by a server that isn't signed by a trusted certificate authority:

    untrusted certificate warning

    For local development this can be bypassed by trusting the certificate. Once I did so I was able to confirm that my filesharing service was working as expected:

    untrusted certificate warning
    untrusted certificate warning
    untrusted certificate warning


    Because of how the authMiddleware function was defined, any unauthenticated request to a sensitive endpoint of the server from an unauthenticated user is redirected back to the login page:

                            func authMiddleware(next http.HandlerFunc) http.HandlerFunc {
                                return func(w http.ResponseWriter, r *http.Request) {
                                    if !isAuthenticated(r) {
                                        http.Redirect(w, r, "/login", http.StatusSeeOther)
                                        return
                                    }
                                    next.ServeHTTP(w, r)
                                }
                        

    That's it for this project, thanks for reading!



    View the project source code on GitHub

    Top Of Page