Skip to content

Commit

Permalink
feat: Add the mini server + add unit testing
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolasdao committed Jul 13, 2017
1 parent e138ae1 commit 1e997eb
Show file tree
Hide file tree
Showing 10 changed files with 626 additions and 74 deletions.
9 changes: 0 additions & 9 deletions HelloWorld/index.js

This file was deleted.

21 changes: 0 additions & 21 deletions HelloWorld/package.json

This file was deleted.

18 changes: 0 additions & 18 deletions HelloWorld/webconfig.json

This file was deleted.

51 changes: 49 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@ This is the main 'raison d'être' of this project. Out-of-the box, Google Cloud
{
"headers": {
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS, POST",
"Access-Control-Allow-Headers": "Content-Type, Origin",
"Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Max-Age": "1296000"
}
}
```
More details about those headers in the [Annexes](#annexes) section under [A.1. CORS Refresher](#a1-cors-refresher).

> CORS is a classic source of headache. Though webfunc allows to easily configure any Google Cloud Functions project, it will not prevent anybody to badly configure a project, and therefore loose a huge amount of time. For that reason, a series of common mistakes have been documented in the [Annexes](#annexes) section under [A.2. CORS Basic Errors](#a2-cors-basic-errors).
#### Adding Multiple Deployment Environments
Let's imagine that 3 different environments have been setup on a Google Cloud Account, the webfunc can easily deploy to any of those environment if they have been configured in the project's _**webconfig.json**_ file:
Expand Down Expand Up @@ -82,6 +84,51 @@ To deploy to a specific environment(prod for example):
```
webfunc deploy prod
```
## Annexes
#### A.1. CORS Refresher
_COMING SOON..._

#### A.2. CORS Basic Errors
_**WithCredentials & CORS**_
The following configuration is forbidden:
```js
{
"headers": {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true"
}
}
```

You cannot allow anybody to access a resource("Access-Control-Allow-Origin": "*") while at the same time allowing anybody to share cookies("Access-Control-Allow-Credentials": "true"). This would be a huge security breach (i.e. [CSRF attach](https://en.wikipedia.org/wiki/Cross-site_request_forgery)).

For that reason, this configuration, though it allow your resource to be called from the same origin, would fail once your API is called from a different origin. A error similar to the following would be thrown by the browser:
```
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
```

__*Solutions*__

If you do need to share cookies, you will have to be explicitely specific about the origins that are allowed to do so:
```js
{
"headers": {
"Access-Control-Allow-Origin": "http://your-allowed-origin.com",
"Access-Control-Allow-Credentials": "true"
}
}
```

If you do need to allow access to anybody, then do not allow requests to send cookies:
```js
{
"headers": {
"Access-Control-Allow-Headers": "Authorization",
"Access-Control-Allow-Origin": "*",
}
}
```
If you do need to pass authentication token, you will have to pass it using a special header(e.g. Authorization), or pass it in the query string if you want to avoid preflight queries (preflight queries happens in cross-origin requests when special headers are being used). However, passing credentials in the query string are considered a bad practice.

## License
Copyright (c) 2017, Neap Pty Ltd.
Expand Down
12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
"bin": {
"webfunc": "./index.js"
},
"main": "index.js",
"main": "src/webfunc.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"eslint": "eslint index.js src/",
"test": "mocha",
"eslint": "eslint index.js src/ test/",
"release": "standard-version"
},
"repository": {
Expand All @@ -35,13 +35,17 @@
"dependencies": {
"colors": "^1.1.2",
"commander": "^2.11.0",
"http-errors": "^1.6.1",
"lodash": "^4.17.4",
"ncp": "^2.0.0",
"replace": "^0.3.0",
"shelljs": "^0.7.8",
"standard-version": "^4.2.0"
},
"devDependencies": {
"eslint": "^4.1.1"
"chai": "^4.1.0",
"eslint": "^4.1.1",
"mocha": "^3.4.2",
"node-mocks-http": "^1.6.4"
}
}
16 changes: 8 additions & 8 deletions src/deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const deploy = (env = 'default') => {
/*eslint-enable */

if (!fs.existsSync(webconfigPath)) {
console.log(`Missing webconfig.json file. Run ${`webfunc init`.italic.bold} to initialize a new one.`.red)
console.log(`Missing webconfig.json file. Run ${'webfunc init'.italic.bold} to initialize a new one.`.red)
/*eslint-disable */
process.exit(1)
/*eslint-enable */
Expand All @@ -30,7 +30,7 @@ const deploy = (env = 'default') => {
const environments = webconfig.env

if (!environments) {
console.log(`${`webconfig.json`.italic.bold} is missing the ${`env`.italic.bold} property.`.red)
console.log(`${'webconfig.json'.italic.bold} is missing the ${'env'.italic.bold} property.`.red)
/*eslint-disable */
process.exit(1)
/*eslint-enable */
Expand All @@ -39,21 +39,21 @@ const deploy = (env = 'default') => {
const config = environments[env]

if (!config) {
console.log(`${`webconfig.json`.italic.bold} does not define any ${env.italic.bold} property under its ${`env`.italic.bold} property.`.red)
console.log(`${'webconfig.json'.italic.bold} does not define any ${env.italic.bold} property under its ${'env'.italic.bold} property.`.red)
/*eslint-disable */
process.exit(1)
/*eslint-enable */
}

if (!config.trigger) {
console.log(`${`webconfig.json`.italic.bold} does not define any ${`trigger`.italic.bold} property under its ${env.italic.bold} environment.`.red)
console.log(`${'webconfig.json'.italic.bold} does not define any ${'trigger'.italic.bold} property under its ${env.italic.bold} environment.`.red)
/*eslint-disable */
process.exit(1)
/*eslint-enable */
}

if (!config.entryPoint) {
console.log(`${`webconfig.json`.italic.bold} does not define any ${`entryPoint`.italic.bold} property under its ${env.italic.bold} environment.`.red)
console.log(`${'webconfig.json'.italic.bold} does not define any ${'entryPoint'.italic.bold} property under its ${env.italic.bold} environment.`.red)
/*eslint-disable */
process.exit(1)
/*eslint-enable */
Expand All @@ -72,21 +72,21 @@ const deploy = (env = 'default') => {
}
else {
if (!config.functionName) {
console.log(`${`webconfig.json`.italic.bold} does not define any ${`functionName`.italic.bold} property under its ${env.italic.bold} environment.`.red)
console.log(`${'webconfig.json'.italic.bold} does not define any ${'functionName'.italic.bold} property under its ${env.italic.bold} environment.`.red)
/*eslint-disable */
process.exit(1)
/*eslint-enable */
}

if (!config.googleProject) {
console.log(`${`webconfig.json`.italic.bold} does not define any ${`googleProject`.italic.bold} property under its ${env.italic.bold} environment.`.red)
console.log(`${'webconfig.json'.italic.bold} does not define any ${'googleProject'.italic.bold} property under its ${env.italic.bold} environment.`.red)
/*eslint-disable */
process.exit(1)
/*eslint-enable */
}

if (!config.bucket) {
console.log(`${`webconfig.json`.italic.bold} does not define any ${`bucket`.italic.bold} property under its ${env.italic.bold} environment.`.red)
console.log(`${'webconfig.json'.italic.bold} does not define any ${'bucket'.italic.bold} property under its ${env.italic.bold} environment.`.red)
/*eslint-disable */
process.exit(1)
/*eslint-enable */
Expand Down
98 changes: 98 additions & 0 deletions src/webfunc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* Copyright (c) 2017, Neap Pty Ltd.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
const path = require('path')
const fs = require('fs')
const httpError = require('http-errors')

let webconfig = null
const getWebConfig = () => {
if (webconfig == null) {
/*eslint-disable */
const webconfigPath = path.join(process.cwd(), 'webconfig.json')
/*eslint-enable */
webconfig = fs.existsSync(webconfigPath) ? require(webconfigPath) : undefined
}
return webconfig
}

let headersCollection = null
const getHeadersCollection = (headers = {}, memoize) => {
if (!memoize || headersCollection == null) {
headersCollection = []
for (let key in headers)
headersCollection.push({ key, value: headers[key] })
}
return headersCollection
}

let allowedOrigins = null
const getAllowedOrigins = (headers = {}, memoize) => {
if (!memoize || allowedOrigins == null) {
allowedOrigins = (headers['Access-Control-Allow-Origin'] || '').split(',')
.reduce((a, s) => {
if (s)
a[s.trim().toLowerCase().replace(/\/$/,'')] = true
return a
}, {})
}
return allowedOrigins
}

let allowedMethods = null
const getAllowedMethods = (headers = {}, memoize) => {
if (!memoize || allowedMethods == null) {
allowedMethods = (headers['Access-Control-Allow-Methods'] || '').split(',')
.reduce((a, s) => {
if (s)
a[s.trim().toLowerCase()] = true
return a
}, {})
}
return allowedMethods
}

const setResponseHeaders = (res, webconfig) => Promise.resolve((webconfig || getWebConfig() || {}).headers)
.then(headers => getHeadersCollection(headers, !webconfig).reduce((response, header) => res.set(header.key, header.value), res))

const handleHttpRequest = (req, res, webconfig) => Promise.resolve(webconfig || getWebConfig() || {})
.then(webConfig => {
const headers = webConfig.headers
const memoize = !webconfig
const origins = getAllowedOrigins(headers, memoize)
const methods = getAllowedMethods(headers, memoize)
const origin = new String(req.headers.origin).toLowerCase()
const referer = new String(req.headers.referer).toLowerCase()
const method = new String(req.method).toLowerCase()
const sameOrigin = referer.indexOf(origin) == 0

// Check CORS
if (Object.keys(origins).length == 0 && !sameOrigin) {
setResponseHeaders(res, webConfig)
throw httpError(403, `Forbidden - CORS issue. Origin '${origin}' is not allowed.`)
}
if (!origins['*'] && Object.keys(origins).length != 0 && !(origin in origins)) {
setResponseHeaders(res, webConfig)
throw httpError(403, `Forbidden - CORS issue. Origin '${origin}' is not allowed.`)
}
if (Object.keys(methods).length != 0 && method != 'get' && method != 'head' && !(method in methods)) {
setResponseHeaders(res, webConfig)
throw httpError(403, `Forbidden - CORS issue. Method '${method.toUpperCase()}' is not allowed.`)
}

if (method == 'head' || method == 'options')
return setResponseHeaders(res, webConfig).then(res => res.status(200).send())
})

const serveHttp = (processHttpRequest, webconfig) => (req, res) => handleHttpRequest(req, res, webconfig)
.then(() => !res.headersSent ? processHttpRequest(req, res) : res)

module.exports = {
setResponseHeaders,
handleHttpRequest,
serveHttp
}
13 changes: 3 additions & 10 deletions templates/simpleWebApp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,12 @@
"version": "{{projectVersion}}",
"scripts": {
"postinstall": "npm dedupe",
"test": "mocha",
"deploy": "node deploy.js",
"logs": "node logs.js"
"test": "mocha"
},
"dependencies": {
"lodash": "^4.17.4",
"shortid": "^2.2.8",
"uuid": "^3.0.1",
"xml2js": "^0.4.17"
},
"devDependencies": {
"colors": "^1.1.2",
"mocha": "^3.4.2",
"shelljs": "^0.7.8"
"chai": "^4.1.0",
"mocha": "^3.4.2"
}
}
3 changes: 1 addition & 2 deletions templates/simpleWebApp/webconfig.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
{
"headers": {
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS, POST",
"Access-Control-Allow-Headers": "authentication, Content-Type, Origin",
"Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Accept",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Max-Age": "1296000"
},
"env": {
Expand Down
Loading

0 comments on commit 1e997eb

Please sign in to comment.