Saturday, July 18, 2020

Google Cloud Functions Issues Upgrading From Go 1.11 To 1.13

I use Google Cloud Functions with Go.  However, I upgraded from Go 1.11 to Go 1.13 (because Go 1.11 is being deprecated) and ran into some annoying, undocumented issues.

Static Files And The Current Working Directory

One of my Cloud Functions acts as a tiny web server; it has a few static HTML files that it serves in addition to its dynamic things.

In Go 1.11, Cloud Functions put the static files (and all the source files, for that matter) in the working directory of the function.  This (1) makes sense, and (2) makes testing easy.

However, in Go 1.13, Cloud Functions puts the static files (and all of the source files) is placed in the ./serverless_function_source_code directory.  Why?  Who knows.  All that mattered is that after a simple version upgrade, all of my stuff broke because it couldn't find files that it was able to find before the upgrade.

I found that using a sync.Once to attempt to change the current working directory (if necessary) is a fairly clean backward-compatible way of handling this issue.

Here's an example; it's fairly verbose, but you could rip out most of the logging if you don't want or need it.

// GoogleCloudFunctionSourceDirectory is where Google Cloud will put the source code that was uploaded.
//
const GoogleCloudFunctionSourceDirectory = "serverless_function_source_code"

// once is an object that will only execute its function one time.
//
// Because we want to log during our initialization, we need to handle this in a non-standard
// function and keep track of our initialization status.
var once sync.Once

// Initialize initializes the application.
//
// Primarily, this changes the current working directory.
func Initialize(log *logrus.Logger) {
log.Infof("Initializing the application.")

path, err := os.Getwd()
if err != nil {
log.Warnf("Could not find the current working directory: %v", err)
}
log.Infof("Current working directory: %s", path)

log.Infof("Looking for top-level source directory: %s", GoogleCloudFunctionSourceDirectory)
fileInfo, err := os.Stat(GoogleCloudFunctionSourceDirectory)
if err == nil && fileInfo.IsDir() {
log.Infof("Found top-level source directory: %s", GoogleCloudFunctionSourceDirectory)
err = os.Chdir(GoogleCloudFunctionSourceDirectory)
if err != nil {
log.Warnf("Could not change to directory %q: %v", GoogleCloudFunctionSourceDirectory, err)
}
}

log.Infof("Initialization complete.")
}

// CloudFunction is an HTTP Cloud Function with a request parameter.
func CloudFunction(w http.ResponseWriter, r *http.Request) {
log := logrus.New()

// Initialize our application if we haven't already.
once.Do(func() { Initialize(log) })

// YOUR CLOUD FUNCTION LOGIC HERE
}

For more information, see the Cloud Functions concepts docs.

Logging And Environment Variables

For whatever reason, Cloud Functions with Go don't log at anything other than the "default" log level; this means that all of my carefully crafted log messages all just get dumped into the logs at the same severity.

I've been using gcfhook with logrus to get around this, but it's not an ideal solution.  That combination works by nullifying all output of the application and then adding a logrus hook that connects to the StackDriver API to send proper logs over the network.  It works fine, but it's silly to have to make a network connection to a logging API when the application itself can output directly.

As of Go 1.13, Cloud Functions will no longer set the FUNCTION_NAME, FUNCTION_REGION, and GCP_PROJECT environment variables.  This is a problem because we need those three pieces of information in order to use the StackDriver API to send the log messages.  You could publish those environment variables back as part of your deployment, but I'd prefer not to.

Fortunately, Cloud Functions can now parse (poorly documented) JSON-formatted lines from stdout and stderr, resulting in proper log messages with severities.  The Cloud Functions docs refer to this as "structured logging", but the docs don't seem to apply correctly.  Cloud Run has a document on how these JSON-formatted lines should look, but it's still a bit hazy.

Anyway, the gcfstructuredlogformatter package introduces a logrus formatter that outputs JSON instead of plain text for logs.  This eliminates the need for the extra environment variables and generally simplifies the logging workflow.  It should only be a couple of lines of code to sub out gcfhook for gcfstructuredlogformatter.

Here's an example:

// CloudFunction is an HTTP Cloud Function with a request parameter.
func CloudFunction(w http.ResponseWriter, r *http.Request) {
log := logrus.New()

if value := os.Getenv("FUNCTION_TARGET"); value == "" {
log.Infof("FUNCTION_TARGET is not set; falling back to normal logging.")
} else {
formatter := gcfstructuredlogformatter.New()

log.SetFormatter(formatter)
}

log.Infof("This is an info message.")
log.Warnf("This is a warning message.")
log.Errorf("This is an error message.")

// YOUR CLOUD FUNCTION LOGIC HERE
}

Hopefully this stopped you from banging your head against the wall for a few hours like I was doing as I tried to frantically figure out why the upgrade had failed in such weird ways.


Wednesday, June 3, 2020

Sharing a single screen in Slack for Linux

I have a bunch of monitors, and for whatever reason, Slack for Linux refuses to let me limit my screen sharing to a single monitor or application.  This means that if I try to share my screen on a call, no one can see or read anything because they just see a giant, wide view of three monitors' worth of pixels crammed into their Slack window (typically only one monitor wide).

In my experience, disabling monitors/displays is just not worth it; I'll have to spend too much time getting everything set back up correctly afterward, and that's really inconvenient and annoying.

