Skip to content

Commit 9b1749e

Browse files
FrancescoMolinaroatarix83
authored andcommitted
Merged in task/dspace-cris-2023_02_x/IIIF-188 (pull request DSpace#3122)
[IIIF-188] configure headers adaptions for allowed origins on IIIF viewer Approved-by: Giuseppe Digilio
2 parents 8b1ac9c + b630c06 commit 9b1749e

7 files changed

Lines changed: 95 additions & 15 deletions

server.ts

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ const DIST_FOLDER = join(process.cwd(), 'dist/browser');
6565
// Set path fir IIIF viewer.
6666
const IIIF_VIEWER = join(process.cwd(), 'dist/iiif');
6767

68+
const miradorHtml = join(IIIF_VIEWER, '/mirador/index.html');
69+
6870
const indexHtml = join(DIST_FOLDER, 'index.html');
6971

7072
const cookieParser = require('cookie-parser');
@@ -86,8 +88,10 @@ const _window = domino.createWindow(indexHtml);
8688
// The REST server base URL
8789
const REST_BASE_URL = environment.rest.ssrBaseUrl || environment.rest.baseUrl;
8890

91+
const IIIF_ALLOWED_ORIGINS = environment.rest.allowedOrigins || [];
92+
8993
// Assign the DOM window and document objects to the global object
90-
(_window as any).screen = {deviceXDPI: 0, logicalXDPI: 0};
94+
(_window as any).screen = { deviceXDPI: 0, logicalXDPI: 0 };
9195
(global as any).window = _window;
9296
(global as any).document = _window.document;
9397
(global as any).navigator = _window.navigator;
@@ -231,6 +235,35 @@ export function app() {
231235
*/
232236
router.use('/iiif', express.static(IIIF_VIEWER, { index: false }));
233237

238+
/*
239+
* Adapt headers to allow embedding of IIIF viewer in authorized pages
240+
*/
241+
server.get('/iiif/mirador/index.html', (req, res) => {
242+
const referer = req.headers.referer;
243+
244+
if (referer && !referer.startsWith('/')) {
245+
try {
246+
const origin = new URL(referer).origin;
247+
if (IIIF_ALLOWED_ORIGINS.includes(origin)) {
248+
console.info('Found allowed origin, setting headers for IIIF viewer');
249+
// CORS header
250+
res.setHeader('Access-Control-Allow-Origin', origin);
251+
// CSP for iframe embedding
252+
res.setHeader('Content-Security-Policy', `frame-ancestors ${origin};`);
253+
console.info('Headers have been set ', res.getHeader('Access-Control-Allow-Origin'), res.getHeader('Content-Security-Policy'));
254+
}
255+
} catch (error) {
256+
console.error('An error occurred setting security headers in response:', error.message);
257+
}
258+
}
259+
260+
res.sendFile(miradorHtml, (err) => {
261+
if (err) {
262+
res.status(500).send('Internal Server Error');
263+
}
264+
});
265+
});
266+
234267
/**
235268
* Checking server status
236269
*/
@@ -286,6 +319,11 @@ function serverSideRender(req, res, sendToUser: boolean = true) {
286319
originUrl: environment.ui.baseUrl,
287320
requestUrl: req.originalUrl,
288321
}, (err, data) => {
322+
323+
if (res.writableEnded || res.headersSent || res.finished) {
324+
return;
325+
}
326+
289327
if (hasNoValue(err) && hasValue(data)) {
290328
// Replace REST URL with UI URL
291329
if (environment.universal.replaceRestUrl && REST_BASE_URL !== environment.rest.baseUrl) {
@@ -644,10 +682,10 @@ function start() {
644682
* The callback function to serve client health check requests
645683
*/
646684
function clientHealthCheck(req, res) {
647-
const isServerHealthy = true;
648-
if (isServerHealthy) {
649-
res.status(200).json({ status: 'UP' });
650-
}
685+
const isServerHealthy = true;
686+
if (isServerHealthy) {
687+
res.status(200).json({ status: 'UP' });
688+
}
651689
}
652690

653691
/*
@@ -665,6 +703,8 @@ function healthCheck(req, res) {
665703
});
666704
});
667705
}
706+
707+
668708
// Webpack will replace 'require' with '__webpack_require__'
669709
// '__non_webpack_require__' is a proxy to Node 'require'
670710
// The below code is to ensure that the server is run only when not requiring the bundle.

src/app/bitstream-page/bitstream-download-redirect.guard.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,8 @@ describe('BitstreamDownloadRedirectGuard', () => {
162162
it('should redirect to the content link', waitForAsync(() => {
163163
TestBed.runInInjectionContext(() => {
164164
resolver(route, state).subscribe(() => {
165-
expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link');
166-
}
165+
expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link', null, true);
166+
},
167167
);
168168
});
169169
}));
@@ -176,7 +176,7 @@ describe('BitstreamDownloadRedirectGuard', () => {
176176
it('should redirect to an updated content link', waitForAsync(() => {
177177
TestBed.runInInjectionContext(() => {
178178
resolver(route, state).subscribe(() => {
179-
expect(hardRedirectService.redirect).toHaveBeenCalledWith('content-url-with-headers');
179+
expect(hardRedirectService.redirect).toHaveBeenCalledWith('content-url-with-headers', null, true);
180180
});
181181
});
182182
}));

src/app/bitstream-page/bitstream-download-redirect.guard.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,16 @@ export const bitstreamDownloadRedirectGuard: CanActivateFn = (
6666
}),
6767
map(([isAuthorized, isLoggedIn, bitstream, fileLink]: [boolean, boolean, Bitstream, string]) => {
6868
if (isAuthorized && isLoggedIn && isNotEmpty(fileLink)) {
69-
hardRedirectService.redirect(fileLink);
69+
hardRedirectService.redirect(fileLink, null, true);
7070
return false;
7171
} else if (isAuthorized && !isLoggedIn && !hasValue(accessToken)) {
72-
hardRedirectService.redirect(bitstream._links.content.href);
72+
hardRedirectService.redirect(bitstream._links.content.href, null, true);
7373
return false;
7474
} else if (!isAuthorized) {
7575
// Either we have an access token, or we are logged in, or we are not logged in.
7676
// For now, the access token does not care if we are logged in or not.
7777
if (hasValue(accessToken)) {
78-
hardRedirectService.redirect(bitstream._links.content.href + '?accessToken=' + accessToken);
78+
hardRedirectService.redirect(bitstream._links.content.href + '?accessToken=' + accessToken, null, true);
7979
return false;
8080
} else if (isLoggedIn) {
8181
return router.createUrlTree([getForbiddenRoute()]);

src/app/core/services/hard-redirect.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ export abstract class HardRedirectService {
1313
* the page to redirect to
1414
* @param statusCode
1515
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
16+
* @param shouldSetCorsHeader
17+
* optional to prevent CORS error on redirect
1618
*/
17-
abstract redirect(url: string, statusCode?: number);
19+
abstract redirect(url: string, statusCode?: number, shouldSetCorsHeader?: boolean);
1820

1921
/**
2022
* Get the current route, with query params included

src/app/core/services/server-hard-redirect.service.spec.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,16 @@ describe('ServerHardRedirectService', () => {
88
const mockRequest = jasmine.createSpyObj(['get']);
99
const mockResponse = jasmine.createSpyObj(['redirect', 'end']);
1010

11-
let service: ServerHardRedirectService = new ServerHardRedirectService(environment, mockRequest, mockResponse);
11+
const serverResponseService = jasmine.createSpyObj('ServerResponseService', {
12+
setHeader: jasmine.createSpy('setHeader'),
13+
});
14+
15+
let service: ServerHardRedirectService = new ServerHardRedirectService(environment, mockRequest, mockResponse, serverResponseService);
1216
const origin = 'https://test-host.com:4000';
1317

1418
beforeEach(() => {
1519
mockRequest.protocol = 'https';
20+
mockRequest.path = '/bitstreams/test-uuid/download';
1621
mockRequest.headers = {
1722
host: 'test-host.com:4000',
1823
};
@@ -76,7 +81,7 @@ describe('ServerHardRedirectService', () => {
7681
ssrBaseUrl: 'https://private-url:4000/server',
7782
baseUrl: 'https://public-url/server',
7883
} } };
79-
service = new ServerHardRedirectService(environmentWithSSRUrl, mockRequest, mockResponse);
84+
service = new ServerHardRedirectService(environmentWithSSRUrl, mockRequest, mockResponse, serverResponseService);
8085

8186
beforeEach(() => {
8287
service.redirect(redirect);
@@ -88,4 +93,21 @@ describe('ServerHardRedirectService', () => {
8893
});
8994
});
9095

96+
describe('Should add cors header on download path', () => {
97+
const redirect = 'https://private-url:4000/server/api/bitstreams/uuid';
98+
const environmentWithSSRUrl: any = { ...environment, ...{ ...environment.rest, rest: {
99+
ssrBaseUrl: 'https://private-url:4000/server',
100+
baseUrl: 'https://public-url/server',
101+
} } };
102+
service = new ServerHardRedirectService(environmentWithSSRUrl, mockRequest, mockResponse, serverResponseService);
103+
104+
beforeEach(() => {
105+
service.redirect(redirect, null, true);
106+
});
107+
108+
it('should set header', () => {
109+
expect(serverResponseService.setHeader).toHaveBeenCalled();
110+
});
111+
});
112+
91113
});

src/app/core/services/server-hard-redirect.service.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
44
import { HardRedirectService } from './hard-redirect.service';
55
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
66
import { isNotEmpty } from '../../shared/empty.util';
7+
import { ServerResponseService } from './server-response.service';
78

89
/**
910
* Service for performing hard redirects within the server app module
@@ -15,6 +16,7 @@ export class ServerHardRedirectService extends HardRedirectService {
1516
@Inject(APP_CONFIG) protected appConfig: AppConfig,
1617
@Inject(REQUEST) protected req: Request,
1718
@Inject(RESPONSE) protected res: Response,
19+
private responseService: ServerResponseService,
1820
) {
1921
super();
2022
}
@@ -26,8 +28,9 @@ export class ServerHardRedirectService extends HardRedirectService {
2628
* the page to redirect to
2729
* @param statusCode
2830
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
31+
* @param shouldSetCorsHeader
2932
*/
30-
redirect(url: string, statusCode?: number) {
33+
redirect(url: string, statusCode?: number, shouldSetCorsHeader?: boolean) {
3134
if (url === this.req.url) {
3235
return;
3336
}
@@ -57,6 +60,10 @@ export class ServerHardRedirectService extends HardRedirectService {
5760
status = 302;
5861
}
5962

63+
if (shouldSetCorsHeader) {
64+
this.setCorsHeader();
65+
}
66+
6067
console.info(`Redirecting from ${this.req.url} to ${redirectUrl} with ${status}`);
6168

6269
this.res.redirect(status, redirectUrl);
@@ -83,4 +90,12 @@ export class ServerHardRedirectService extends HardRedirectService {
8390
getCurrentOrigin(): string {
8491
return this.req.protocol + '://' + this.req.headers.host;
8592
}
93+
94+
/**
95+
* Set CORS header to allow embedding of redirected content.
96+
* The actual security header will be set by the rest
97+
*/
98+
setCorsHeader() {
99+
this.responseService.setHeader('Access-Control-Allow-Origin', '*');
100+
}
86101
}

src/config/server-config.interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ export class ServerConfig implements Config {
1010
// This boolean will be automatically set on server startup based on whether "baseUrl" and "ssrBaseUrl"
1111
// have different values.
1212
public hasSsrBaseUrl?: boolean;
13+
public allowedOrigins?: string[];
1314
}

0 commit comments

Comments
 (0)