scratch-www/bin/configure-fastly.js
Matthew Taylor ca067fdc5e Use custom VCL for Pass/!Pass conditions
We’ve now exceeded our max number of characters for a condition in the Fastly API, and we need to make it larger to accommodate regex conditionals that can match on any of the routes in www currently.

This fixes the issue by moving the conditions – and the states that are affected by it, like setting the backend or cache ttls – to two custom vcl files that are updated via the Fastly API. One is for the `vcl_rev` config, and one is for the `vcl_fetch` config.
2017-04-25 11:06:57 -04:00

259 lines
9.8 KiB
JavaScript

var async = require('async');
var defaults = require('lodash.defaults');
var glob = require('glob');
var path = require('path');
var route_json = require('../src/routes.json');
const FASTLY_SERVICE_ID = process.env.FASTLY_SERVICE_ID || '';
const S3_BUCKET_NAME = process.env.S3_BUCKET_NAME || '';
const BUCKET_NAME_HEADER_NAME = 'Bucket name';
var fastly = require('./lib/fastly-extended')(process.env.FASTLY_API_KEY, FASTLY_SERVICE_ID);
var extraAppRoutes = [
// Homepage with querystring.
// TODO: Should this be added for every route?
'/\\?',
// View html
'/[^\/]*\.html$'
];
/*
* Given the relative path to the static directory, return an array of
* patterns matching the files and directories there.
*/
var getStaticPaths = function (pathToStatic) {
var staticPaths = glob.sync(path.resolve(__dirname, pathToStatic));
return staticPaths.filter(function (pathName) {
// Exclude view html, resolve everything else in the build
return path.extname(pathName) !== '.html';
}).map(function (pathName) {
// Reduce absolute path to relative paths like '/js'
var base = path.dirname(path.resolve(__dirname, pathToStatic));
return pathName.replace(base, '') + (path.extname(pathName) ? '' : '/');
});
};
/*
* Given a list of express routes, return a list of patterns to match
* the express route and a static view file associated with the route
*/
var getViewPaths = function (routes) {
return routes.reduce(function (paths, route) {
var path = route.routeAlias || route.pattern;
if (paths.indexOf(path) === -1) {
paths.push(path);
}
return paths;
}, []);
};
/*
* Translate an express-style pattern e.g. /path/:arg/ to a regex
* all :arguments become .+?
*/
var expressPatternToRegex = function (pattern) {
return pattern.replace(/(:[^/]+)/gi, '.+?');
};
/*
* Given a list of patterns for paths, OR all of them together into one
* string suitable for a Fastly condition
*/
var pathsToCondition = function (paths) {
return 'req.url~"^(' + paths.reduce(function (conditionString, pattern) {
return conditionString + (conditionString ? '|' : '') + pattern;
}, '') + ')"';
};
/*
* Helper method to NOT a condition statement
*/
var negateConditionStatement = function (statement) {
return '!(' + statement + ')';
};
/*
* Combine static paths, routes, and any additional paths to a single
* fastly condition to match req.url
*/
var getAppRouteCondition = function (pathToStatic, routes, additionalPaths) {
var staticPaths = getStaticPaths(pathToStatic);
var viewPaths = getViewPaths(routes);
var allPaths = [].concat(staticPaths, viewPaths, additionalPaths);
return pathsToCondition(allPaths);
};
var getConditionNameForRoute = function (route, type) {
return 'routes/' + route.pattern + ' (' + type + ')';
};
var getHeaderNameForRoute = function (route) {
if (route.name) return 'rewrites/' + route.name;
if (route.redirect) return 'redirects/' + route.pattern;
};
var getResponseNameForRoute = function (route) {
return 'redirects/' + route.pattern;
};
var routes = route_json.map(function (route) {
return defaults({}, {pattern: expressPatternToRegex(route.pattern)}, route);
});
async.auto({
version: function (cb) {
fastly.getLatestVersion(function (err, response) {
if (err) return cb(err);
// Validate latest version before continuing
if (response.active || response.locked) {
fastly.cloneVersion(response.number, function (err, response) {
if (err) return cb('Failed to clone latest version: ' + err);
cb(null, response.number);
});
} else {
cb(null, response.number);
}
});
},
recvCustomVCL: ['version', function (cb, results) {
// For all the routes in routes.json, construct a varnish-style regex that matches
// on any of those route conditions.
var notPassStatement = getAppRouteCondition('../build/*', routes, extraAppRoutes);
// For all the routes in routes.json, construct a varnish-style regex that matches
// only if NONE of those routes are matched.
var passStatement = negateConditionStatement(notPassStatement);
// For a non-pass condition, point backend at s3
var backendCondition = fastly.setBackend(
'F_s3',
S3_BUCKET_NAME,
notPassStatement
);
// For a pass condition, set forwarding headers
var forwardCondition = fastly.setForwardHeaders(passStatement);
fastly.setCustomVCL(
results.version,
'recv-condition',
backendCondition + forwardCondition,
cb
);
}],
fetchCustomVCL: ['version', function (cb, results) {
var passStatement = negateConditionStatement(getAppRouteCondition('../build/*', routes, extraAppRoutes));
var ttlCondition = fastly.setResponseTTL(passStatement);
fastly.setCustomVCL(results.version, 'fetch-condition', ttlCondition, cb);
}],
setBucketNameHeader: ['version', 'notPassRequestCondition', function (cb, results) {
var header = {
name: BUCKET_NAME_HEADER_NAME,
action: 'set',
ignore_if_set: 0,
type: 'REQUEST',
dst: 'http.host',
src: '"' + S3_BUCKET_NAME + '"',
request_condition: results.notPassRequestCondition.name,
priority: 1
};
fastly.setFastlyHeader(results.version, header, cb);
}],
appRouteRequestConditions: ['version', function (cb, results) {
var conditions = {};
async.forEachOf(routes, function (route, id, cb2) {
var condition = {
name: getConditionNameForRoute(route, 'request'),
statement: 'req.url ~ "' + route.pattern + '"',
type: 'REQUEST',
// Priority needs to be > 1 to not interact with http->https redirect
priority: 10 + id
};
fastly.setCondition(results.version, condition, function (err, response) {
if (err) return cb2(err);
conditions[id] = response;
cb2(null, response);
});
}, function (err) {
if (err) return cb(err);
cb(null, conditions);
});
}],
appRouteHeaders: ['version', 'appRouteRequestConditions', function (cb, results) {
var headers = {};
async.forEachOf(routes, function (route, id, cb2) {
if (route.redirect) {
async.auto({
responseCondition: function (cb3) {
var condition = {
name: getConditionNameForRoute(route, 'response'),
statement: 'req.url ~ "' + route.pattern + '"',
type: 'RESPONSE',
priority: id
};
fastly.setCondition(results.version, condition, cb3);
},
responseObject: function (cb3) {
var responseObject = {
name: getResponseNameForRoute(route),
status: 301,
response: 'Moved Permanently',
request_condition: getConditionNameForRoute(route, 'request')
};
fastly.setResponseObject(results.version, responseObject, cb3);
},
redirectHeader: ['responseCondition', function (cb3, redirectResults) {
var header = {
name: getHeaderNameForRoute(route),
action: 'set',
ignore_if_set: 0,
type: 'RESPONSE',
dst: 'http.Location',
src: '"' + route.redirect + '"',
response_condition: redirectResults.responseCondition.name
};
fastly.setFastlyHeader(results.version, header, cb3);
}]
}, function (err, redirectResults) {
if (err) return cb2(err);
headers[id] = redirectResults.redirectHeader;
cb2(null, redirectResults);
});
} else {
var header = {
name: getHeaderNameForRoute(route, 'request'),
action: 'set',
ignore_if_set: 0,
type: 'REQUEST',
dst: 'url',
src: '"/' + route.name + '.html"',
request_condition: results.appRouteRequestConditions[id].name,
priority: 10
};
fastly.setFastlyHeader(results.version, header, function (err, response) {
if (err) return cb2(err);
headers[id] = response;
cb2(null, response);
});
}
}, function (err) {
if (err) return cb(err);
cb(null, headers);
});
}]},
function (err, results) {
if (err) throw new Error(err);
if (process.env.FASTLY_ACTIVATE_CHANGES) {
fastly.activateVersion(results.version, function (err, response) {
if (err) throw new Error(err);
process.stdout.write('Successfully configured and activated version ' + response.number + '\n');
fastly.purgeAll(FASTLY_SERVICE_ID, function (err) {
if (err) throw new Error(err);
process.stdout.write('Purged all.\n');
});
});
}
}
);