├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config.go ├── config_test.go ├── docker ├── Dockerfile └── build.sh ├── go.mod ├── go.sum ├── lambda.go ├── lambda_test.go ├── reqrep.go ├── reqrep_test.go ├── setup.go ├── setup_test.go └── test_all.sh /.gitignore: -------------------------------------------------------------------------------- 1 | Caddyfile 2 | coverage.out 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | env: 3 | - GO111MODULE=on 4 | go: 5 | - "1.12" 6 | - tip 7 | 8 | before_script: 9 | - go get github.com/mattn/goveralls 10 | 11 | script: ./test_all.sh 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 James Cooper 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![Build Status](https://travis-ci.org/coopernurse/caddy-awslambda.svg?branch=master)](https://travis-ci.org/coopernurse/caddy-awslambda) [![Coverage Status](https://coveralls.io/repos/github/coopernurse/caddy-awslambda/badge.svg?branch=master)](https://coveralls.io/github/coopernurse/caddy-awslambda?branch=master) 3 | 4 | ## Overview 5 | 6 | `awslambda` is a Caddy plugin that gateways requests from Caddy to AWS Lambda functions. 7 | 8 | awslambda proxies requests to AWS Lambda functions using the 9 | [AWS Lambda Invoke](http://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html) operation. 10 | It provides an alternative to AWS API Gateway and provides a simple way to declaratively proxy 11 | requests to a set of Lambda functions without per-function configuration. 12 | 13 | Given that AWS Lambda has no notion of request and response headers, this plugin defines a standard 14 | JSON envelope format that encodes HTTP requests in a standard way, and expects the JSON returned from 15 | the Lambda functions to conform to the response JSON envelope format. 16 | 17 | *Contributors*: If you wish to contribute to this plugin, scroll to the bottom of this file 18 | to the "Building" section for notes on how to build caddy locally with this plugin enabled. 19 | 20 | ## Examples 21 | 22 | (1) Proxy all requests starting with /lambda/ to AWS Lambda, using env vars for AWS access keys and region: 23 | 24 | ``` 25 | awslambda /lambda/ 26 | ``` 27 | 28 | 29 | (2) Proxy requests starting with `/api/` to AWS Lambda using the `us-west-2` region, for functions staring with `api-` but not ending with `-internal`. A qualifier is used to target the `prod` aliases for each function. 30 | 31 | ``` 32 | awslambda /api/ { 33 | aws_region us-west-2 34 | qualifier prod 35 | include api-* 36 | exclude *-internal 37 | } 38 | ``` 39 | 40 | ## Syntax 41 | 42 | ``` 43 | awslambda { 44 | aws_access aws access key value 45 | aws_secret aws secret key value 46 | aws_region aws region name 47 | qualifier qualifier value 48 | include included function names... 49 | exclude excluded function names... 50 | name_prepend string to prepend to function name 51 | name_append string to append to function name 52 | single name of a single lambda function to invoke 53 | strip_path_prefix If true, path and function name are stripped from the path 54 | header_upstream header-name header-value 55 | } 56 | ``` 57 | 58 | * **aws_access** is the AWS Access Key to use when invoking Lambda functions. If omitted, the AWS_ACCESS_KEY_ID env var is used. 59 | * **aws_secret** is the AWS Secret Key to use when invoking Lambda functions. If omitted, the AWS_SECRET_ACCESS_KEY env var is used. 60 | * **aws_region** is the AWS Region name to use (e.g. 'us-west-1'). If omitted, the AWS_REGION env var is used. 61 | * **qualifier** is the qualifier value to use when invoking Lambda functions. Typically this is set to a function version or alias name. If omitted, no qualifier will be passed on the AWS Invoke invocation. 62 | * **include** is an optional space separated list of function names to include. Prefix and suffix globs ('*') are supported. If omitted, any function name not excluded may be invoked. 63 | * **exclude** is an optional space separated list of function names to exclude. Prefix and suffix globs are supported. 64 | * **name_prepend** is an optional string to prepend to the function name parsed from the URL before invoking the Lambda. 65 | * **name_append** is an optional string to append to the function name parsed from the URL before invoking the Lambda. 66 | * **single** is an optional function name. If set, function name is not parsed from the URI path. 67 | * **strip_path_prefix** If 'true', path and function name is stripped from the path sent as request metadata to the Lambda function. (default=false) 68 | * **header_upstream** Inject "header" key-value pairs into the upstream request json. Supports usage of [caddyfile placeholders](https://caddyserver.com/docs/placeholders). Can be used multiple times. Comes handy with frameworks like express. Example: 69 | ``` 70 | header_upstream X-API-Secret super1337secretapikey 71 | header_upstream X-Forwarded-For {remote} 72 | header_upstream X-Forwarded-Host {hostonly} 73 | header_upstream X-Forwarded-Proto {scheme} 74 | ``` 75 | 76 | Function names are parsed from the portion of request path following the path-prefix in the 77 | directive based on this convention: `[path-prefix]/[function-name]/[extra-path-info]` unless `single` attribute is set. 78 | 79 | For example, given a directive `awslambda /lambda/`, requests to `/lambda/hello-world` and `/lambda/hello-world/abc` 80 | would each invoke the AWS Lambda function named `hello-world`. 81 | 82 | The `include` and `exclude` globs are simple wildcards, not regular expressions. 83 | For example, `include foo*` would match `food` and `footer` but not `buffoon`, while 84 | `include *foo*` would match all three. 85 | 86 | `include` and `exclude` rules are run before `name_prepend` and `name_append` are applied and 87 | are run against the parsed function name, not the entire URL path. 88 | 89 | If you adopt a simple naming convention for your Lambda functions, these rules can be used to 90 | group access to a set of Lambdas under a single URL path prefix. 91 | 92 | `name_prepend` and `name_append` allow for shorter names in URLs and works well with tools such 93 | as Apex, which prepend the project name to all Lambda functions. For example, given an URL path 94 | of `/api/foo` with a `name_prepend` of `acme-api-`, the plugin will try to invoke the function 95 | named `acme-api-foo`. 96 | 97 | ## Writing Lambdas 98 | 99 | See [Lambda Functions](/docs/awslambda-functions) for details on the JSON request and reply 100 | envelope formats. Lambda functions that comply with this format may set arbitrary HTTP response 101 | status codes and headers. 102 | 103 | All examples in this document use the `node-4.3` AWS Lambda runtime. 104 | 105 | ### Examples 106 | 107 | Consider this `Caddyfile`: 108 | 109 | ``` 110 | awslambda /caddy/ { 111 | aws_access redacted 112 | aws_secret redacted 113 | aws_region us-west-2 114 | include caddy-* 115 | } 116 | ``` 117 | 118 | And this Lambda function, named `caddy-echo`: 119 | 120 | ```javascript 121 | 'use strict'; 122 | exports.handler = (event, context, callback) => { 123 | callback(null, event); 124 | }; 125 | ``` 126 | 127 | When we request it via `curl` we receive the following response, which reflects the 128 | request envelope Caddy sent to the lambda function: 129 | 130 | 131 | ``` 132 | $ curl -s -X POST -d 'hello' http://localhost:2015/caddy/caddy-echo | jq . 133 | { 134 | "type": "HTTPJSON-REQ", 135 | "meta": { 136 | "method": "POST", 137 | "path": "/caddy/caddy-echo", 138 | "query": "", 139 | "host": "localhost:2020", 140 | "proto": "HTTP/1.1", 141 | "headers": { 142 | "accept": [ 143 | "*/*" 144 | ], 145 | "content-length": [ 146 | "5" 147 | ], 148 | "content-type": [ 149 | "application/x-www-form-urlencoded" 150 | ], 151 | "user-agent": [ 152 | "curl/7.43.0" 153 | ] 154 | } 155 | }, 156 | "body": "hello" 157 | } 158 | ``` 159 | 160 | The request envelope format is described in detail below, but there are three top level fields: 161 | 162 | * `type` - always set to `HTTPJSON-REQ` 163 | * `meta` - JSON object containing HTTP request metadata such as the request method and headers 164 | * `body` - HTTP request body (if provided) 165 | 166 | Since our Lambda function didn't respond using the reply envelope, the raw reply was sent 167 | to the HTTP client and the `Content-Type` header was set to `application/json` automatically. 168 | 169 | Let's write a 2nd Lambda function that uses the request metadata and sends a reply using the 170 | envelope format. 171 | 172 | Lambda function name: `caddy-echo-html` 173 | 174 | ```javascript 175 | 'use strict'; 176 | exports.handler = (event, context, callback) => { 177 | var html, reply; 178 | html = 'Caddy Echo' + 179 | '

Request:

' + 180 | '
' + JSON.stringify(event, null, 2) +
181 |            '
'; 182 | reply = { 183 | 'type': 'HTTPJSON-REP', 184 | 'meta': { 185 | 'status': 200, 186 | 'headers': { 187 | 'Content-Type': [ 'text/html' ] 188 | } 189 | }, 190 | body: html 191 | }; 192 | callback(null, reply); 193 | }; 194 | ``` 195 | 196 | If we request `http://localhost:2015/caddy/caddy-echo-html` in a desktop web browser, the HTML 197 | formatted reply is displayed with a pretty-printed version of the request inside `
` tags.
198 | 
199 | In a final example we'll send a redirect using a 302 HTTP response status.
200 | 
201 | Lambda function name: `caddy-redirect`
202 | 
203 | ```javascript
204 | 'use strict';
205 | exports.handler = (event, context, callback) => {
206 |     var redirectUrl, reply;
207 |     redirectUrl = 'https://caddyserver.com/'
208 |     reply = {
209 |         'type': 'HTTPJSON-REP',
210 |         'meta': {
211 |             'status': 302,
212 |             'headers': {
213 |                 'Location': [ redirectUrl ]
214 |             }
215 |         },
216 |         body: 'Page has moved to: ' + redirectUrl
217 |     };
218 |     callback(null, reply);
219 | };
220 | ```
221 | 
222 | If we request `http://localhost:2015/caddy/caddy-redirect` we are redirected to the Caddy home page.
223 | 
224 | 
225 | ### Request envelope
226 | 
227 | The request payload sent from Caddy to the AWS Lambda function is a JSON object with the following fields:
228 | 
229 | * `type` - always the string literal `HTTPJSON-REQ`
230 | * `body` - the request body, or an empty string if no body is provided.
231 | * `meta` - a JSON object with the following fields:
232 |   * `method` - HTTP request method (e.g. `GET` or `POST`)
233 |   * `path` - URI path without query string
234 |   * `query` - Raw query string (without '?')
235 |   * `host` - Host client request was made to. May be of the form host:port
236 |   * `proto` - Protocol used by the client
237 |   * `headers` - a JSON object of HTTP headers sent by the client. Keys will be lower case. Values will be string arrays.
238 | 
239 | ### Reply envelope
240 | 
241 | AWS Lambda functions should return a JSON object with the following fields:
242 | 
243 | * `type` - always the string literal `HTTPJSON-REP`
244 | * `body` - response body
245 | * `meta` - optional response metadata. If provided, must be a JSON object with these fields:
246 |   * `status` - HTTP status code (e.g. 200)
247 |   * `headers` - a JSON object of HTTP headers. Values **must** be string arrays.
248 | 
249 | If `meta` is not provided, a 200 status will be returned along with a `Content-Type: application/json` header.
250 | 
251 | ### Gotchas
252 | 
253 | * Request and reply header values must be **string arrays**. For example:
254 | 
255 | ```javascript
256 | // Valid
257 | var reply = {
258 |     'type': 'HTTPJSON-REP',
259 |     'meta': {
260 |         'headers': {
261 |             'content-type': [ 'text/html' ]
262 |         }
263 |     }
264 | };
265 | 
266 | // Invalid
267 | var reply = {
268 |     'type': 'HTTPJSON-REP',
269 |     'meta': {
270 |         'headers': {
271 |             'content-type': 'text/html'
272 |         }
273 |     }
274 | };
275 | ```
276 | 
277 | * Reply must have a top level `'type': 'HTTPJSON-REP'` field. The rationale is that since
278 | all Lambda responses must be JSON we need a way to detect the presence of the envelope. Without
279 | this field, the raw reply JSON will be sent back to the client unmodified.
280 |     
281 | ## Building
282 | 
283 | If you want to modify the plugin and test your changes locally, follow these steps to
284 | recompile caddy with the plugin installed:
285 | 
286 | These instructions are mostly taken from Caddy's README.  Note that this process now uses
287 | the Go Module system to download dependencies.
288 | 
289 | 1. Set the transitional environment variable for Go modules: `export GO111MODULE=on`
290 | 2. Create a new folder anywhere and within create a Go file  (extension `.go`) with the contents below, adjusting to import the plugins you want to include:
291 | ```go
292 | package main
293 | 
294 | import (
295 | 	"github.com/caddyserver/caddy/caddy/caddymain"
296 | 	
297 | 	// Register this plugin - you may add other packages here, one per line
298 |     _ "github.com/coopernurse/caddy-awslambda"
299 | )
300 | 
301 | func main() {
302 | 	// optional: disable telemetry
303 | 	// caddymain.EnableTelemetry = false
304 | 	caddymain.Run()
305 | }
306 | ```
307 | 3. `go mod init caddy`
308 | 4. Run `go get github.com/caddyserver/caddy`
309 | 5. `go install` will then create your binary at `$GOPATH/bin`, or `go build` will put it in the current directory.
310 | 
311 | Verify that the plugin is installed:
312 | 
313 | ```bash
314 | ./caddy -plugins | grep aws
315 | 
316 | # you should see:
317 |   http.awslambda
318 | ```
319 | 
320 | These instructions are based on these notes:
321 | https://github.com/caddyserver/caddy/wiki/Plugging-in-Plugins-Yourself
322 | 


--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
  1 | package awslambda
  2 | 
  3 | import (
  4 | 	"encoding/json"
  5 | 	"net/http"
  6 | 	"strings"
  7 | 
  8 | 	"github.com/aws/aws-sdk-go/aws"
  9 | 	"github.com/aws/aws-sdk-go/aws/credentials"
 10 | 	"github.com/aws/aws-sdk-go/aws/session"
 11 | 	"github.com/aws/aws-sdk-go/service/lambda"
 12 | 	"github.com/caddyserver/caddy"
 13 | 	"github.com/caddyserver/caddy/caddyhttp/httpserver"
 14 | )
 15 | 
 16 | // Config specifies configuration for a single awslambda block
 17 | type Config struct {
 18 | 	// Path this config block maps to
 19 | 	Path string
 20 | 	// AWS Access Key. If omitted, AWS_ACCESS_KEY_ID env var is used.
 21 | 	AwsAccess string
 22 | 	// AWS Secret Key. If omitted, AWS_SECRET_ACCESS_KEY env var is used.
 23 | 	AwsSecret string
 24 | 	// AWS Region. If omitted, AWS_REGION env var is used.
 25 | 	AwsRegion string
 26 | 	// Optional qualifier to use on Invoke requests.
 27 | 	// This can be used to pin a configuration to a particular alias (e.g. 'prod' or 'dev')
 28 | 	Qualifier string
 29 | 	// Function name include rules. Prefix and suffix '*' globs are supported.
 30 | 	// Functions matching *any* of these rules will be proxied.
 31 | 	// If Include is empty, all function names will be allowed (unless explicitly excluded).
 32 | 	Include []string
 33 | 	// Function name exclude rules. Prefix and suffix '*" globs are supported.
 34 | 	// Functions matching *any* of these rules will be excluded, and not proxied.
 35 | 	// If Exclude is empty, no exclude rules will be applied.
 36 | 	Exclude []string
 37 | 	// Optional strings to prepend or append to the parsed function name from the URL
 38 | 	// before invoking the lambda. These are applied after the Include/Exclude rules are run
 39 | 	NamePrepend string
 40 | 	NameAppend  string
 41 | 
 42 | 	// If set, all requests to this path will invoke this function.
 43 | 	// The function name will not be parsed from the URL.
 44 | 	// This is useful for cases where you are multiplexing requests inside
 45 | 	// the lambda function itself.
 46 | 	//
 47 | 	// Note: If set, Include and Exclude will be ignored.
 48 | 	//
 49 | 	Single string
 50 | 
 51 | 	// If true, the Path field and function name will be removed from the
 52 | 	// RequestMeta.Path sent to the lambda function.  If Single is set,
 53 | 	// only the Path will be removed.
 54 | 	//
 55 | 	// For example, given: awslambda /api/ and a request to: /api/hello/foo
 56 | 	// the RequestMeta.Path would be /foo
 57 | 	StripPathPrefix bool
 58 | 
 59 | 	// headers to set in the upstream "headers" array - caddy placeholders work here
 60 | 	UpstreamHeaders map[string][]string
 61 | 
 62 | 	invoker Invoker
 63 | }
 64 | 
 65 | // AcceptsFunction tests whether the given function name is supported for
 66 | // this configuration by applying the Include and Exclude rules.
 67 | //
 68 | // Some additional lightweight sanity tests are also performed.  For example,
 69 | // empty strings and names containing periods (prohibited by AWS Lambda) will
 70 | // return false, but there is no attempt to ensure that all AWS Lambda naming
 71 | // rules are validated.  That is, some invalid names could be passed through.
 72 | //
 73 | func (c *Config) AcceptsFunction(name string) bool {
 74 | 	if name == "" || strings.Index(name, ".") >= 0 {
 75 | 		return false
 76 | 	}
 77 | 
 78 | 	if len(c.Include) > 0 {
 79 | 		found := false
 80 | 		for _, k := range c.Include {
 81 | 			if matchGlob(name, k) {
 82 | 				found = true
 83 | 				break
 84 | 			}
 85 | 		}
 86 | 		if !found {
 87 | 			return false
 88 | 		}
 89 | 	}
 90 | 
 91 | 	for _, k := range c.Exclude {
 92 | 		if matchGlob(name, k) {
 93 | 			return false
 94 | 		}
 95 | 	}
 96 | 
 97 | 	return true
 98 | }
 99 | 
100 | // ToAwsConfig returns a new *aws.Config instance using the AWS related values on Config.
101 | // If AwsRegion is empty, the AWS_REGION env var is used.
102 | // If AwsAccess is empty, the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY env vars are used.
103 | func (c *Config) ToAwsConfig() *aws.Config {
104 | 	awsConf := aws.NewConfig()
105 | 	if c.AwsRegion != "" {
106 | 		awsConf.WithRegion(c.AwsRegion)
107 | 	}
108 | 	if c.AwsAccess != "" {
109 | 		awsConf.WithCredentials(credentials.NewStaticCredentials(
110 | 			c.AwsAccess, c.AwsSecret, "",
111 | 		))
112 | 	}
113 | 	return awsConf
114 | }
115 | 
116 | // ParseFunction returns the fragment of path immediately after the
117 | // config path, excluding string and named anchors.
118 | //
119 | // For example, given a path of '/lambda/my-func/pathparam?a=/foo',
120 | // ParseFunction returns 'my-func'
121 | func (c *Config) ParseFunction(path string) string {
122 | 	path = strings.TrimPrefix(path, c.Path)
123 | 	pos := strings.Index(path, "?")
124 | 	if pos > -1 {
125 | 		path = path[:pos]
126 | 	}
127 | 	pos = strings.Index(path, "#")
128 | 	if pos > -1 {
129 | 		path = path[:pos]
130 | 	}
131 | 
132 | 	return strings.Split(path, "/")[0]
133 | }
134 | 
135 | // MaybeToInvokeInput returns a new InvokeInput instanced based on the  HTTP request.
136 | // If the function name parsed from the r.URL.Path doesn't comply with the Config's
137 | // include/exclude rules, then nil, nil is returned.
138 | // Otherwise an InvokeInput is returned with all fields populated based on the
139 | // http.Request, and the NameAppend and NamePrepend rules applied (if any).
140 | func (c *Config) MaybeToInvokeInput(r *http.Request) (*lambda.InvokeInput, error) {
141 | 	// Verify that parsed function name is allowed based on Config rules
142 | 	funcName := c.Single
143 | 	if funcName == "" {
144 | 		funcName = c.ParseFunction(r.URL.Path)
145 | 		if !c.AcceptsFunction(funcName) {
146 | 			return nil, nil
147 | 		}
148 | 	}
149 | 
150 | 	req, err := NewRequest(r)
151 | 	if err != nil {
152 | 		return nil, err
153 | 	}
154 | 	if c.StripPathPrefix && req.Meta != nil {
155 | 		req.Meta.Path = c.stripPathPrefix(req.Meta.Path, funcName)
156 | 	}
157 | 
158 | 	if len(c.UpstreamHeaders) > 0 {
159 | 		// inject upstream headers defined with the header_upstream directive into req.Meta.Headers
160 | 		// uses caddy's integrated replacer for placeholder replacement (https://caddyserver.com/docs/placeholders)
161 | 		replInt := r.Context().Value(httpserver.ReplacerCtxKey)
162 | 		replacer := replInt.(httpserver.Replacer)
163 | 		for k, v := range c.UpstreamHeaders {
164 | 			newValue := make([]string, len(v))
165 | 			for i, v := range v {
166 | 				newValue[i] = replacer.Replace(v)
167 | 			}
168 | 			req.Meta.Headers[strings.ToLower(k)] = newValue
169 | 		}
170 | 	}
171 | 
172 | 	payload, err := json.Marshal(req)
173 | 	if err != nil {
174 | 		return nil, err
175 | 	}
176 | 
177 | 	if c.NamePrepend != "" {
178 | 		funcName = c.NamePrepend + funcName
179 | 	}
180 | 	if c.NameAppend != "" {
181 | 		funcName = funcName + c.NameAppend
182 | 	}
183 | 
184 | 	input := &lambda.InvokeInput{
185 | 		FunctionName: &funcName,
186 | 		Payload:      payload,
187 | 	}
188 | 	if c.Qualifier != "" {
189 | 		input.Qualifier = &c.Qualifier
190 | 	}
191 | 	return input, nil
192 | }
193 | 
194 | func (c *Config) initLambdaClient() error {
195 | 	sess, err := session.NewSession(c.ToAwsConfig())
196 | 	if err != nil {
197 | 		return err
198 | 	}
199 | 	c.invoker = lambda.New(sess)
200 | 	return nil
201 | }
202 | 
203 | func (c *Config) stripPathPrefix(reqPath, funcName string) string {
204 | 	prefix := c.Path
205 | 	if c.Single == "" {
206 | 		if !strings.HasSuffix(prefix, "/") {
207 | 			prefix += "/"
208 | 		}
209 | 		prefix += funcName
210 | 	}
211 | 
212 | 	if strings.HasPrefix(reqPath, prefix) {
213 | 		reqPath = reqPath[len(prefix):]
214 | 		if !strings.HasPrefix(reqPath, "/") {
215 | 			reqPath = "/" + reqPath
216 | 		}
217 | 	}
218 | 	return reqPath
219 | }
220 | 
221 | // ParseConfigs parses a Caddy awslambda config block into a Config struct.
222 | func ParseConfigs(c *caddy.Controller) ([]*Config, error) {
223 | 	var configs []*Config
224 | 	var conf *Config
225 | 	last := ""
226 | 
227 | 	for c.Next() {
228 | 		val := c.Val()
229 | 		lastTmp := last
230 | 		last = ""
231 | 		switch lastTmp {
232 | 		case "awslambda":
233 | 			conf = &Config{
234 | 				Path:    val,
235 | 				Include: []string{},
236 | 				Exclude: []string{},
237 | 			}
238 | 			configs = append(configs, conf)
239 | 		case "aws_access":
240 | 			conf.AwsAccess = val
241 | 		case "aws_secret":
242 | 			conf.AwsSecret = val
243 | 		case "aws_region":
244 | 			conf.AwsRegion = val
245 | 		case "qualifier":
246 | 			conf.Qualifier = val
247 | 		case "name_prepend":
248 | 			conf.NamePrepend = val
249 | 		case "name_append":
250 | 			conf.NameAppend = val
251 | 		case "single":
252 | 			conf.Single = val
253 | 		case "strip_path_prefix":
254 | 			conf.StripPathPrefix = toBool(val)
255 | 		case "include":
256 | 			conf.Include = append(conf.Include, val)
257 | 			conf.Include = append(conf.Include, c.RemainingArgs()...)
258 | 		case "exclude":
259 | 			conf.Exclude = append(conf.Exclude, val)
260 | 			conf.Exclude = append(conf.Exclude, c.RemainingArgs()...)
261 | 		case "header_upstream":
262 | 			if conf.UpstreamHeaders == nil {
263 | 				conf.UpstreamHeaders = make(map[string][]string)
264 | 			}
265 | 			value := strings.Join(c.RemainingArgs(), " ")
266 | 			conf.UpstreamHeaders[val] = []string{value}
267 | 		default:
268 | 			last = val
269 | 		}
270 | 	}
271 | 
272 | 	for _, conf := range configs {
273 | 		err := conf.initLambdaClient()
274 | 		if err != nil {
275 | 			return nil, err
276 | 		}
277 | 	}
278 | 
279 | 	return configs, nil
280 | }
281 | 
282 | // toBool treats any of the following as true: 1, yes, y, on, true
283 | // otherwise returns false
284 | func toBool(s string) bool {
285 | 	s = strings.ToLower(s)
286 | 	if s == "1" || s == "y" || s == "yes" || s == "true" || s == "on" {
287 | 		return true
288 | 	}
289 | 	return false
290 | }
291 | 
292 | // matchGlob returns true if string s matches the rule.
293 | // Simple prefix and suffix wildcards are supported with '*'.
294 | // For example, string 'hello' matches rules: 'hello', 'hel*', '*llo', '*ell*'
295 | func matchGlob(s, rule string) bool {
296 | 	if s == rule {
297 | 		return true
298 | 	}
299 | 
300 | 	if strings.HasPrefix(rule, "*") {
301 | 		if strings.HasSuffix(rule, "*") {
302 | 			rule = rule[1 : len(rule)-1]
303 | 			return strings.Index(s, rule) >= 0
304 | 		}
305 | 		return strings.HasSuffix(s, rule[1:])
306 | 	} else if strings.HasSuffix(rule, "*") {
307 | 		return strings.HasPrefix(s, rule[0:len(rule)-1])
308 | 	} else {
309 | 		return false
310 | 	}
311 | }
312 | 


--------------------------------------------------------------------------------
/config_test.go:
--------------------------------------------------------------------------------
  1 | package awslambda
  2 | 
  3 | import (
  4 | 	"bytes"
  5 | 	"context"
  6 | 	"io"
  7 | 	"net/http"
  8 | 	"reflect"
  9 | 	"testing"
 10 | 
 11 | 	"github.com/aws/aws-sdk-go/aws"
 12 | 	"github.com/aws/aws-sdk-go/aws/credentials"
 13 | 	"github.com/aws/aws-sdk-go/service/lambda"
 14 | 	"github.com/caddyserver/caddy"
 15 | 	"github.com/caddyserver/caddy/caddyhttp/httpserver"
 16 | )
 17 | 
 18 | func TestAcceptsFunction(t *testing.T) {
 19 | 	c := Config{
 20 | 		Include: []string{
 21 | 			"test*", "hello-world",
 22 | 		},
 23 | 		Exclude: []string{
 24 | 			"*cats*", "fish",
 25 | 		},
 26 | 	}
 27 | 
 28 | 	for i, test := range []struct {
 29 | 		name     string
 30 | 		expected bool
 31 | 	}{
 32 | 		{"", false},
 33 | 		{"test", true},
 34 | 		{"testSomething", true},
 35 | 		{"test-cats", false},
 36 | 		{"test-fishy-stuff", true},
 37 | 		{"fish", false},
 38 | 		{"test_dog", true},
 39 | 		{"hello-world", true},
 40 | 		{"hello-world-2", false},
 41 | 	} {
 42 | 		actual := c.AcceptsFunction(test.name)
 43 | 		if actual != test.expected {
 44 | 			t.Errorf("\nTest %d - name: %s\nExpected: %v\n  Actual: %v",
 45 | 				i, test.name, test.expected, actual)
 46 | 		}
 47 | 	}
 48 | }
 49 | 
 50 | func TestMatchGlob(t *testing.T) {
 51 | 	for i, test := range []struct {
 52 | 		candidate string
 53 | 		rule      string
 54 | 		expected  bool
 55 | 	}{
 56 | 		{"hello", "hello", true},
 57 | 		{"hello", "ello", false},
 58 | 		{"hello", "*ello", true},
 59 | 		{"hello", "hel*", true},
 60 | 	} {
 61 | 		actual := matchGlob(test.candidate, test.rule)
 62 | 		if actual != test.expected {
 63 | 			t.Errorf("\nTest %d - candidate: %s    rule: %s\nExpected: %v\n  Actual: %v",
 64 | 				i, test.candidate, test.rule, test.expected, actual)
 65 | 		}
 66 | 	}
 67 | }
 68 | 
 69 | func TestToAwsConfigStaticCreds(t *testing.T) {
 70 | 	c := &Config{
 71 | 		AwsAccess: "a-key",
 72 | 		AwsSecret: "secret",
 73 | 	}
 74 | 	expected := credentials.NewStaticCredentials("a-key", "secret", "")
 75 | 	actual := c.ToAwsConfig()
 76 | 	if !reflect.DeepEqual(expected, actual.Credentials) {
 77 | 		t.Errorf("\nExpected: %v\n  Actual: %v", expected, actual.Credentials)
 78 | 	}
 79 | }
 80 | 
 81 | func TestToAwsConfigStaticRegion(t *testing.T) {
 82 | 	c := &Config{
 83 | 		AwsRegion: "us-west-2",
 84 | 	}
 85 | 	expected := aws.NewConfig()
 86 | 	actual := c.ToAwsConfig()
 87 | 	if c.AwsRegion != *actual.Region {
 88 | 		t.Errorf("\nExpected: %v\n  Actual: %v", c.AwsRegion, *actual.Region)
 89 | 	}
 90 | 	if !reflect.DeepEqual(expected.Credentials, actual.Credentials) {
 91 | 		t.Errorf("\nExpected: %v\n  Actual: %v", expected.Credentials, actual.Credentials)
 92 | 	}
 93 | }
 94 | 
 95 | func TestToAwsConfigDefaults(t *testing.T) {
 96 | 	c := &Config{}
 97 | 	expected := aws.NewConfig()
 98 | 	actual := c.ToAwsConfig()
 99 | 	if !reflect.DeepEqual(expected, actual) {
100 | 		t.Errorf("\nExpected: %v\n  Actual: %v", expected, actual.Credentials)
101 | 	}
102 | }
103 | 
104 | func TestParseConfigs(t *testing.T) {
105 | 	for i, test := range []struct {
106 | 		input    string
107 | 		expected []*Config
108 | 	}{
109 | 		{"awslambda /foo/", []*Config{&Config{
110 | 			Path:    "/foo/",
111 | 			Include: []string{},
112 | 			Exclude: []string{},
113 | 		}}},
114 | 		{`awslambda /blah/ {
115 |     aws_access my-access
116 |     aws_secret my-secret
117 |     aws_region us-west-1
118 |     qualifier  prod
119 |     include    foo*  some-other
120 |     exclude    *blah*
121 |     name_prepend   apex-foo_
122 |     name_append    _suffix_here
123 |     single             my-single-func
124 |     strip_path_prefix  on
125 |     header_upstream    x-real-ip {remote}
126 | }`,
127 | 			[]*Config{
128 | 				&Config{
129 | 					Path:            "/blah/",
130 | 					AwsAccess:       "my-access",
131 | 					AwsSecret:       "my-secret",
132 | 					AwsRegion:       "us-west-1",
133 | 					Qualifier:       "prod",
134 | 					Include:         []string{"foo*", "some-other"},
135 | 					Exclude:         []string{"*blah*"},
136 | 					NamePrepend:     "apex-foo_",
137 | 					NameAppend:      "_suffix_here",
138 | 					Single:          "my-single-func",
139 | 					StripPathPrefix: true,
140 | 					UpstreamHeaders: map[string][]string{
141 | 						"x-real-ip": []string{"{remote}"},
142 | 					},
143 | 				},
144 | 			},
145 | 		},
146 | 		{`awslambda /first/ {
147 |     aws_region us-west-2
148 |     qualifier  dev
149 |     exclude    foo
150 | }
151 | awslambda /second/path/ {
152 |     aws_region us-east-1
153 |     include one two three*
154 | }`,
155 | 			[]*Config{
156 | 				&Config{
157 | 					Path:      "/first/",
158 | 					AwsRegion: "us-west-2",
159 | 					Qualifier: "dev",
160 | 					Include:   []string{},
161 | 					Exclude:   []string{"foo"},
162 | 				},
163 | 				&Config{
164 | 					Path:      "/second/path/",
165 | 					AwsRegion: "us-east-1",
166 | 					Include:   []string{"one", "two", "three*"},
167 | 					Exclude:   []string{},
168 | 				},
169 | 			},
170 | 		},
171 | 	} {
172 | 		controller := caddy.NewTestController("http", test.input)
173 | 		actual, err := ParseConfigs(controller)
174 | 		if err != nil {
175 | 			t.Errorf("ParseConfigs return err: %v", err)
176 | 		}
177 | 		for i := range actual {
178 | 			actual[i].invoker = nil
179 | 		}
180 | 		eqOrErr(test.expected, actual, i, t)
181 | 	}
182 | }
183 | 
184 | func TestParseFunction(t *testing.T) {
185 | 	for i, test := range []struct {
186 | 		path     string
187 | 		expected string
188 | 	}{
189 | 		{"/foo/bar", "bar"},
190 | 		{"/foo/bar/baz", "bar"},
191 | 		{"/foo", ""},
192 | 		{"/foo/", ""},
193 | 		{"/foo/bar?a=b", "bar"},
194 | 		{"/foo/bar#anchor-here", "bar"},
195 | 		{"/foo/bar?a=/blah#anchor-here", "bar"},
196 | 		{"/foo/bar/baz?a=/blah#anchor-here", "bar"},
197 | 	} {
198 | 		c := Config{Path: "/foo/"}
199 | 		actual := c.ParseFunction(test.path)
200 | 		if actual != test.expected {
201 | 			t.Errorf("\nTest %d\nExpected: %s\n  Actual: %s", i, test.expected, actual)
202 | 		}
203 | 	}
204 | }
205 | 
206 | func TestMaybeToInvokeInput(t *testing.T) {
207 | 	r1 := mustNewRequest("PUT", "/api/user", bytes.NewBufferString("hello world"))
208 | 	r2 := mustNewRequest("PUT", "/api/user", bytes.NewBufferString("hello world"))
209 | 
210 | 	// expect a non-nil input
211 | 	c := Config{
212 | 		Path:        "/api/",
213 | 		NamePrepend: "before-",
214 | 		NameAppend:  "-after",
215 | 		Qualifier:   "prod",
216 | 		UpstreamHeaders: map[string][]string{
217 | 			"x-real-proto":  []string{"{proto}"},
218 | 			"x-real-method": []string{"{method}"},
219 | 		},
220 | 	}
221 | 	input, err := c.MaybeToInvokeInput(r1)
222 | 	if err != nil {
223 | 		t.Fatalf("MaybeToInvokeInput returned err: %v", err)
224 | 	}
225 | 	if input == nil {
226 | 		t.Fatalf("MaybeToInvokeInput returned nil input")
227 | 	}
228 | 	funcName := "before-user-after"
229 | 	req, err := NewRequest(r2)
230 | 	if err != nil {
231 | 		t.Fatalf("NewRequest returned err: %v", err)
232 | 	}
233 | 	req.Meta.Headers["x-real-proto"] = []string{"HTTP/1.1"}
234 | 	req.Meta.Headers["x-real-method"] = []string{"PUT"}
235 | 	expected := lambda.InvokeInput{
236 | 		FunctionName: &funcName,
237 | 		Qualifier:    &c.Qualifier,
238 | 		Payload:      marshalJSON(req),
239 | 	}
240 | 	eqOrErr(expected, *input, 0, t)
241 | 
242 | 	// expect a nil input since include rule doesn't match
243 | 	c.Include = []string{"*blah*"}
244 | 	input, err = c.MaybeToInvokeInput(r1)
245 | 	if err != nil || input != nil {
246 | 		t.Fatalf("MaybeToInvokeInput returned err or non-nil input: input=%v  err=%v", input, err)
247 | 	}
248 | }
249 | 
250 | func TestSingleFunction(t *testing.T) {
251 | 	r1 := mustNewRequest("PUT", "/api/user", bytes.NewBufferString("hello world"))
252 | 
253 | 	// expect a non-nil input
254 | 	c := Config{
255 | 		Single: "single-func",
256 | 
257 | 		// ignored:
258 | 		Exclude: []string{"single"},
259 | 		Include: []string{"foo"},
260 | 	}
261 | 	input, err := c.MaybeToInvokeInput(r1)
262 | 	if err != nil {
263 | 		t.Fatalf("MaybeToInvokeInput returned err: %v", err)
264 | 	}
265 | 	if c.Single != *input.FunctionName {
266 | 		t.Errorf("FunctionName wrong: %s != %s", c.Single, *input.FunctionName)
267 | 	}
268 | }
269 | 
270 | func TestStripPathPrefix(t *testing.T) {
271 | 	c := Config{
272 | 		Path:   "/api/",
273 | 		Single: "single-func",
274 | 	}
275 | 
276 | 	for i, test := range []struct {
277 | 		reqPath  string
278 | 		funcName string
279 | 		isSingle bool
280 | 		expected string
281 | 	}{
282 | 		{"/api/foo", "foo", false, "/"},
283 | 		{"/api/blahstuff/other/things", "no-match", false, "/api/blahstuff/other/things"},
284 | 		{"/api/foo", "foo", true, "/foo"},
285 | 		{"/other/foo", "foo", false, "/other/foo"},
286 | 		{"/other/foo", "foo", true, "/other/foo"},
287 | 	} {
288 | 		c.Single = ""
289 | 		if test.isSingle {
290 | 			c.Single = "single-func"
291 | 		}
292 | 
293 | 		actual := c.stripPathPrefix(test.reqPath, test.funcName)
294 | 		if actual != test.expected {
295 | 			t.Errorf("Test %d failed:\nExpected: %s\n  Actual: %s", i, test.expected, actual)
296 | 		}
297 | 	}
298 | }
299 | 
300 | func mustNewRequest(method, path string, body io.Reader) *http.Request {
301 | 	req, err := http.NewRequest(method, path, body)
302 | 	if err != nil {
303 | 		panic(err)
304 | 	}
305 | 	replacer := httpserver.NewReplacer(req, nil, "")
306 | 	newContext := context.WithValue(req.Context(), httpserver.ReplacerCtxKey, replacer)
307 | 	req = req.WithContext(newContext)
308 | 	return req
309 | }
310 | 


--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
 1 | FROM debian:jessie
 2 | 
 3 | RUN apt-get update && \
 4 |   apt-get upgrade -y && \
 5 |   apt-get install -y ca-certificates && \
 6 |   apt-get clean -y && \
 7 |   apt-get autoclean -y && \
 8 |   apt-get autoremove -y && \
 9 |   rm -rf /usr/share/locale/* && \
10 |   rm -rf /var/cache/debconf/*-old && \
11 |   rm -rf /var/lib/apt/lists/* && \
12 |   rm -rf /usr/share/doc/*
13 |   
14 | ADD caddy /usr/bin/caddy
15 | 
16 | CMD ["/usr/bin/caddy", "-conf=/etc/Caddyfile"]
17 | 
18 | 
19 | 


--------------------------------------------------------------------------------
/docker/build.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/bash
 2 | 
 3 | set -e
 4 | 
 5 | cd ..
 6 | GOOS=linux GOARCH=amd64 caddydev -o docker/caddy awslambda
 7 | cd docker
 8 | sudo docker build -t coopernurse/caddy-awslambda .
 9 | rm -f caddy
10 | 


--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/coopernurse/caddy-awslambda
2 | 
3 | go 1.12
4 | 
5 | require (
6 | 	github.com/aws/aws-sdk-go v1.20.14
7 | 	github.com/caddyserver/caddy v1.0.1
8 | )
9 | 


--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 2 | github.com/aws/aws-sdk-go v1.20.14 h1:ivPlTrZmHf4f4TvAG79yOyo2fRH0JW4dz+fsV8IQnbU=
 3 | github.com/aws/aws-sdk-go v1.20.14/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 4 | github.com/bifurcation/mint v0.0.0-20180715133206-93c51c6ce115 h1:fUjoj2bT6dG8LoEe+uNsKk8J+sLkDbQkJnB6Z1F02Bc=
 5 | github.com/bifurcation/mint v0.0.0-20180715133206-93c51c6ce115/go.mod h1:zVt7zX3K/aDCk9Tj+VM7YymsX66ERvzCJzw8rFCX2JU=
 6 | github.com/caddyserver/caddy v1.0.1 h1:oor6ep+8NoJOabpFXhvjqjfeldtw1XSzfISVrbfqTKo=
 7 | github.com/caddyserver/caddy v1.0.1/go.mod h1:G+ouvOY32gENkJC+jhgl62TyhvqEsFaDiZ4uw0RzP1E=
 8 | github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY=
 9 | github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
10 | github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9 h1:a1zrFsLFac2xoM6zG1u72DWJwZG3ayttYLfmLbxVETk=
11 | github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
13 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
14 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
15 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
16 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
17 | github.com/go-acme/lego v2.5.0+incompatible h1:5fNN9yRQfv8ymH3DSsxla+4aYeQt2IgfZqHKVnK8f0s=
18 | github.com/go-acme/lego v2.5.0+incompatible/go.mod h1:yzMNe9CasVUhkquNvti5nAtPmG94USbYxYrZfTkIn0M=
19 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
20 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
21 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
22 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
23 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
24 | github.com/hashicorp/go-syslog v1.0.0 h1:KaodqZuhUoZereWVIYmpUgZysurB1kBLX2j0MwMrUAE=
25 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
26 | github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47 h1:UnszMmmmm5vLwWzDjTFVIkfhvWF1NdrmChl8L2NUDCw=
27 | github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
28 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
29 | github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8=
30 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
31 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
32 | github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE=
33 | github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
34 | github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
35 | github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f h1:sSeNEkJrs+0F9TUau0CgWTTNEwF23HST3Eq0A+QIx+A=
36 | github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f/go.mod h1:JpH9J1c9oX6otFSgdUHwUBUizmKlrMjxWnIAjff4m04=
37 | github.com/lucas-clemente/quic-clients v0.1.0/go.mod h1:y5xVIEoObKqULIKivu+gD/LU90pL73bTdtQjPBvtCBk=
38 | github.com/lucas-clemente/quic-go v0.10.2 h1:iQtTSZVbd44k94Lu0U16lLBIG3lrnjDvQongjPd4B/s=
39 | github.com/lucas-clemente/quic-go v0.10.2/go.mod h1:hvaRS9IHjFLMq76puFJeWNfmn+H70QZ/CXoxqw9bzao=
40 | github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced h1:zqEC1GJZFbGZA0tRyNZqRjep92K5fujFtFsu5ZW7Aug=
41 | github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced/go.mod h1:NCcRLrOTZbzhZvixZLlERbJtDtYsmMw8Jc4vS8Z0g58=
42 | github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
43 | github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2 h1:xKE9kZ5C8gelJC3+BNM6LJs1x21rivK7yxfTZMAuY2s=
44 | github.com/mholt/certmagic v0.6.2-0.20190624175158-6a42ef9fe8c2/go.mod h1:g4cOPxcjV0oFq3qwpjSA30LReKD8AoIfwAY9VvG35NY=
45 | github.com/miekg/dns v1.1.3 h1:1g0r1IvskvgL8rR+AcHzUA+oFmGcQlaIm4IqakufeMM=
46 | github.com/miekg/dns v1.1.3/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
47 | github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
48 | github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
49 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
50 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
51 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
52 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
53 | github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4 h1:S9YlS71UNJIyS61OqGAmLXv3w5zclSidN+qwr80XxKs=
54 | github.com/russross/blackfriday v0.0.0-20170610170232-067529f716f4/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
55 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
56 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
57 | golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
58 | golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
59 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
60 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
61 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
62 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
63 | golang.org/x/net v0.0.0-20190328230028-74de082e2cca h1:hyA6yiAgbUwuWqtscNvWAI7U1CtlaD1KilQ6iudt1aI=
64 | golang.org/x/net v0.0.0-20190328230028-74de082e2cca/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
65 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
66 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
67 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
68 | golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
69 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
70 | golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e h1:ZytStCyV048ZqDsWHiYDdoI2Vd4msMcrDECFxS+tL9c=
71 | golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
72 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
73 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
74 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
75 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
76 | gopkg.in/mcuadros/go-syslog.v2 v2.2.1/go.mod h1:l5LPIyOOyIdQquNg+oU6Z3524YwrcqEm0aKH+5zpt2U=
77 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
78 | gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
79 | gopkg.in/square/go-jose.v2 v2.2.2 h1:orlkJ3myw8CN1nVQHBFfloD+L3egixIa4FvUP6RosSA=
80 | gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
81 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
82 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
83 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
84 | 


--------------------------------------------------------------------------------
/lambda.go:
--------------------------------------------------------------------------------
  1 | package awslambda
  2 | 
  3 | import (
  4 | 	"encoding/base64"
  5 | 	"net/http"
  6 | 
  7 | 	"github.com/aws/aws-sdk-go/service/lambda"
  8 | 	"github.com/caddyserver/caddy/caddyhttp/httpserver"
  9 | )
 10 | 
 11 | // Invoker calls a single AWS Lambda function - can be mocked for tests
 12 | type Invoker interface {
 13 | 	Invoke(input *lambda.InvokeInput) (*lambda.InvokeOutput, error)
 14 | }
 15 | 
 16 | // Handler represents a middleware instance that can gateway requests to AWS Lambda
 17 | type Handler struct {
 18 | 	Next    httpserver.Handler
 19 | 	Configs []*Config
 20 | }
 21 | 
 22 | // ServeHTTP satisfies the httpserver.Handler interface by proxying
 23 | // the request to AWS Lambda via the Invoke function
 24 | //
 25 | // See: http://docs.aws.amazon.com/lambda/latest/dg/API_Invoke.html
 26 | //
 27 | func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
 28 | 	conf, invokeInput, err := h.match(r)
 29 | 	if err != nil {
 30 | 		return 0, err
 31 | 	}
 32 | 	if conf == nil || conf.Path == "" || invokeInput == nil {
 33 | 		return h.Next.ServeHTTP(w, r)
 34 | 	}
 35 | 
 36 | 	// Invoke function at AWS
 37 | 	invokeOut, err := conf.invoker.Invoke(invokeInput)
 38 | 	if err != nil {
 39 | 		return 0, err
 40 | 	}
 41 | 
 42 | 	// Unpack the reply JSON
 43 | 	reply, err := ParseReply(invokeOut.Payload)
 44 | 	if err != nil {
 45 | 		return 0, err
 46 | 	}
 47 | 
 48 | 	// Write the response HTTP headers
 49 | 	for k, vals := range reply.Meta.Headers {
 50 | 		for _, v := range vals {
 51 | 			w.Header().Add(k, v)
 52 | 		}
 53 | 	}
 54 | 
 55 | 	// Default the Content-Type to application/json if not provided on reply
 56 | 	if w.Header().Get("content-type") == "" {
 57 | 		w.Header().Set("content-type", "application/json")
 58 | 	}
 59 | 	if reply.Meta.Status <= 0 {
 60 | 		reply.Meta.Status = http.StatusOK
 61 | 	}
 62 | 
 63 | 	w.WriteHeader(reply.Meta.Status)
 64 | 
 65 | 	// Optionally decode the response body
 66 | 	var bodyBytes []byte
 67 | 	if reply.BodyEncoding == "base64" && reply.Body != "" {
 68 | 		bodyBytes, err = base64.StdEncoding.DecodeString(reply.Body)
 69 | 		if err != nil {
 70 | 			return 0, err
 71 | 		}
 72 | 	} else {
 73 | 		bodyBytes = []byte(reply.Body)
 74 | 	}
 75 | 
 76 | 	// Write the response body
 77 | 	_, err = w.Write(bodyBytes)
 78 | 	if err != nil || reply.Meta.Status >= 400 {
 79 | 		return 0, err
 80 | 	}
 81 | 
 82 | 	return reply.Meta.Status, nil
 83 | }
 84 | 
 85 | // match finds the best match for a proxy config based on r.
 86 | func (h Handler) match(r *http.Request) (*Config, *lambda.InvokeInput, error) {
 87 | 	var c *Config
 88 | 	var invokeInput *lambda.InvokeInput
 89 | 	var err error
 90 | 	var longestMatch int
 91 | 	for _, conf := range h.Configs {
 92 | 		basePath := conf.Path
 93 | 		if httpserver.Path(r.URL.Path).Matches(basePath) && len(basePath) > longestMatch {
 94 | 			// Convert the request to Invoke input struct
 95 | 			invokeInput, err = conf.MaybeToInvokeInput(r)
 96 | 			if err != nil {
 97 | 				return c, nil, err
 98 | 			} else if invokeInput != nil {
 99 | 				longestMatch = len(basePath)
100 | 				c = conf
101 | 			}
102 | 		}
103 | 	}
104 | 	return c, invokeInput, nil
105 | }
106 | 


--------------------------------------------------------------------------------
/lambda_test.go:
--------------------------------------------------------------------------------
  1 | package awslambda
  2 | 
  3 | import (
  4 | 	"bytes"
  5 | 	"encoding/json"
  6 | 	"net/http"
  7 | 	"net/http/httptest"
  8 | 	"testing"
  9 | 
 10 | 	"github.com/aws/aws-sdk-go/service/lambda"
 11 | )
 12 | 
 13 | func TestInvokeOK(t *testing.T) {
 14 | 	replyPayload := `{ "name": "bob"}`
 15 | 	invoker := &FakeInvoker{
 16 | 		Calls: []*lambda.InvokeInput{},
 17 | 		Reply: &lambda.InvokeOutput{
 18 | 			Payload: []byte(replyPayload),
 19 | 		},
 20 | 	}
 21 | 	h := initHandler(invoker)
 22 | 	r, err := http.NewRequest("POST", "/lambda-test/foo", bytes.NewBufferString("hi"))
 23 | 	if err != nil {
 24 | 		t.Fatalf("Failed to create request: %v", err)
 25 | 	}
 26 | 	w := httptest.NewRecorder()
 27 | 
 28 | 	status, err := h.ServeHTTP(w, r)
 29 | 	if err != nil {
 30 | 		t.Errorf("ServeHTTP returned err: %v", err)
 31 | 	}
 32 | 	if status != 200 {
 33 | 		t.Errorf("Expected 200 status, got: %d", status)
 34 | 	}
 35 | 
 36 | 	if len(invoker.Calls) != 1 {
 37 | 		t.Errorf("Expected 1 Invoke call, but got: %+v", invoker.Calls)
 38 | 	}
 39 | 
 40 | 	expected := replyPayload
 41 | 	actual := w.Body.String()
 42 | 	if expected != actual {
 43 | 		t.Errorf("\nResponse body did not match\nExpected: %s\n  Actual: %s", expected, actual)
 44 | 	}
 45 | }
 46 | 
 47 | func TestInvokeInvalidFunc(t *testing.T) {
 48 | 	h := initHandler(nil)
 49 | 	h.Configs[0].Include = []string{"blah"}
 50 | 	r, err := http.NewRequest("POST", "/lambda-test/invalid", bytes.NewBufferString("hi"))
 51 | 	if err != nil {
 52 | 		t.Fatalf("Failed to create request: %v", err)
 53 | 	}
 54 | 	w := httptest.NewRecorder()
 55 | 
 56 | 	status, err := h.ServeHTTP(w, r)
 57 | 	if err != nil {
 58 | 		t.Errorf("ServeHTTP returned err: %v", err)
 59 | 	}
 60 | 	if status != 202 {
 61 | 		t.Errorf("Expected 202 status, got: %d", status)
 62 | 	}
 63 | }
 64 | 
 65 | ////////////////////////////////////////
 66 | 
 67 | func marshalJSON(i interface{}) []byte {
 68 | 	j, err := json.Marshal(i)
 69 | 	if err != nil {
 70 | 		panic(err)
 71 | 	}
 72 | 	return j
 73 | }
 74 | 
 75 | func initHandler(invoker Invoker) Handler {
 76 | 	return Handler{
 77 | 		Next: &FakeHandler{ReplyStatus: 202},
 78 | 		Configs: []*Config{
 79 | 			&Config{
 80 | 				Path:    "/lambda-test/",
 81 | 				invoker: invoker,
 82 | 			},
 83 | 		},
 84 | 	}
 85 | }
 86 | 
 87 | type FakeInvoker struct {
 88 | 	Calls      []*lambda.InvokeInput
 89 | 	Reply      *lambda.InvokeOutput
 90 | 	ReplyError error
 91 | }
 92 | 
 93 | func (i *FakeInvoker) Invoke(input *lambda.InvokeInput) (*lambda.InvokeOutput, error) {
 94 | 	i.Calls = append(i.Calls, input)
 95 | 	return i.Reply, i.ReplyError
 96 | }
 97 | 
 98 | type FakeHandler struct {
 99 | 	ReplyStatus int
100 | 	ReplyError  error
101 | 	Calls       int
102 | }
103 | 
104 | func (h *FakeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
105 | 	h.Calls++
106 | 	return h.ReplyStatus, h.ReplyError
107 | }
108 | 


--------------------------------------------------------------------------------
/reqrep.go:
--------------------------------------------------------------------------------
  1 | package awslambda
  2 | 
  3 | import (
  4 | 	"bytes"
  5 | 	"encoding/json"
  6 | 	"io"
  7 | 	"net/http"
  8 | 	"strings"
  9 | )
 10 | 
 11 | // Request represents a single HTTP request.  It will be serialized as JSON
 12 | // and sent to the AWS Lambda function as the function payload.
 13 | type Request struct {
 14 | 	// Set to the constant "HTTPJSON-REQ"
 15 | 	Type string `json:"type"`
 16 | 	// Metadata about the HTTP request
 17 | 	Meta *RequestMeta `json:"meta"`
 18 | 	// HTTP request body (may be empty)
 19 | 	Body string `json:"body"`
 20 | }
 21 | 
 22 | // RequestMeta represents HTTP metadata present on the request
 23 | type RequestMeta struct {
 24 | 	// HTTP method used by client (e.g. GET or POST)
 25 | 	Method string `json:"method"`
 26 | 
 27 | 	// Path portion of URL without the query string
 28 | 	Path string `json:"path"`
 29 | 
 30 | 	// Query string (without '?')
 31 | 	Query string `json:"query"`
 32 | 
 33 | 	// Host field from net/http Request, which may be of the form host:port
 34 | 	Host string `json:"host"`
 35 | 
 36 | 	// Proto field from net/http Request, for example "HTTP/1.1"
 37 | 	Proto string `json:"proto"`
 38 | 
 39 | 	// HTTP request headers
 40 | 	Headers map[string][]string `json:"headers"`
 41 | }
 42 | 
 43 | // NewRequest returns a new Request based on the HTTP request.
 44 | // Returns an error if the HTTP request body cannot be read.
 45 | func NewRequest(r *http.Request) (*Request, error) {
 46 | 	// TODO: possibly use buf.Grow() based on content-length header (if present)?
 47 | 	// Not sure if that will speed things up in the common case
 48 | 	buf := &bytes.Buffer{}
 49 | 	if r.Body != nil {
 50 | 		_, err := io.Copy(buf, r.Body)
 51 | 		if err != nil {
 52 | 			return nil, err
 53 | 		}
 54 | 	}
 55 | 
 56 | 	return &Request{
 57 | 		Type: "HTTPJSON-REQ",
 58 | 		Meta: newRequestMeta(r),
 59 | 		Body: buf.String(),
 60 | 	}, nil
 61 | }
 62 | 
 63 | // newRequestMeta returns a new RequestMeta based on the HTTP request
 64 | func newRequestMeta(r *http.Request) *RequestMeta {
 65 | 	headers := make(map[string][]string)
 66 | 	for k, v := range r.Header {
 67 | 		headers[strings.ToLower(k)] = v
 68 | 	}
 69 | 	return &RequestMeta{
 70 | 		Method:  r.Method,
 71 | 		Path:    r.URL.Path,
 72 | 		Query:   r.URL.RawQuery,
 73 | 		Host:    r.Host,
 74 | 		Proto:   r.Proto,
 75 | 		Headers: headers,
 76 | 	}
 77 | }
 78 | 
 79 | // Reply encapsulates the response from a Lambda invocation.
 80 | // AWS Lambda functions should return a JSON object that matches this format.
 81 | type Reply struct {
 82 | 	// Must be set to the constant "HTTPJSON-REP"
 83 | 	Type string `json:"type"`
 84 | 	// Reply metadata. If omitted, a default 200 status with empty headers will be used.
 85 | 	Meta *ReplyMeta `json:"meta"`
 86 | 	// Response body
 87 | 	Body string `json:"body"`
 88 | 	// Encoding of Body - Valid values: "", "base64"
 89 | 	BodyEncoding string `json:"bodyEncoding"`
 90 | }
 91 | 
 92 | // ReplyMeta encapsulates HTTP response metadata that the lambda function wishes
 93 | // Caddy to set on the HTTP response.
 94 | //
 95 | // *NOTE* that header values must be encoded as string arrays
 96 | type ReplyMeta struct {
 97 | 	// HTTP status code (e.g. 200 or 404)
 98 | 	Status int `json:"status"`
 99 | 	// HTTP response headers
100 | 	Headers map[string][]string `json:"headers"`
101 | }
102 | 
103 | // ParseReply unpacks the Lambda response data into a Reply.
104 | // If the reply is a JSON object with a 'type' field equal to 'HTTPJSON-REP', then
105 | // data will be unmarshaled directly as a Reply struct.
106 | //
107 | // If data is not a JSON object, or the object's type field is omitted or set to
108 | // a string other than 'HTTPJSON-REP', then data will be set as the Reply.body
109 | // and Reply.meta will contain a default struct with a 200 status and
110 | // a content-type header of 'application/json'.
111 | func ParseReply(data []byte) (*Reply, error) {
112 | 	if len(data) > 0 && data[0] == '{' {
113 | 		var rep Reply
114 | 		err := json.Unmarshal(data, &rep)
115 | 		if err == nil && rep.Type == "HTTPJSON-REP" {
116 | 			if rep.Meta == nil {
117 | 				rep.Meta = &defaultMeta
118 | 			}
119 | 			return &rep, nil
120 | 		}
121 | 	}
122 | 
123 | 	return &Reply{
124 | 		Type: "HTTPJSON-REP",
125 | 		Meta: &defaultMeta,
126 | 		Body: string(data),
127 | 	}, nil
128 | }
129 | 
130 | var defaultMeta = ReplyMeta{
131 | 	Status: 200,
132 | 	Headers: map[string][]string{
133 | 		"content-type": []string{"application/json"},
134 | 	},
135 | }
136 | 


--------------------------------------------------------------------------------
/reqrep_test.go:
--------------------------------------------------------------------------------
  1 | package awslambda
  2 | 
  3 | import (
  4 | 	"bytes"
  5 | 	"encoding/json"
  6 | 	"net/http"
  7 | 	"net/url"
  8 | 	"reflect"
  9 | 	"testing"
 10 | )
 11 | 
 12 | func TestNewRequest(t *testing.T) {
 13 | 	for i, test := range []struct {
 14 | 		method   string
 15 | 		url      string
 16 | 		body     string
 17 | 		headers  map[string][]string
 18 | 		expected Request
 19 | 	}{
 20 | 		{
 21 | 			"GET", "http://example.com/foo?a=b&c=1", "", nil,
 22 | 			Request{
 23 | 				Type: "HTTPJSON-REQ",
 24 | 				Meta: &RequestMeta{
 25 | 					Method:  "GET",
 26 | 					Path:    "/foo",
 27 | 					Query:   "a=b&c=1",
 28 | 					Headers: map[string][]string{},
 29 | 				},
 30 | 			},
 31 | 		},
 32 | 		{
 33 | 			"POST", "https://www.example.org/cat/dog/bird", "post-body-here",
 34 | 			map[string][]string{
 35 | 				"x-header-1":   []string{"1-val"},
 36 | 				"content-type": []string{"image/jpeg"},
 37 | 			},
 38 | 			Request{
 39 | 				Type: "HTTPJSON-REQ",
 40 | 				Meta: &RequestMeta{
 41 | 					Method:  "POST",
 42 | 					Path:    "/cat/dog/bird",
 43 | 					Headers: map[string][]string{},
 44 | 				},
 45 | 			},
 46 | 		},
 47 | 	} {
 48 | 		u, err := url.Parse(test.url)
 49 | 		if err != nil {
 50 | 			t.Errorf("Unable to parse url: %s", test.url)
 51 | 		}
 52 | 
 53 | 		httpReq := &http.Request{
 54 | 			Method: test.method,
 55 | 			URL:    u,
 56 | 			Header: http.Header(test.headers),
 57 | 		}
 58 | 
 59 | 		if test.body != "" {
 60 | 			httpReq.Body = newBufCloser(test.body)
 61 | 			test.expected.Body = test.body
 62 | 		}
 63 | 
 64 | 		if test.headers != nil {
 65 | 			test.expected.Meta.Headers = test.headers
 66 | 		}
 67 | 
 68 | 		actual, err := NewRequest(httpReq)
 69 | 		if err != nil {
 70 | 			t.Errorf("\nTest %d returned non-nil err: %v", i, err)
 71 | 		} else if actual == nil {
 72 | 			t.Errorf("\nTest %d returned nil request", i)
 73 | 		} else {
 74 | 			eqOrErr(test.expected, *actual, i, t)
 75 | 		}
 76 | 	}
 77 | }
 78 | 
 79 | func TestParseReply(t *testing.T) {
 80 | 	for i, test := range []struct {
 81 | 		data          []byte
 82 | 		expectDefault bool
 83 | 		expected      Reply
 84 | 	}{
 85 | 		{[]byte("hello"), true, Reply{}},
 86 | 		{nil, true, Reply{}},
 87 | 		{[]byte(`{"type":"other", "meta": "stuff"}`), true, Reply{}},
 88 | 		{[]byte(`{"type":"HTTPJSON-REP", "meta": { "status": 404 }, "body": "1234" }`), false,
 89 | 			Reply{
 90 | 				Meta: &ReplyMeta{
 91 | 					Status: 404,
 92 | 				},
 93 | 				Body: "1234",
 94 | 			}},
 95 | 		{[]byte(`{"type":"HTTPJSON-REP", "body": "zzzz" }`), false,
 96 | 			Reply{
 97 | 				Meta: &defaultMeta,
 98 | 				Body: "zzzz",
 99 | 			}},
100 | 	} {
101 | 		if test.expectDefault {
102 | 			test.expected = Reply{
103 | 				Meta: &defaultMeta,
104 | 				Body: string(test.data),
105 | 			}
106 | 		}
107 | 
108 | 		test.expected.Type = "HTTPJSON-REP"
109 | 
110 | 		actual, err := ParseReply(test.data)
111 | 		if err != nil {
112 | 			t.Errorf("\nTest %d returned err: %v", i, err)
113 | 		} else if actual == nil {
114 | 			t.Errorf("Test %d returned nil", i)
115 | 		} else {
116 | 			eqOrErr(test.expected, *actual, i, t)
117 | 		}
118 | 	}
119 | }
120 | 
121 | /////////////////////
122 | 
123 | func newBufCloser(s string) *BufCloser {
124 | 	return &BufCloser{
125 | 		bytes.NewBufferString(s),
126 | 	}
127 | }
128 | 
129 | type BufCloser struct {
130 | 	*bytes.Buffer
131 | }
132 | 
133 | func (b *BufCloser) Close() error {
134 | 	return nil
135 | }
136 | 
137 | func eqOrErr(expected, actual interface{}, num int, t *testing.T) bool {
138 | 	if !reflect.DeepEqual(expected, actual) {
139 | 		ex, err := json.Marshal(expected)
140 | 		ac, err2 := json.Marshal(actual)
141 | 		if err != nil || err2 != nil {
142 | 			t.Errorf("\nTest %d\nExpected: %+v\n  Actual: %+v", num, expected, actual)
143 | 			return false
144 | 		}
145 | 		t.Errorf("\nTest %d\nExpected: %s\n  Actual: %s", num, ex, ac)
146 | 		return false
147 | 	}
148 | 	return true
149 | }
150 | 


--------------------------------------------------------------------------------
/setup.go:
--------------------------------------------------------------------------------
 1 | package awslambda
 2 | 
 3 | import (
 4 | 	"github.com/caddyserver/caddy"
 5 | 	"github.com/caddyserver/caddy/caddyhttp/httpserver"
 6 | )
 7 | 
 8 | func init() {
 9 | 	caddy.RegisterPlugin("awslambda", caddy.Plugin{
10 | 		ServerType: "http",
11 | 		Action:     setup,
12 | 	})
13 | }
14 | 
15 | // setup configures a new AWS Lambda middleware instance.
16 | func setup(c *caddy.Controller) error {
17 | 	configs, err := ParseConfigs(c)
18 | 	if err != nil {
19 | 		return err
20 | 	}
21 | 
22 | 	httpserver.GetConfig(c).AddMiddleware(func(next httpserver.Handler) httpserver.Handler {
23 | 		return Handler{
24 | 			Next:    next,
25 | 			Configs: configs,
26 | 		}
27 | 	})
28 | 	return nil
29 | }
30 | 


--------------------------------------------------------------------------------
/setup_test.go:
--------------------------------------------------------------------------------
 1 | package awslambda
 2 | 
 3 | import (
 4 | 	"testing"
 5 | 
 6 | 	"github.com/caddyserver/caddy"
 7 | 	"github.com/caddyserver/caddy/caddyhttp/httpserver"
 8 | )
 9 | 
10 | func TestSetup(t *testing.T) {
11 | 	input := "awslambda /foo"
12 | 	c := caddy.NewTestController("http", input)
13 | 	err := setup(c)
14 | 	if err != nil {
15 | 		t.Errorf("setup() returned err: %v", err)
16 | 	}
17 | 
18 | 	mids := httpserver.GetConfig(c).Middleware()
19 | 	mid := mids[len(mids)-1]
20 | 	handler := mid(nil).(Handler)
21 | 
22 | 	expected := []*Config{
23 | 		&Config{
24 | 			Path:    "/foo",
25 | 			Include: []string{},
26 | 			Exclude: []string{},
27 | 		},
28 | 	}
29 | 	handler.Configs[0].invoker = nil
30 | 	eqOrErr(expected, handler.Configs, 0, t)
31 | }
32 | 


--------------------------------------------------------------------------------
/test_all.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/sh
 2 | 
 3 | set -e
 4 | 
 5 | export GO111MODULE=on
 6 | go test -v -covermode=count -coverprofile=coverage.out
 7 | 
 8 | if [ -n "$COVERALLS_TOKEN" ]; then
 9 |     $GOPATH/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN
10 | fi
11 | 


--------------------------------------------------------------------------------