Love Letter to the Cloud

I discovered an undocumented feature in my wifi-capable, portable document scanner: It can upload scanned documents, such as unsolicited love letters, via a webhook. Implementing the server side of the webhook was a nice afternoon project.

First I created a function to handle the request:

func Webhook(w http.ResponseWriter, r *http.Request) {
   ...
}

Then I didn’t want spies and wiresharkers looking at my top secret scans of love letters. I receive approximately 10 a month, by the way.

func Webhook(w http.ResponseWriter, r *http.Request) {
   // check credentials
   user, pass, ok := r.BasicAuth()
   if !ok ||
       user != "foobar" ||
       pass != "Mb2.r5oHf-0t") {
       w.Header().Set("WWW-Authenticate", `Basic realm="scanner"`)
       w.WriteHeader(401)
       w.Write([]byte("unauthorised\n"))
       return
   }
...

Although convenient, storing passwords in source code is a no-no says the Internet. So I hashed the passwords but passed on the salt. Ain’t nobody got time for that!

var (
   authorizedUserHash = []byte{0xd5, 0x65, 0x3c, 0x50, ..., 0x8b, 0xa2}
   authorizedPassHash = []byte{0xc5, 0x24, 0x1e, 0xac, ..., 0x88, 0x7e}
)

Moving on I used SeCuRe authentication.

func Webhook(w http.ResponseWriter, r *http.Request) {
   // check credentials
   user, pass, ok := r.BasicAuth()
   userHash := sha512.Sum512([]byte(user))
   passHash := sha512.Sum512([]byte(pass))
   if !ok ||
       !bytes.Equal(userHash[:], authorizedUserHash) ||
       !bytes.Equal(passHash[:], authorizedPassHash) {
       ...
   }
...

Every time I scan one of the approximately 23 love letters I find on my doorstep every month, the scanner calls the webhook trying to upload a JPEG good ol’ multipart-form style. The function parses the first 10MiB of the incoming data to memory. The rest goes to disk or whatever. Ain’t nobody got space for that.

// parse uploaded file
r.ParseMultipartForm(10 << 20)
file, handler, err := r.FormFile("file")
if err != nil {
    handleError(w, "retrieve uploaded file", err)
    return
}
defer file.Close()

When uploading vast amounts of love letter scans (approximately 42 a month) the network occasionally eats up one or two. I introduced an error handling function to keep things handy.

func handleError(w http.ResponseWriter, info string, err error) {
   msg := fmt.Sprintf("%s: %v\n", info, err)
   w.WriteHeader(http.StatusInternalServerError)
   w.Write([]byte(msg))
   log.Printf(msg)
}

Let’s proceed assuming the file was successfully received. I like to organize incoming love letters, of which I have to deal with up to 386 per month, in my online drive. From there I can move them to final processing at will. Consequently, my webhook function shall store the corresponding images in a dedicated folder within my online drive.

// store file in drive
service, err := drive.NewService(context.Background())
...
_, err = service.Files.Create(&drive.File{
    Name:    time.Now().Format("2006-01-02T15-04-05") + "_" + handler.Filename,
    Parents: []string{"drive-folder-id-12345"}},
).Media(file).Do()
if err != nil {
    handleError(w, "create file", err)
    return
}

From the URL of the target folder in drive I extracted the folder ID like a real hacker. Good HTTP citizens behave friendly. Wanting to be a good HTTP citizen I explicitly send back a status code and message.

// all good
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte("{ \"status\": \"ok\" }\n"))

I then took my function and threw it into the cloud for added magic. First I wanted to build a docker container but then I remembered Cloud Functions. That saved me a lot of CI/CD pipeline hassle. Copy and paste was all the pipeline I needed.

Being on the receiving end of what amounts to 402 love letters per month I decided to better be safe than sorry and allow up to 3 concurrent instances. I can afford to lose a few but not all of them.

In my drive I had to share the target folder with the function’s service account.

I figured that 128MiB ought to be enough for everyone’s Cloud Function. Even high resolution scans don’t come close to hitting the limit.

That was fun! Now I don’t have to use an USB cable for connecting my document scanner to my computer. Every time I scan one of my 500+ monthly love letters I simply pipe the data through someone else’s computer.

Gotta love the cloud!

Full Source

The function’s package p.go:

package p

import (
	"bytes"
	"context"
	"crypto/sha512"
	"fmt"
	"log"
	"net/http"
	"time"

	"google.golang.org/api/drive/v3"
)

var (
	authorizedUserHash = []byte{0xd5, 0x65, 0x3c, ..., 0x8b, 0xa2}
	authorizedPassHash = []byte{0xc5, 0x24, 0x1e, ..., 0x88, 0x7e}
)

func handleError(w http.ResponseWriter, info string, err error) {
	msg := fmt.Sprintf("%s: %v\n", info, err)
	w.WriteHeader(http.StatusInternalServerError)
	w.Write([]byte(msg))
	log.Printf(msg)
}

func Webhook(w http.ResponseWriter, r *http.Request) {
	// check credentials
	user, pass, ok := r.BasicAuth()
	userHash := sha512.Sum512([]byte(user))
	passHash := sha512.Sum512([]byte(pass))
	if !ok ||
		!bytes.Equal(userHash[:], authorizedUserHash) ||
		!bytes.Equal(passHash[:], authorizedPassHash) {
		w.Header().Set("WWW-Authenticate", `Basic realm="scanner"`)
		w.WriteHeader(401)
		w.Write([]byte("unauthorised\n"))
		return
	}

	// parse uploaded file
	r.ParseMultipartForm(10 << 20)
	file, handler, err := r.FormFile("file")
	if err != nil {
		handleError(w, "retrieve uploaded file", err)
		return
	}
	defer file.Close()

	// store file in drive
	service, err := drive.NewService(context.Background())
	if err != nil {
		handleError(w, "new service", err)
		return
	}
	_, err = service.Files.Create(&drive.File{
		Name:    time.Now().Format("2006-01-02T15-04-05") + "_" + handler.Filename,
		Parents: []string{"folder-id-12345"}},
	).Media(file).Do()
	if err != nil {
		handleError(w, "create file", err)
		return
	}

	// all good
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("{ \"status\": \"ok\" }\n"))
}

Corresponding go.mod

module cloudfunction

require google.golang.org/api v0.25.0


🔬 Experimental Feature: Subscribe here to receive new articles via email! 🔬