Skip to content

Commit

Permalink
Add iiif-server
Browse files Browse the repository at this point in the history
  • Loading branch information
mbklein committed Sep 18, 2024
1 parent db5faf2 commit 8a11223
Show file tree
Hide file tree
Showing 7 changed files with 599 additions and 1 deletion.
10 changes: 9 additions & 1 deletion .gitignore
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
36 changes: 36 additions & 0 deletions iiif-server/README.md
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
62 changes: 62 additions & 0 deletions iiif-server/src/authorize.js
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;
165 changes: 165 additions & 0 deletions iiif-server/src/index.js
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
})
)
};
101 changes: 101 additions & 0 deletions iiif-server/src/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 8a11223

Please sign in to comment.