-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
599 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,15 @@ | ||
# Terraform | ||
**/.terraform | ||
**/builds | ||
*.tfstate | ||
*.tfvars | ||
*.plan | ||
|
||
# SAM | ||
samconfig.* | ||
.aws-sam | ||
package.yml | ||
|
||
# Local Build | ||
node_modules | ||
data_services/model/.working | ||
data_services/model/.working |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# iiif-server | ||
|
||
This SAM project project includes the viewer-request function and SAM template required to install and configure the NUL IIIF Server. | ||
|
||
## Usage | ||
|
||
The `config-env` parameter will always be the environment you're deploying to (e.g., `staging`) | ||
|
||
### First Use | ||
``` | ||
sam build | ||
sam deploy --config-env staging --guided | ||
``` | ||
|
||
This will prompt for all of the deploy parameters, and then give you the option to save them in a config file for later use. | ||
|
||
### Subsequent Use | ||
``` | ||
sam build | ||
sam deploy --config-env staging | ||
``` | ||
|
||
This will use the previously saved config file | ||
|
||
## Deploy parameters | ||
|
||
- `Hostname`: The host part of the server FQDN (e.g., `iiif`) | ||
- `DomainName`: The domain part of the server FQDN (e.g., `rdc.library.northwestern.edu`) | ||
- `AllowFromReferers`: A regular expression to match against the Referer header for pass-through - `authorization` (Default: `""`) | ||
- `CertificateArn`: The ARN of an SSL certificate that matches the server FQDN | ||
- `DcApiEndpoint`: The public endpoint for the DC API | ||
- `IiifLambdaMemory`: The amount of memory in MB for the IIIF lambda to use (Default: `2048`) | ||
- `IiifLambdaTimeout`: The timeout for the IIIF lambda (Default: `10`) | ||
- `Namespace`: The infrastructure namespace prefix to use for secrets management | ||
- `ServerlessIiifVersion`: The version of Serverless IIIF to deploy (Default: `5.0.0`) | ||
- `SourceBucket`: The bucket where the pyramid TIFF images are stored |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
const isString = require("lodash.isstring"); | ||
const fetch = require("node-fetch"); | ||
|
||
let allowFrom, dcApiEndpoint; | ||
function reconfigure(config) { | ||
({ allowFrom, dcApiEndpoint } = config); | ||
} | ||
|
||
function allowedFromRegexes(str) { | ||
var configValues = isString(str) ? str.split(";") : []; | ||
var result = []; | ||
for (var re in configValues) { | ||
result.push(new RegExp(configValues[re])); | ||
} | ||
return result; | ||
} | ||
|
||
function isBlurred({ region, size }) { | ||
if (region !== "full") return false; // not a full frame request | ||
if (typeof size !== "string") return false; // size parameter not specified | ||
|
||
const match = size.match(/!(\d+)?,(\d+)?/); | ||
if (match === null) return false; // constrained height and width not specified | ||
const width = Number(match[1]); | ||
const height = Number(match[2]); | ||
if (width <= 5 || height <= 5) return true; // image constrained to <=5px in its largest dimension | ||
|
||
return false; | ||
} | ||
|
||
async function authorize(params, referer, cookie, clientIp, config) { | ||
reconfigure(config); | ||
const allowedFrom = allowedFromRegexes(allowFrom); | ||
|
||
if (params.filename == "info.json") return true; | ||
if (isBlurred(params)) return true; | ||
|
||
for (var re in allowedFrom) { | ||
if (allowedFrom[re].test(referer)) return true; | ||
} | ||
|
||
const id = params.id.split("/").slice(-1)[0]; | ||
|
||
return await getImageAuthorization(id, cookie, clientIp); | ||
} | ||
|
||
async function getImageAuthorization(id, cookieHeader, clientIp) { | ||
const opts = { | ||
headers: { | ||
cookie: cookieHeader, | ||
}, | ||
}; | ||
if (clientIp) opts.headers["x-client-ip"] = clientIp; | ||
|
||
const response = await fetch( | ||
`${dcApiEndpoint}/file-sets/${id}/authorization`, | ||
opts | ||
); | ||
return response.status == 204; | ||
} | ||
|
||
module.exports = authorize; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
const authorize = require("./authorize"); | ||
const middy = require("@middy/core"); | ||
const secretsManager = require("@middy/secrets-manager"); | ||
|
||
function getEventHeader(request, name) { | ||
if ( | ||
request.headers && | ||
request.headers[name] && | ||
request.headers[name].length > 0 | ||
) { | ||
return request.headers[name][0].value; | ||
} else { | ||
return undefined; | ||
} | ||
} | ||
|
||
function viewerRequestOptions(request) { | ||
const origin = getEventHeader(request, "origin") || "*"; | ||
return { | ||
status: "200", | ||
statusDescription: "OK", | ||
headers: { | ||
"access-control-allow-headers": [ | ||
{ key: "Access-Control-Allow-Headers", value: "authorization, cookie" } | ||
], | ||
"access-control-allow-credentials": [ | ||
{ key: "Access-Control-Allow-Credentials", value: "true" } | ||
], | ||
"access-control-allow-methods": [ | ||
{ key: "Access-Control-Allow-Methods", value: "GET, OPTIONS" } | ||
], | ||
"access-control-allow-origin": [ | ||
{ key: "Access-Control-Allow-Origin", value: origin } | ||
] | ||
}, | ||
body: "OK" | ||
}; | ||
} | ||
|
||
function parsePath(path) { | ||
const segments = path.split(/\//).reverse(); | ||
|
||
if (segments.length < 8) { | ||
return { | ||
poster: segments[2] == "posters", | ||
id: segments[1], | ||
filename: segments[0] | ||
}; | ||
} else { | ||
return { | ||
poster: segments[5] == "posters", | ||
id: segments[4], | ||
region: segments[3], | ||
size: segments[2], | ||
rotation: segments[1], | ||
filename: segments[0] | ||
}; | ||
} | ||
} | ||
|
||
async function viewerRequestIiif(request, { config }) { | ||
const path = decodeURI(request.uri.replace(/%2f/gi, "")); | ||
const params = parsePath(path); | ||
const referer = getEventHeader(request, "referer"); | ||
const cookie = getEventHeader(request, "cookie"); | ||
const authed = await authorize( | ||
params, | ||
referer, | ||
cookie, | ||
request.clientIp, | ||
config | ||
); | ||
console.log("Authorized:", authed); | ||
|
||
// Return a 403 response if not authorized to view the requested item | ||
if (!authed) { | ||
return { | ||
status: "403", | ||
statusDescription: "Forbidden", | ||
body: "Forbidden" | ||
}; | ||
} | ||
|
||
// Set the x-preflight-location request header to the location of the requested item | ||
const pairtree = params.id.match(/.{1,2}/g).join("/"); | ||
const s3Location = params.poster | ||
? `s3://${config.tiffBucket}/posters/${pairtree}-poster.tif` | ||
: `s3://${config.tiffBucket}/${pairtree}-pyramid.tif`; | ||
request.headers["x-preflight-location"] = [ | ||
{ key: "X-Preflight-Location", value: s3Location } | ||
]; | ||
return request; | ||
} | ||
|
||
async function processViewerRequest(event, context) { | ||
console.log("Initiating viewer-request trigger"); | ||
const { request } = event.Records[0].cf; | ||
let result; | ||
|
||
if (request.method === "OPTIONS") { | ||
// Intercept OPTIONS request and return proper response | ||
result = viewerRequestOptions(request); | ||
} else { | ||
result = await viewerRequestIiif(request, context); | ||
} | ||
|
||
return result; | ||
} | ||
|
||
// async function logSecret() { | ||
// const { | ||
// SecretsManagerClient, | ||
// GetSecretValueCommand | ||
// } = require("@aws-sdk/client-secrets-manager"); | ||
// const client = new SecretsManagerClient(); | ||
// const result = await client.send( | ||
// new GetSecretValueCommand({ | ||
// SecretId: process.env.AWS_LAMBDA_FUNCTION_NAME | ||
// }) | ||
// ); | ||
// console.log("raw", result.SecretString); | ||
// console.log("parsed", JSON.parse(result.SecretString)); | ||
// } | ||
|
||
async function processRequest(event, context) { | ||
const { eventType } = event.Records[0].cf.config; | ||
let result; | ||
|
||
console.log("Event Type:", eventType); | ||
if (eventType === "viewer-request") { | ||
result = await processViewerRequest(event, context); | ||
} else { | ||
result = event.Records[0].cf.request; | ||
} | ||
|
||
return result; | ||
} | ||
|
||
function functionNameAndRegion() { | ||
let nameVar = process.env.AWS_LAMBDA_FUNCTION_NAME; | ||
const match = /^(?<functionRegion>[a-z]{2}-[a-z]+-\d+)\.(?<functionName>.+)$/.exec(nameVar); | ||
if (match) { | ||
return { ...match.groups } | ||
} else { | ||
return { | ||
functionName: nameVar, | ||
functionRegion: process.env.AWS_REGION | ||
} | ||
} | ||
} | ||
|
||
const { functionName, functionRegion } = functionNameAndRegion(); | ||
console.log("Initializing", functionName, 'in', functionRegion); | ||
|
||
module.exports = { | ||
handler: | ||
middy(processRequest) | ||
.use( | ||
secretsManager({ | ||
fetchData: { config: functionName }, | ||
awsClientOptions: { region: functionRegion }, | ||
setToContext: true | ||
}) | ||
) | ||
}; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.