<file_sep>package passphrase

//go:generate go run generate.go

import (
	"bytes"
	"crypto/rand"
	"io"
	"math/big"
	"sync"
)

// MaxWorkerCount sets the concurrency limit for random number generation.
var MaxWorkerCount = 128

var wordsCount = int64(len(Words))

type result struct {
	Number int64
	Error  error
}

func generateRandomNumber(maxSize int64, results chan result) {
	bigInt, err := rand.Int(rand.Reader, big.NewInt(maxSize))
	if err != nil {
		results <- result{0, err}
		return
	}
	results <- result{bigInt.Int64(), nil}
}

func generateRandomNumbers(maxSize int64, results chan result, count int) {
	if count <= MaxWorkerCount {
		for i := 0; i < count; i++ {
			go generateRandomNumber(maxSize, results)
		}
		return
	}
	tasks := make(chan int)
	var wg sync.WaitGroup
	for worker := 0; worker < MaxWorkerCount; worker++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for range tasks {
				generateRandomNumber(maxSize, results)
			}
		}()
	}
	for i := 0; i < count; i++ {
		tasks <- i
	}
	close(tasks)
	wg.Wait()
}

// Write writes a passphrase with the given number of words.
func Write(writer io.Writer, numberOfWords int) (n int, err error) {
	results := make(chan result)
	go generateRandomNumbers(wordsCount, results, numberOfWords)
	for i := 0; i < numberOfWords; i++ {
		result := <-results
		if result.Error != nil {
			return n, result.Error
		}
		str := Words[result.Number]
		if n != 0 {
			str = " " + str
		}
		bytesWritten, err := io.WriteString(writer, str)
		n += bytesWritten
		if err != nil {
			return n, err
		}
	}
	return n, nil
}

// String returns a passphrase with the given number of words.
func String(numberOfWords int) (str string, err error) {
	var buffer bytes.Buffer
	_, err = Write(&buffer, numberOfWords)
	if err != nil {
		return "", err
	}
	return string(buffer.Bytes()), nil
}
<file_sep>FROM golang:alpine as build
WORKDIR /opt/passphrase
COPY . .
# ldflags explanation (see `go tool link`):
#   -s  disable symbol table
#   -w  disable DWARF generation
RUN cd ./passphrase && go build -ldflags="-s -w" -o /bin/passphrase

FROM scratch
COPY --from=build /bin/passphrase /bin/
USER 65534
ENTRYPOINT ["passphrase"]
<file_sep>#!/bin/sh

#
# Starts the given make target and a watch process for source file changes.
# Reloads the active Chrome/Safari/Firefox tab.
# Optionally executes a command on each source file change.
#
# Requires make for the command execution.
# Requires entr for the watch process.
#
# Usage: ./watch.sh target [chrome|safari|firefox] [-- cmd args...]
#

stop() {
  STATUS=$?
  kill "$PID"
  exit $STATUS
}

start() {
  make -s "$1" &
  PID=$!
}

cd "$(dirname "$0")" || exit 1

trap stop INT TERM
start "$1"
shift

while true; do
  find . -name '*.go' | entr -d -p ./reload-browser.sh "$@"
done
<file_sep>package parse_test

import (
	"strconv"
	"testing"

	"github.com/blueimp/passphrase/internal/parse"
)

func TestNaturalNumber(t *testing.T) {
	number := parse.NaturalNumber("")
	if number != 0 {
		t.Errorf(
			"Failed to handle empty parameter, got: %d, expected: %d.",
			number,
			0,
		)
	}
	number = parse.NaturalNumber(strconv.Itoa(parse.MaxInt) + "0")
	if number != parse.MaxInt {
		t.Errorf(
			"Failed to handle int overflow, got: %d, expected: %d.",
			number,
			parse.MaxInt,
		)
	}
	number = parse.NaturalNumber(strconv.Itoa(-parse.MaxInt-1) + "0")
	if number != 0 {
		t.Errorf(
			"Failed to handle int underflow, got: %d, expected: %d.",
			number,
			0,
		)
	}
	number = parse.NaturalNumber(strconv.Itoa(parse.MaxInt))
	if number != parse.MaxInt {
		t.Errorf(
			"Failed to handle max int, got: %d, expected: %d.",
			number,
			parse.MaxInt,
		)
	}
	number = parse.NaturalNumber("banana")
	if number != 0 {
		t.Errorf(
			"Failed to handle non int string, got: %d, expected: %d.",
			number,
			0,
		)
	}
	for i := 0; i <= 10; i++ {
		number := parse.NaturalNumber(strconv.Itoa(i))
		if number != i {
			t.Errorf(
				"Failed to handle positive number as parameter, got: %d, expected: %d.",
				number,
				i,
			)
		}
	}
	for i := -10; i < 0; i++ {
		number := parse.NaturalNumber(strconv.Itoa(i))
		if number != 0 {
			t.Errorf(
				"Failed to handle negative number as parameter, got: %d, expected: %d.",
				number,
				0,
			)
		}
	}
	for i := 0; i <= 10; i++ {
		number := parse.NaturalNumber("", i)
		if number != i {
			t.Errorf(
				"Failed to handle default number, got: %d, expected: %d.",
				number,
				i,
			)
		}
	}
	for i := 0; i <= 10; i++ {
		number := parse.NaturalNumber("100", 0, i)
		if number != i {
			t.Errorf(
				"Failed to respect max number, got: %d, expected: %d.",
				number,
				i,
			)
		}
	}
	for i := 0; i <= 10; i++ {
		number := parse.NaturalNumber("-1", 0, parse.MaxInt, i)
		if number != i {
			t.Errorf(
				"Failed to respect min number, got: %d, expected: %d.",
				number,
				i,
			)
		}
	}
}
<file_sep>package strings

