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.
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:
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.
uploadHandler
function expects to receive a request posting a file. The
HTML code defining the file
key
is shown below.
uploads
directory
and
then copy the uploaded file there.
fileHandler
function is responsible for serving a file on the server to
a
client - usually a web browser.
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:
session
. I've also added some
configuration
to make this cookie adhere to OWASP best security practices:
logoutHandler
invalidates the cookie by setting the
authenticated
key's value to false.
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)
}
}
main()
function is an entry point for this Go program.
templates
directory.
Content-Type
header when uploading and downloading files
which will prevent some XSS attacks.
DENY
preventing the content from being displayed
in a frame, an important component of Clickjacking attacks.
HttpOnly
cookie
flag is set to true in order to prevent the cookie from being retrieved using Javascript in
XSS
attacks
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.
SameSite
cookie flag is set to true which will prevent the cookie from
being
sent to other websites.
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)
}
}
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:
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:
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!