Skip to content

Commit 0b1462c

Browse files
FrancescoMolinaroatarix83
authored andcommitted
Merged in task/main-cris/IIIF-188 (pull request DSpace#3182)
Task/main cris/IIIF-188 Approved-by: Giuseppe Digilio
2 parents e2f8c4e + 6d4d2f1 commit 0b1462c

File tree

7 files changed

+93
-14
lines changed

7 files changed

+93
-14
lines changed

server.ts

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

70+
const miradorHtml = join(IIIF_VIEWER, '/mirador/index.html');
71+
7072
const indexHtml = join(DIST_FOLDER, 'index.html');
7173

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

93+
const IIIF_ALLOWED_ORIGINS = environment.rest.allowedOrigins || [];
94+
9195
// Assign the DOM window and document objects to the global object
92-
(_window as any).screen = {deviceXDPI: 0, logicalXDPI: 0};
96+
(_window as any).screen = { deviceXDPI: 0, logicalXDPI: 0 };
9397
(global as any).window = _window;
9498
(global as any).document = _window.document;
9599
(global as any).navigator = _window.navigator;
@@ -211,6 +215,35 @@ export function app() {
211215
*/
212216
router.use('/iiif', express.static(IIIF_VIEWER, { index: false }));
213217

218+
/*
219+
* Adapt headers to allow embedding of IIIF viewer in authorized pages
220+
*/
221+
server.get('/iiif/mirador/index.html', (req, res) => {
222+
const referer = req.headers.referer;
223+
224+
if (referer && !referer.startsWith('/')) {
225+
try {
226+
const origin = new URL(referer).origin;
227+
if (IIIF_ALLOWED_ORIGINS.includes(origin)) {
228+
console.info('Found allowed origin, setting headers for IIIF viewer');
229+
// CORS header
230+
res.setHeader('Access-Control-Allow-Origin', origin);
231+
// CSP for iframe embedding
232+
res.setHeader('Content-Security-Policy', `frame-ancestors ${origin};`);
233+
console.info('Headers have been set ', res.getHeader('Access-Control-Allow-Origin'), res.getHeader('Content-Security-Policy'));
234+
}
235+
} catch (error) {
236+
console.error('An error occurred setting security headers in response:', error);
237+
}
238+
}
239+
240+
res.sendFile(miradorHtml, (err) => {
241+
if (err) {
242+
res.status(500).send('Internal Server Error');
243+
}
244+
});
245+
});
246+
214247
/**
215248
* Checking server status
216249
*/
@@ -283,6 +316,10 @@ function serverSideRender(req, res, next, sendToUser: boolean = true) {
283316
],
284317
})
285318
.then((html) => {
319+
if (res.writableEnded || res.headersSent || res.finished) {
320+
return;
321+
}
322+
286323
if (hasValue(html)) {
287324
// Replace REST URL with UI URL
288325
if (environment.ssr.replaceRestUrl && REST_BASE_URL !== environment.rest.baseUrl) {
@@ -646,10 +683,10 @@ function start() {
646683
* The callback function to serve client health check requests
647684
*/
648685
function clientHealthCheck(req, res) {
649-
const isServerHealthy = true;
650-
if (isServerHealthy) {
651-
res.status(200).json({ status: 'UP' });
652-
}
686+
const isServerHealthy = true;
687+
if (isServerHealthy) {
688+
res.status(200).json({ status: 'UP' });
689+
}
653690
}
654691

655692
/*
@@ -667,6 +704,8 @@ function healthCheck(req, res) {
667704
});
668705
});
669706
}
707+
708+
670709
// Webpack will replace 'require' with '__webpack_require__'
671710
// '__non_webpack_require__' is a proxy to Node 'require'
672711
// 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: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ describe('BitstreamDownloadRedirectGuard', () => {
169169
it('should redirect to the content link', waitForAsync(() => {
170170
TestBed.runInInjectionContext(() => {
171171
resolver(route, state).subscribe(() => {
172-
expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link');
172+
expect(hardRedirectService.redirect).toHaveBeenCalledWith('bitstream-content-link', null, true);
173173
},
174174
);
175175
});
@@ -183,7 +183,7 @@ describe('BitstreamDownloadRedirectGuard', () => {
183183
it('should redirect to an updated content link', waitForAsync(() => {
184184
TestBed.runInInjectionContext(() => {
185185
resolver(route, state).subscribe(() => {
186-
expect(hardRedirectService.redirect).toHaveBeenCalledWith('content-url-with-headers');
186+
expect(hardRedirectService.redirect).toHaveBeenCalledWith('content-url-with-headers', null, true);
187187
});
188188
});
189189
}));

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,16 +78,16 @@ export const bitstreamDownloadRedirectGuard: CanActivateFn = (
7878
}),
7979
map(([isAuthorized, isLoggedIn, bitstream, fileLink]: [boolean, boolean, Bitstream, string]) => {
8080
if (isAuthorized && isLoggedIn && isNotEmpty(fileLink)) {
81-
hardRedirectService.redirect(fileLink);
81+
hardRedirectService.redirect(fileLink, null, true);
8282
return false;
8383
} else if (isAuthorized && !isLoggedIn && !hasValue(accessToken)) {
84-
hardRedirectService.redirect(bitstream._links.content.href);
84+
hardRedirectService.redirect(bitstream._links.content.href, null, true);
8585
return false;
8686
} else if (!isAuthorized) {
8787
// Either we have an access token, or we are logged in, or we are not logged in.
8888
// For now, the access token does not care if we are logged in or not.
8989
if (hasValue(accessToken)) {
90-
hardRedirectService.redirect(bitstream._links.content.href + '?accessToken=' + accessToken);
90+
hardRedirectService.redirect(bitstream._links.content.href + '?accessToken=' + accessToken, null, true);
9191
return false;
9292
} else if (isLoggedIn) {
9393
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
@@ -17,6 +17,7 @@ import {
1717
} from '../../../express.tokens';
1818
import { isNotEmpty } from '../../shared/empty.util';
1919
import { HardRedirectService } from './hard-redirect.service';
20+
import { ServerResponseService } from './server-response.service';
2021

2122
/**
2223
* Service for performing hard redirects within the server app module
@@ -28,6 +29,7 @@ export class ServerHardRedirectService extends HardRedirectService {
2829
@Inject(APP_CONFIG) protected appConfig: AppConfig,
2930
@Inject(REQUEST) protected req: Request,
3031
@Inject(RESPONSE) protected res: Response,
32+
private responseService: ServerResponseService,
3133
) {
3234
super();
3335
}
@@ -39,8 +41,9 @@ export class ServerHardRedirectService extends HardRedirectService {
3941
* the page to redirect to
4042
* @param statusCode
4143
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
44+
* @param shouldSetCorsHeader
4245
*/
43-
redirect(url: string, statusCode?: number) {
46+
redirect(url: string, statusCode?: number, shouldSetCorsHeader?: boolean) {
4447
if (url === this.req.url) {
4548
return;
4649
}
@@ -70,6 +73,10 @@ export class ServerHardRedirectService extends HardRedirectService {
7073
status = 302;
7174
}
7275

76+
if (shouldSetCorsHeader) {
77+
this.setCorsHeader();
78+
}
79+
7380
console.info(`Redirecting from ${this.req.url} to ${redirectUrl} with ${status}`);
7481

7582
this.res.redirect(status, redirectUrl);
@@ -96,4 +103,12 @@ export class ServerHardRedirectService extends HardRedirectService {
96103
getCurrentOrigin(): string {
97104
return this.req.protocol + '://' + this.req.headers.host;
98105
}
106+
107+
/**
108+
* Set CORS header to allow embedding of redirected content.
109+
* The actual security header will be set by the rest
110+
*/
111+
setCorsHeader() {
112+
this.responseService.setHeader('Access-Control-Allow-Origin', '*');
113+
}
99114
}

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)