import (
	"testing"
)

func TestInSlice(t *testing.T) {
	list := []string{"apple", "banana", "coconut"}
	if !InSlice("apple", list) {
		t.Error("Failed to find string in list.")
	}
	if InSlice("orange", list) {
		t.Error("Incorrectly found non-member string in list.")
	}
}
<file_sep>module github.com/blueimp/passphrase

go 1.12
<file_sep>module github.com/blueimp/passphrase/appengine

require github.com/blueimp/passphrase v1.0.0

replace github.com/blueimp/passphrase v1.0.0 => ../

// Use alternative replace pattern to deploy to App Engine:
//replace github.com/blueimp/passphrase v1.0.0 => ./passphrase
<file_sep>package main

import (
	"fmt"
	"io/ioutil"
	"os"
	"strconv"
	"strings"
	"testing"

	"github.com/blueimp/passphrase"
	istrings "github.com/blueimp/passphrase/internal/strings"
)

func generatePassphrase(args []string) (code int, out string, err string) {
	os.Args = append([]string{"noop"}, args...)
	outReader, outWriter, _ := os.Pipe()
	errReader, errWriter, _ := os.Pipe()
	originalOut := os.Stdout
	originalErr := os.Stderr
	defer func() {
		os.Stdout = originalOut
		os.Stderr = originalErr
	}()
	os.Stdout = outWriter
	os.Stderr = errWriter
	exit = func(c int) {
		code = c
	}
	func() {
		main()
		outWriter.Close()
		errWriter.Close()
	}()
	stdout, _ := ioutil.ReadAll(outReader)
	stderr, _ := ioutil.ReadAll(errReader)
	return code, string(stdout), string(stderr)
}

func TestMain(t *testing.T) {
	code, out, err := generatePassphrase([]string{})
	if code != 0 {
		t.Errorf("Unexpected status code, got %d, expected: %d.", code, 0)
	}
	if err != "" {
		t.Errorf("Unexpected error output: %s.", err)
	}
	number := len(strings.Split(out, " "))
	if number != defaultNumber {
		t.Errorf(
			"Incorrect default number of words, got: %d, expected: %d.",
			defaultNumber,
			number,
		)
	}
	code, out, err = generatePassphrase([]string{"test"})
	if code != 1 {
		t.Errorf("Unexpected status code, got %d, expected: %d.", code, -1)
	}
	if err != "argument is not a natural number: test\n" {
		t.Errorf("Expected \"not a natural number\" error, got: \"%s\"", err)
	}
	if out != "\n" {
		t.Errorf("Expected empty passphrase, got: %s", out)
	}
	code, out, err = generatePassphrase([]string{"0"})
	if code != 0 {
		t.Errorf("Unexpected status code, got %d, expected: %d.", code, 0)
	}
	if err != "" {
		t.Errorf("Unexpected error output: %s.", err)
	}
	if out != "\n" {
		t.Errorf("Expected empty passphrase, got: %s", out)
	}
	for i := -1; i >= -10; i-- {
		code, out, err := generatePassphrase([]string{strconv.Itoa(i)})
		if code != 1 {
			t.Errorf("Unexpected status code, got %d, expected: %d.", code, -1)
		}
		if err != fmt.Sprintf("argument is not a natural number: %d\n", i) {
			t.Errorf("Expected \"not a natural number\" error, got: \"%s\"", err)
		}
		if out != "\n" {
			t.Errorf("Expected empty passphrase, got: %s", out)
		}
	}
	for i := 1; i <= 10; i++ {
		code, out, err := generatePassphrase([]string{strconv.Itoa(i)})
		if code != 0 {
			t.Errorf("Unexpected status code, got %d, expected: %d.", code, 0)
		}
		if err != "" {
			t.Errorf("Unexpected error output: %s.", err)
		}
		words := strings.Split(strings.TrimSuffix(out, "\n"), " ")
		number := len(words)
		if number != i {
			t.Errorf("Incorrect number of words, got: %d, expected: %d.", number, i)
		}
		for _, word := range words {
			if !istrings.InSlice(word, passphrase.Words[:]) {
				t.Errorf("Passphrase word is not in the word list: %s", word)
			}
			if len(word) < passphrase.MinWordLength {
				t.Errorf(
					"Passphrase word is shorter than %d characters: %s",
					passphrase.MinWordLength,
					word,
				)
			}
		}
	}
}
<file_sep>package main