The solution that I've landed on is Xephyr; Xephyr runs a second X11 server inside a new window, so when I need to get on a call where I'll have to share my screen, I simply:
  1. Launch a new Xephyr display.
  2. Close Slack.
  3. Open Slack on the Xephyr display.
  4. Open whatever else I'll need to share in the Xephyr display, typically a web browser or a terminal.
  5. Get on the Slack call and share my "screen".
Some small details:
  • You'll need to open Xephyr with the resolution that you want; given window decorations and such, you may need to play around with this a bit.  Once you find out what works, put it in a script.
  • In order to resize windows in Xephyr, it'll need to be running a window manager.  I struggled to get any "startx"-related things working, but I found that "twm" worked well enough for my purposes.
  • Some applications, such as Chrome, won't open on two displays at the same time.  I just open a different browser in my Xephyr display (for example, I use "google-chrome" normally and "chromium-browser" in Xephyr), but you can also run Chrome using a different profile directory and it'll run in the other display.
Install Xephyr and TWM:
sudo apt install xserver-xephyr twm

Run Zephyr, Slack, and Chromium:
# Launch Xephyr and create display ":1".
Xephyr -ac -noreset -screen 1920x1000 :1 &
# Start a window manager in Xephyr.
DISPLAY=:1 twm &>/dev/null &
# Open Slack in Xephyr.
DISPLAY=:1 slack &>/dev/null &
# Open Chromium in Xephyr.
DISPLAY=:1 chromium-browser &>/dev/null &

It's kind of dirty, but it works extremely well, and I don't have to worry about messing with my monitor setup when I need to give a presentation.

Edit: an earlier version of this post used "Xephyr -bc -ac -noreset -screen 1920x1000 :1 &" for the Xephyr command; I can't get this to work with "-bc" anymore; I must have copied the wrong command when I published the post.

Wednesday, January 29, 2020

Unit-testing reCAPTCHA v2 and v3 in Go

I recently worked on a project where we allowed new users to sign up for our system with a form.  A new user would need to provide us with her name, her e-mail address, and a password.  In order to prevent spamming, we used reCAPTCHA v3, and so that meant that we also submitted a reCAPTCHA token along with the rest of the new-user data.

Unit-testing the sign-up process was fairly simple if we turned off the reCAPTCHA requirement, but the weakest link in the whole process is the one part that we could not control: reCAPTCHA.  It would be foolish not to have test coverage around the reCAPTCHA workflow.

So, how do you unit-test reCAPTCHA?

Focus: Server-side testing

For the purposes of this post, I'm going to be focusing on testing reCAPTCHA on the server side.  This means that I'm not concerned with validating that users acted like humans fiddling around on a website.  Instead, I'm concerned with what our sign-up endpoint does when it receives valid and invalid reCAPTCHA tokens.

reCAPTCHA in Go

There are a variety of Go package to provide reCAPTCHA support; however, only one of them (1) has support for Go modules, and (2) has support for unit testing built in:

For docs, see:

When you create a new reCAPTCHA site, you're given public and private keys (short little strings, nothing huge).  The public key is used on the client side when you make your connection to the reCAPTCHA API, and a response token is provided back.  The private key is used on the server side to connect to the reCAPTCHA API and validate the response token.

Since the client side will likely be a line or two of Javascript that generates a token, our server-side work will be focused on validating that token.

Assuming that the newly generated token is "NEW_TOKEN" and that the private key is "YOUR_PRIVATE_KEY", then this is all you have to do in order to validate that token:

import "github.com/tekkamanendless/go-recaptcha"

// ...

recaptchaVerifier := recaptcha.New("YOUR_PRIVATE_KEY")
success, err := recaptchaVerifier.Verify("NEW_TOKEN")
if err != nil {
   // Fail with some 500-level error about not being able to verify the token
}


if !success {
   // Fail with some 400-level error about not being a human
}


// The token is valid!


And that's it!  All we really care about is whether or not it worked.

Unit testing

tekkamanendless/go-recaptcha includes a package called "recaptchatest" that provides a fake reCAPTCHA API running as an httptest.Server instance.  This server simulates enough of the reCAPTCHA API to let you do the kinds of testing that you need to.

Just like the actual reCAPTCHA service, you can create multiple "sites" on the test server.  Each site will have a public and private key, and you can call the NewResponseToken method of a site to have that site generate a valid token for that site.

In terms of design, you'll set up the test server, the test site, and the valid token in advance of your test.  When you create your Recaptcha instance with the test site's private key, all you have to do is set the VerifyEndpoint property of that instance to point to the test server (otherwise, it would try to talk to the real reCAPTCHA API and fail).

Here's a simple example:

import (
   "github.com/tekkamanendless/go-recaptcha" 
   "github.com/tekkamanendless/go-recaptcha/recaptchatest"
)

// ...

// Create a new reCAPTCHA test server, site, and valid token before the main test.
testServer := recaptchatest.NewServer()
defer testServer.Close()

site := testServer.NewSite()
token := site.NewResponseToken()

// Create the reCAPTCHA verifier with the site's private key.
recaptchaVerifier := recaptcha.New(site.PrivateKey)

// Override the endpoint so that it uses the test server.
recaptchaVerifier.VerifyEndpoint = testServer.VerifyEndpoint()

// Run your test.

// ...

// Validate that the reCAPTCHA token is good.
success, err := recaptchaVerifier.Verify(token)
assert.Nil(t, err)
assert.True(t, success)

The recaptchatest test server doesn't do too much that's fancy, but it will properly return a failure if the same token is verified twice or if the token is too old.  It also has some functions to let you tweak the token properties so you don't have to wait around for 2 minutes for a token to age out; you can make one that's already too old (see Site.GenerateToken for more information).