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