import (
	"fmt"
	"os"

	"github.com/blueimp/passphrase"
	"github.com/blueimp/passphrase/internal/parse"
)

const defaultNumber = 4

var exit = os.Exit

func main() {
	var number int
	if len(os.Args) > 1 {
		arg := os.Args[1]
		number = parse.NaturalNumber(arg, defaultNumber)
		if number == 0 && arg != "0" {
			fmt.Fprintln(os.Stderr, "argument is not a natural number:", arg)
			exit(1)
		}
	} else {
		number = defaultNumber
	}
	passphrase.MaxWorkerCount = parse.NaturalNumber(
		os.Getenv("PASSPHRASE_MAX_WORKER_COUNT"),
		passphrase.MaxWorkerCount,
		parse.MaxInt,
		1,
	)
	_, err := passphrase.Write(os.Stdout, number)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		exit(1)
	}
	fmt.Println()
}
<file_sep>#!/bin/sh

#
# Reloads the active tab of the given browser (defaults to Chrome).
# Keeps the browser window in the background (Chrome/Safari only).
# Can optionally execute a given command before reloading the browser tab.
# Browser reloading is supported on MacOS only for now.
#
# Usage: ./reload-browser.sh [chrome|safari|firefox] -- [command args...] 
#

set -e

RELOAD_CHROME='tell application "Google Chrome"
  reload active tab of window 1
end tell'

RELOAD_SAFARI='tell application "Safari"
  set URL of document 1 to (URL of document 1)
end tell'

RELOAD_FIREFOX='activate application "Firefox"
tell application "System Events" to keystroke "r" using command down'

case "$1" in
  firefox)  OSASCRIPT=$RELOAD_FIREFOX;;
  safari)   OSASCRIPT=$RELOAD_SAFARI;;
  *)        OSASCRIPT=$RELOAD_CHROME;;
esac

if shift; then
  [ "$1" = "--" ] && shift
  "$@"
fi

if command -v osascript > /dev/null 2>&1; then
  exec osascript -e "$OSASCRIPT"
fi
<file_sep>../../words.go<file_sep># --- Variables ---

# Include .env file if available:
-include .env

# The platform to use for local development and deployment.
# Can be either "appengine" or "lambda":
PLATFORM ?= appengine

# Fake AWS credentials as fix for AWS SAM Local issue #134:
# See also https://github.com/awslabs/aws-sam-local/issues/134
FAKE_AWS_ENV = AWS_ACCESS_KEY_ID=0 AWS_SECRET_ACCESS_KEY=0

# AWS CLI wrapped with aws-vault for secure credentials access,
# can be overriden by defining the AWS_CLI environment variable:
AWS_CLI ?= aws-vault exec '$(AWS_PROFILE)' -- aws

# The absolute path for the passphrase binary installation:
BIN_PATH = $(GOPATH)/bin/passphrase

# Dependencies to build the passphrase command-line interface:
CLI_DEPS = passphrase/cli.go passphrase/go.mod passphrase.go words.go

# Dependencies to build the lambda application:
LAMBDA_DEPS = lambda/lambda.go lambda/go.mod passphrase.go words.go


# --- Main targets ---

# The default target builds the CLI binary:
all: passphrase/passphrase

# Cross-compiles the lambda binary:
lambda: lambda/bin/main

# Generates the word list as go code:
words:
	go generate

# Runs the unit tests for all components:
test: words.go
	@go test ./...
	@cd passphrase; go test ./...
	@cd appengine; go test ./...
	@cd lambda; go test ./...

# Installs the passphrase binary at $GOPATH/bin/passphrase:
install: $(BIN_PATH)

# Deletes the passphrase binary from $GOPATH/bin/passphrase:
uninstall:
	rm -f $(BIN_PATH)

# Generates a sample lambda event:
event: lambda/event.json

# Invokes the lambda function locally:
invoke: lambda/event.json lambda/bin/main
	cd lambda; $(FAKE_AWS_ENV) sam local invoke -e event.json

# Starts a local server for the given platform:
start: $(PLATFORM)-start

# Starts a local server for the given platform and a watch process:
watch: $(PLATFORM)-watch

# Deploys the project for the given platform:
deploy: $(PLATFORM)-deploy

# Opens a browser tab with the production URL of the App Engine project:
browse:
	cd appengine; gcloud app browse --project $(PROJECT)

# Prints the API Gateway URL of the deployed lambda function:
url: lambda/passphrase.url
	@grep -o 'https://.*' lambda/passphrase.url

# Deletes the CloudFormation stack of the lambda function:
destroy:
	rm -f lambda/passphrase.url
	$(AWS_CLI) cloudformation delete-stack --stack-name '$(STACK_NAME)'

# Removes all build artifacts:
clean:
	rm -f \
		lambda/bin/main \
		lambda/debug \
		lambda/debug.test \
		lambda/deploy.yml \
		lambda/deployed.txt \
		lambda/event.json \
		lambda/passphrase.url \
		passphrase/debug \
		passphrase/debug.test \
		passphrase/passphrase


# --- Helper targets ---

# Defines phony targets (targets without a corresponding target file):
.PHONY: \
	all \
	passphrase \
	lambda \
	words \
	test \
	install \
	uninstall \
	event \
	invoke \
	start \
	watch \
	deploy \
	browse \
	url \
	destroy \
	appengine-start \
	appengine-watch \
	appengine-deploy \
	lambda-start \
	lambda-watch \
	lambda-deploy \
	clean

# Installs the passphrase binary at $GOPATH/bin/passphrase:
$(BIN_PATH): $(CLI_DEPS)
	cd passphrase; go install

# Builds the passphrase binary:
passphrase/passphrase: $(CLI_DEPS)
	cd passphrase; go build

# Generates the word list as go code if generate.go or words.txt change:
words.go: generate.go words.txt
	go generate

# Starts a local App Engine server:
appengine-start:
	cd appengine; dev_appserver.py .

# Starts a local App Engine server and a watch process for source file changes,
# on MacOS also automatically reloads the active Chrome/Safari/Firefox tab:
appengine-watch:
	@exec ./watch.sh start $(BROWSER)

# Deploys the App Engine project to Google Cloud:
appengine-deploy:
	cd appengine; gcloud app deploy --project $(PROJECT) --version $(VERSION)

# Starts a local API Gateway:
# Fake AWS credentials as fix for AWS SAM Local issue #134:
# See also https://github.com/awslabs/aws-sam-local/issues/134
lambda-start:
	cd lambda; AWS_ACCESS_KEY_ID=0 AWS_SECRET_ACCESS_KEY=0 sam local start-api

# Starts a local API Gateway and a watch process for source file changes,
# on MacOS also automatically reloads the active Chrome/Safari/Firefox tab:
lambda-watch:
	@exec ./watch.sh start $(BROWSER) -- make -s lambda

# Deploys the lambda function to AWS:
lambda-deploy: lambda/deployed.txt url

# Cross-compiles the lambda binary:
# ldflags explanation (see `go tool link`):
#   -s  disable symbol table
#   -w  disable DWARF generation
lambda/bin/main: $(LAMBDA_DEPS)
	cd lambda; \
		GOOS=linux GOARCH=amd64 go build -ldflags='-s -w' -o bin/main

# Generates a sample lambda event:
lambda/event.json:
	cd lambda; sam local generate-event api > event.json

# Packages the lambda binary and uploads it to S3:
lambda/deploy.yml: lambda/bin/main lambda/template.yml
	cd lambda; $(AWS_CLI) cloudformation package \
		--template-file template.yml \
		--s3-bucket '$(DEPLOYMENT_BUCKET)' \
		--s3-prefix '$(DEPLOYMENT_PREFIX)' \
		--output-template-file deploy.yml

# Deploys the packaged binary to AWS:
lambda/deployed.txt: lambda/deploy.yml
	cd lambda; $(AWS_CLI) cloudformation deploy \
		--template-file deploy.yml \
		--stack-name '$(STACK_NAME)' \
		--parameter-overrides LambdaRole='$(LAMBDA_ROLE)'
	date >> lambda/deployed.txt

# Generates a passphrase.url file with the API Gateway URL:
lambda/passphrase.url:
	API_GW_ID=$$($(AWS_CLI) cloudformation describe-stack-resource \
		--stack-name '$(STACK_NAME)' \
		--logical-resource-id ServerlessRestApi \
		--query StackResourceDetail.PhysicalResourceId \
		--output text \
	) && \
	printf '%s\nURL=https://%s.execute-api.$(AWS_REGION).amazonaws.com/Prod\n' \
		[InternetShortcut] \
		"$$API_GW_ID" \
		> lambda/passphrase.url
<file_sep>../../go.mod<file_sep># Passphrase
> Better passwords by combining random words.

Passphrase is a Go library, command-line interface, Google App Engine
application and AWS Lambda function to generate a random sequence of words.

It is inspired by <NAME>'s [xkcd webcomic #936](https://xkcd.com/936/)
with the title "Password Strength":

![Password Strength](https://imgs.xkcd.com/comics/password_strength.png)

## Installation
The `passphrase` command-line interface can be installed via
[go get](https://golang.org/cmd/go/):

```sh
go get github.com/blueimp/passphrase/passphrase
```

## Usage
By default, `passphrase` prints four space-separated words, but also accepts
an argument for the number of words to generate:

```sh
passphrase [number]
```

The concurrency limit for random number generation (default: `128`) can be
adjusted with the environment variable `PASSPHRASE_MAX_WORKER_COUNT`:

```sh
PASSPHRASE_MAX_WORKER_COUNT=1000 passphrase 1000
```

## Import
The `passphrase` library can be imported and used with any type
implementing the [io.Writer interface](https://golang.org/pkg/io/#Writer), e.g.
`os.Stdout`:

```go
package main

import (
	"fmt"
	"os"

	"github.com/blueimp/passphrase"
)

var exit = os.Exit

func main() {
	_, err := passphrase.Write(os.Stdout, 4)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		exit(1)
	}
	fmt.Println()
}
```

Or alternatively with a simple `string` return value:

```go
package main

import (
	"fmt"
	"os"

	"github.com/blueimp/passphrase"
)

var exit = os.Exit

func main() {
	pass, err := passphrase.String(4)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		exit(1)
	}
	fmt.Println(pass)
}
```

## Word list
This repository includes the word list `google-10000-english-usa-no-swears.txt`
from Josh Kaufman's repository
[google-10000-english](https://github.com/first20hours/google-10000-english/),
but `passphrase` can also be compiled with another list of newline separated
words.

The words module can be generated the following way:

```sh
WORD_LIST_URL=words.txt MIN_WORD_LENGTH=3 make words
```

The `WORD_LIST_URL` variable can point to a URL or a local file path and falls
back to `words.txt`.

Words shorter than `MIN_WORD_LENGTH` (defaults to a minimum word length of `3`
characters) are filtered out.

The updated word list module can then be used in a new build.

## Build
First, clone the project and then switch into its source directory:

```sh
git clone https://github.com/blueimp/passphrase.git
cd passphrase
```

*Please note:*  
This project relies on [Go modules](https://github.com/golang/go/wiki/Modules)
for automatic dependency resolution.

To build the CLI binary, run
[Make](https://en.wikipedia.org/wiki/Make_\(software\)) in the repository:

```sh
make
```

The locally built binary can be installed at `$GOPATH/bin/passphrase` with the
following command:

```sh
make install
```

The uninstall command removes the binary from `$GOPATH/bin/passphrase`:

```sh
make uninstall
```

To clean up all build artifacts, run the following:

```sh
make clean
```

## Test
All components come with unit tests, which can be executed the following way:

```sh
make test
```

## Google App Engine
Passphrase can be deployed as
[Google App Engine](https://cloud.google.com/appengine/docs/go/) application.

The application accepts a query parameter `n` to define the number of words to
generate, but limits the sequence to `100` words, e.g.:

```
https://PROJECT.appspot.com/?n=100
```

### Requirements
App engine development and deployment requires the
[Google Cloud SDK](https://cloud.google.com/appengine/docs/standard/go/download)
with the `app-engine-go` component.

On MacOS, `google-cloud-sdk` can be installed via
[Homebrew Cask](https://caskroom.github.io/).

```sh
brew cask install google-cloud-sdk
gcloud components install app-engine-go
```

To make `dev_appserver.py` available in the `PATH`, a symlink has to be added:

```sh
ln -s /usr/local/Caskroom/google-cloud-sdk/latest/google-cloud-sdk/bin/dev_* \
	/usr/local/bin/
```

The local watch task requires [entr](https://bitbucket.org/eradman/entr) to be
installed, which is available in the repositories of popular Linux distributions
and can be installed on MacOS via [Homebrew](https://brew.sh/):

```sh
brew install entr
```

### Environment variables
The following variables have to be set, e.g. by adding them to a `.env` file,
which gets included in the provided `Makefile`:

```sh
# The App Engine project:
PROJECT=passphrasebot
# The App Engine project version:
VERSION=1
```

### Deploy
To deploy the application, execute the following:

```sh
make deploy
```

To open the URL of the application in a browser tab, run the following:

```sh
make browse
```

### Local development
To start a local App Engine server, run the following:

```sh
make start
```

On MacOS, to also automatically reload the active Chrome/Safari/Firefox tab, run
the following:

```sh
[BROWSER=chrome|safari|firefox] make watch
```

## AWS Lambda
Passphrase can be deployed as [AWS lambda](https://aws.amazon.com/lambda/)
function with an [API Gateway](https://aws.amazon.com/api-gateway/) triggger.

The function accepts a query parameter `n` to define the number of words to
generate, but limits the sequence to `100` words, e.g.:

```
https://API_GW_ID.execute-api.REGION.amazonaws.com/Prod?n=100
```

### Requirements
Deployment requires the [AWS CLI](https://aws.amazon.com/cli/) as well as
[aws-vault](https://github.com/99designs/aws-vault) for secure credentials
access.  
Alternatively, it's also possible to reset the wrapped `aws` CLI command by
exporting `AWS_CLI=aws` as environment variable.

Local invocations require
[AWS SAM Local](https://github.com/awslabs/aws-sam-local).

The local watch task requires [entr](https://bitbucket.org/eradman/entr) to be
installed, which is available in the repositories of popular Linux distributions
and can be installed on MacOS via [Homebrew](https://brew.sh/):

```sh
brew install entr
```

### Environment variables
The following variables have to be set, e.g. by adding them to a `.env` file,
which gets included in the provided `Makefile`:

```sh
# Platform to use for local development and deployment (appengine or lambda):
PLATFORM=lambda
# The AWS profile to use for aws-vault:
AWS_PROFILE=default
# The S3 bucket where the lambda package can be uploaded:
DEPLOYMENT_BUCKET=example-bucket
# The S3 object prefix for the lambda package:
DEPLOYMENT_PREFIX=passphrase
# The CloudFormation stack name:
STACK_NAME=passphrase
# The name of an existing IAM role for AWS Lambda with
# AWSLambdaBasicExecutionRole attached:
LAMBDA_ROLE=arn:aws:iam::000000000000:role/aws-lambda-basic-execution-role
# The AWS service region, required to construct the API Gateway URL:
AWS_REGION=eu-west-1
```

### Build
To build the AWS Lambda function binary, run the following:

```sh
make lambda
```

### Deploy
To package and deploy the function, execute the following:

```sh
make deploy
```

After the deployment succeeds, the
[API Gateway](https://aws.amazon.com/api-gateway/) URL is printed.

This URL can also be retrieved later with the following command:

```sh
make url
```

To remove the AWS Lambda function and API Gateway configuration, execute the
following:

```sh
make destroy
```

### Local development
Using [AWS SAM Local](https://github.com/awslabs/aws-sam-local), the function
can also be invoked and served locally.

A sample API Gateway event can be generated the following way:

```sh
make event
```

To invoke the function locally, execute the following:

```sh
make invoke
```

To start the local API Gateway along with a watch process for source file
changes, run the following:

```sh
[BROWSER=chrome|safari|firefox] make watch
```

The watch task recompiles the lambda binary on changes.  
On MacOS, it also automatically reloads the active Chrome/Safari/Firefox tab.

## License
Released under the [MIT license](https://opensource.org/licenses/MIT).
<file_sep>package main

import (
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/blueimp/passphrase"
	"github.com/blueimp/passphrase/internal/parse"
)

const defaultNumber = 4
const maxNumber = 100

func indexHandler(response http.ResponseWriter, request *http.Request) {
	number := parse.NaturalNumber(
		request.FormValue("n"),
		defaultNumber,
		maxNumber,
	)
	response.Header().Set("cache-control", "private")
	response.Header().Set("content-type", "text/plain;charset=utf-8")
	response.Header().Set(
		"strict-transport-security",
		"max-age=31536000;includeSubDomains;preload",
	)
	response.Header().Set("x-content-type-options", "nosniff")
	_, err := passphrase.Write(response, number)
	if err != nil {
		log.Println(err)
	}
}

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
	http.HandleFunc("/", indexHandler)
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}
<file_sep>package passphrase_test

import (
	"bytes"
	"io/ioutil"
	"strings"
	"testing"

	"github.com/blueimp/passphrase"
	istrings "github.com/blueimp/passphrase/internal/strings"
)

func TestWrite(t *testing.T) {
	var buffer bytes.Buffer
	for i := 0; i > -10; i-- {
		passphrase.Write(&buffer, i)
		str := string(buffer.Bytes())
		buffer.Reset()
		if str != "" {
			t.Errorf("Expected empty passphrase, got: %s", str)
		}
	}
	for i := 1; i <= 10; i++ {
		passphrase.Write(&buffer, i)
		str := string(buffer.Bytes())
		buffer.Reset()
		words := strings.Split(str, " ")
		number := len(words)
		if number != i {
			t.Errorf("Incorrect number of words, got: %d, expected: %d.", number, i)
		}
		for _, word := range words {
			if !istrings.InSlice(word, passphrase.Words[:]) {
				t.Errorf("Passphrase word is not in the word list: %s", word)
			}
			if len(word) < passphrase.MinWordLength {
				t.Errorf(
					"Passphrase word is shorter than %d characters: %s",
					passphrase.MinWordLength,
					word,
				)
			}
		}
	}
}

func TestString(t *testing.T) {
	for i := 0; i > -10; i-- {
		str, _ := passphrase.String(i)
		if str != "" {
			t.Errorf("Expected empty passphrase, got: %s", str)
		}
	}
	for i := 1; i <= 10; i++ {
		str, _ := passphrase.String(i)
		words := strings.Split(str, " ")
		number := len(words)
		if number != i {
			t.Errorf("Incorrect number of words, got: %d, expected: %d.", number, i)
		}
		for _, word := range words {
			if !istrings.InSlice(word, passphrase.Words[:]) {
				t.Errorf("Passphrase word is not in the word list: %s", word)
			}
			if len(word) < passphrase.MinWordLength {
				t.Errorf(
					"Passphrase word is shorter than %d characters: %s",
					passphrase.MinWordLength,
					word,
				)
			}
		}
	}
}

func benchmarkWrite(i int, b *testing.B) {
	for n := 0; n < b.N; n++ {
		passphrase.Write(ioutil.Discard, i)
	}
}

func benchmarkString(i int, b *testing.B) {
	for n := 0; n < b.N; n++ {
		passphrase.String(i)
	}
}

func BenchmarkWrite4(b *testing.B)    { benchmarkWrite(4, b) }
func BenchmarkWrite16(b *testing.B)   { benchmarkWrite(16, b) }
func BenchmarkWrite64(b *testing.B)   { benchmarkWrite(64, b) }
func BenchmarkWrite256(b *testing.B)  { benchmarkWrite(256, b) }
func BenchmarkWrite1024(b *testing.B) { benchmarkWrite(1024, b) }

func BenchmarkString4(b *testing.B)    { benchmarkString(4, b) }
func BenchmarkString16(b *testing.B)   { benchmarkString(16, b) }
func BenchmarkString64(b *testing.B)   { benchmarkString(64, b) }
func BenchmarkString256(b *testing.B)  { benchmarkString(256, b) }
func BenchmarkString1024(b *testing.B) { benchmarkString(1024, b) }
<file_sep>package main

import (
	"strconv"
	"strings"
	"testing"

	"github.com/aws/aws-lambda-go/events"
	"github.com/blueimp/passphrase"
	istrings "github.com/blueimp/passphrase/internal/strings"
)

func param(number int) map[string]string {
	parameter := strconv.Itoa(number)
	return map[string]string{"n": parameter}
}

func passphraseRequest(args map[string]string) events.APIGatewayProxyResponse {
	response, err := Handler(&events.APIGatewayProxyRequest{
		QueryStringParameters: args,
	})
	if err != nil {
		panic(err)
	}
	return response
}

func TestHandler(t *testing.T) {
	response := passphraseRequest(map[string]string{})
	if response.StatusCode != 200 {
		t.Errorf("Expected status code 200, got: %d", response.StatusCode)
	}
	if response.Headers["cache-control"] != "private" {
		t.Errorf(
			"Expected cache-control \"private\", got: \"%s\"",
			response.Headers["cache-control"],
		)
	}
	if response.Headers["content-type"] != "text/plain;charset=utf-8" {
		t.Errorf(
			"Expected content-type \"text/plain;charset=utf-8\", got: \"%s\"",
			response.Headers["content-type"],
		)
	}
	hsts := "max-age=31536000;includeSubDomains;preload"
	if response.Headers["strict-transport-security"] != hsts {
		t.Errorf(
			"Expected strict-transport-security \"%s\", got: \"%s\"",
			hsts,
			response.Headers["strict-transport-security"],
		)
	}
	if response.Headers["x-content-type-options"] != "nosniff" {
		t.Errorf(
			"Expected x-content-type-options \"nosniff\", got: \"%s\"",
			response.Headers["x-content-type-options"],
		)
	}
	number := len(strings.Split(response.Body, " "))
	if number != defaultNumber {
		t.Errorf(
			"Incorrect default number of words, got: %d, expected: %d.",
			defaultNumber,
			number,
		)
	}
	for i := 0; i >= -maxNumber; i-- {
		response := passphraseRequest(param(i))
		if response.Body != "" {
			t.Errorf("Expected empty passphrase, got: %s", response.Body)
		}
	}
	for i := 1; i <= maxNumber; i++ {
		response := passphraseRequest(param(i))
		words := strings.Split(response.Body, " ")
		number := len(words)
		if number != i {
			t.Errorf("Incorrect number of words, got: %d, expected: %d.", number, i)
		}
		for _, word := range words {
			if !istrings.InSlice(word, passphrase.Words[:]) {
				t.Errorf("Passphrase word is not in the word list: %s", word)
			}
			if len(word) < passphrase.MinWordLength {
				t.Errorf(
					"Passphrase word is shorter than %d characters: %s",
					passphrase.MinWordLength,
					word,
				)
			}
		}
	}
	for i := maxNumber + 1; i <= maxNumber+11; i++ {
		response := passphraseRequest(param(i))
		words := strings.Split(response.Body, " ")
		number := len(words)
		if number != maxNumber {
			t.Errorf(
				"Incorrect number of words, got: %d, expected: %d.",
				number,
				maxNumber,
			)
		}
		for _, word := range words {
			if !istrings.InSlice(word, passphrase.Words[:]) {
				t.Errorf("Passphrase word is not in the word list: %s", word)
			}
			if len(word) < passphrase.MinWordLength {
				t.Errorf(
					"Passphrase word is shorter than %d characters: %s",
					passphrase.MinWordLength,
					word,
				)
			}
		}
	}
}
<file_sep>package main

import (
	"encoding/json"
	"log"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/blueimp/passphrase"
	"github.com/blueimp/passphrase/internal/parse"
)

const defaultNumber = 4
const maxNumber = 100

func logRequest(request *events.APIGatewayProxyRequest) {
	encodedRequest, err := json.Marshal(request)
	if err != nil {
		log.Println("Error:", err)
	} else {
		log.Println("Request:", string(encodedRequest))
	}
}

// Handler is the Lambda function handler.
func Handler(request *events.APIGatewayProxyRequest) (
	events.APIGatewayProxyResponse,
	error,
) {
	logRequest(request)
	number := parse.NaturalNumber(
		request.QueryStringParameters["n"],
		defaultNumber,
		maxNumber,
	)
	pass, err := passphrase.String(number)
	if err != nil {
		return events.APIGatewayProxyResponse{}, err
	}
	return events.APIGatewayProxyResponse{
		StatusCode: 200,
		Headers: map[string]string{
			"cache-control":             "private",
			"content-type":              "text/plain;charset=utf-8",
			"strict-transport-security": "max-age=31536000;includeSubDomains;preload",
			"x-content-type-options":    "nosniff",
		},
		Body: pass,
	}, nil
}

func main() {
	lambda.Start(Handler)
}
<file_sep>package main

import (
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"strconv"
	"strings"
	"testing"

	"github.com/blueimp/passphrase"
	istrings "github.com/blueimp/passphrase/internal/strings"
)

func param(number int) string {
	return "/?n=" + strconv.Itoa(number)
}

func passphraseRequest(url string) (response http.Response, result string) {
	request := httptest.NewRequest("GET", url, nil)
	recorder := httptest.NewRecorder()
	indexHandler(recorder, request)
	response = *recorder.Result()
	body, _ := ioutil.ReadAll(response.Body)
	return response, string(body)
}

func TestPassphrase(t *testing.T) {
	response, result := passphraseRequest("/")
	if response.StatusCode != 200 {
		t.Errorf("Expected status code 200, got: %d", response.StatusCode)
	}
	if response.Header.Get("cache-control") != "private" {
		t.Errorf(
			"Expected cache-control \"private\", got: \"%s\"",
			response.Header.Get("cache-control"),
		)
	}
	if response.Header.Get("content-type") != "text/plain;charset=utf-8" {
		t.Errorf(
			"Expected content-type \"text/plain;charset=utf-8\", got: \"%s\"",
			response.Header.Get("content-type"),
		)
	}
	hsts := "max-age=31536000;includeSubDomains;preload"
	if response.Header.Get("strict-transport-security") != hsts {
		t.Errorf(
			"Expected strict-transport-security \"%s\", got: \"%s\"",
			hsts,
			response.Header.Get("strict-transport-security"),
		)
	}
	if response.Header.Get("x-content-type-options") != "nosniff" {
		t.Errorf(
			"Expected x-content-type-options \"nosniff\", got: \"%s\"",
			response.Header.Get("x-content-type-options"),
		)
	}
	number := len(strings.Split(result, " "))
	if number != defaultNumber {
		t.Errorf(
			"Incorrect default number of words, got: %d, expected: %d.",
			defaultNumber,
			number,
		)
	}
	for i := 0; i >= -maxNumber; i-- {
		_, result := passphraseRequest(param(i))
		if result != "" {
			t.Errorf("Expected empty passphrase, got: %s", response.Body)
		}
	}
	for i := 1; i <= maxNumber; i++ {
		_, result := passphraseRequest(param(i))
		words := strings.Split(result, " ")
		number := len(words)
		if number != i {
			t.Errorf("Incorrect number of words, got: %d, expected: %d.", number, i)
		}
		for _, word := range words {
			if !istrings.InSlice(word, passphrase.Words[:]) {
				t.Errorf("Passphrase word is not in the word list: %s", word)
			}
			if len(word) < passphrase.MinWordLength {
				t.Errorf(
					"Passphrase word is shorter than %d characters: %s",
					passphrase.MinWordLength,
					word,
				)
			}
		}
	}
	for i := maxNumber + 1; i <= maxNumber+11; i++ {
		_, result := passphraseRequest(param(i))
		words := strings.Split(result, " ")
		number := len(words)
		if number != maxNumber {
			t.Errorf(
				"Incorrect number of words, got: %d, expected: %d.",
				number,
				maxNumber,
			)
		}
		for _, word := range words {
			if !istrings.InSlice(word, passphrase.Words[:]) {
				t.Errorf("Passphrase word is not in the word list: %s", word)
			}
			if len(word) < passphrase.MinWordLength {
				t.Errorf(
					"Passphrase word is shorter than %d characters: %s",
					passphrase.MinWordLength,
					word,
				)
			}
		}
	}
}
<file_sep>module github.com/blueimp/passphrase/passphrase

require github.com/blueimp/passphrase v1.0.0

replace github.com/blueimp/passphrase v1.0.0 => ../
<file_sep>../../passphrase.go<file_sep>module github.com/blueimp/passphrase/lambda

require (
	github.com/aws/aws-lambda-go v1.1.0
	github.com/blueimp/passphrase v1.0.0
)

replace github.com/blueimp/passphrase v1.0.0 => ../
<file_sep>package parse

import (
	"strconv"
)

// MaxInt is the naximum integer value on this platform.
const MaxInt = int(^uint(0) >> 1)

// NaturalNumber interprets the given string parameter as natural number.
// The given int args set default values and constraints:
//   arg[0] => default number (defaults to 0)
//   arg[1] => max mumber     (defaults to MaxInt)
//   arg[2] => min number     (default to 0)
// The default number is returned if the parameter is empty.
// The max number is returned if the parameter exceeds its size.
// Zero is returned if the interpreted string is not a natural number.
// The default, max and min numbers are assumed to be natural numbers.
func NaturalNumber(parameter string, args ...int) int {
	var number int
	var argsLength = len(args)
	if parameter == "" {
		if argsLength == 0 {
			return 0
		}
		return args[0]
	}
	number, err := strconv.Atoi(parameter)
	if err != nil {
		numError, ok := err.(*strconv.NumError)
		if ok && numError.Err == strconv.ErrRange && string(parameter[0]) != "-" {
			number = MaxInt
		} else {
			number = 0
		}
	}
	minNumber := 0
	if argsLength > 2 {
		minNumber = args[2]
	}
	if number < minNumber {
		return minNumber
	}
	maxNumber := MaxInt
	if argsLength > 1 {
		maxNumber = args[1]
	}
	if number > maxNumber {
		return maxNumber
	}
	return number
}
