1 | package edu.illinois.library.cantaloupe;
|
---|
2 |
|
---|
3 | import edu.illinois.library.cantaloupe.config.Configuration;
|
---|
4 | import edu.illinois.library.cantaloupe.config.Key;
|
---|
5 | import edu.illinois.library.cantaloupe.processor.UnsupportedOutputFormatException;
|
---|
6 | import edu.illinois.library.cantaloupe.processor.UnsupportedSourceFormatException;
|
---|
7 | import edu.illinois.library.cantaloupe.resource.AbstractResource;
|
---|
8 | import edu.illinois.library.cantaloupe.resource.IllegalClientArgumentException;
|
---|
9 | import edu.illinois.library.cantaloupe.resource.GSLandingResource;
|
---|
10 | import edu.illinois.library.cantaloupe.resource.TrailingSlashRemovingResource;
|
---|
11 | import edu.illinois.library.cantaloupe.resource.admin.AdminResource;
|
---|
12 | import edu.illinois.library.cantaloupe.resource.admin.StatusResource;
|
---|
13 | import edu.illinois.library.cantaloupe.resource.api.TaskResource;
|
---|
14 | import edu.illinois.library.cantaloupe.resource.api.TasksResource;
|
---|
15 | import edu.illinois.library.cantaloupe.resource.iiif.RedirectingResource;
|
---|
16 | import org.restlet.Application;
|
---|
17 | import org.restlet.Request;
|
---|
18 | import org.restlet.Response;
|
---|
19 | import org.restlet.Restlet;
|
---|
20 | import org.restlet.data.ChallengeScheme;
|
---|
21 | import org.restlet.data.MediaType;
|
---|
22 | import org.restlet.data.Status;
|
---|
23 | import org.restlet.representation.Representation;
|
---|
24 | import org.restlet.resource.Directory;
|
---|
25 | import org.restlet.resource.ResourceException;
|
---|
26 | import org.restlet.routing.Router;
|
---|
27 | import org.restlet.routing.Template;
|
---|
28 | import org.restlet.security.Authenticator;
|
---|
29 | import org.restlet.security.ChallengeAuthenticator;
|
---|
30 | import org.restlet.security.LocalVerifier;
|
---|
31 | import org.restlet.service.CorsService;
|
---|
32 | import org.restlet.service.StatusService;
|
---|
33 |
|
---|
34 | import java.io.FileNotFoundException;
|
---|
35 | import java.io.IOException;
|
---|
36 | import java.io.PrintWriter;
|
---|
37 | import java.io.StringWriter;
|
---|
38 | import java.io.UnsupportedEncodingException;
|
---|
39 | import java.nio.file.AccessDeniedException;
|
---|
40 | import java.nio.file.NoSuchFileException;
|
---|
41 | import java.util.Arrays;
|
---|
42 | import java.util.Collections;
|
---|
43 | import java.util.HashSet;
|
---|
44 | import java.util.List;
|
---|
45 | import java.util.Map;
|
---|
46 | import java.util.logging.Level;
|
---|
47 |
|
---|
48 | /**
|
---|
49 | * Restlet Application implementation. Creates endpoint routes and connects
|
---|
50 | * them to {@link org.restlet.resource.Resource resources}.
|
---|
51 | *
|
---|
52 | * @see <a href="https://restlet.com/open-source/documentation/jdocs/2.3/jse">
|
---|
53 | * Restlet 2.3 JSE Javadoc</a>
|
---|
54 | * @see <a href="https://restlet.com/open-source/documentation/2.3/changelog">
|
---|
55 | * Restlet Change Log</a>
|
---|
56 | */
|
---|
57 | public class GSRestletApplication extends RestletApplication {
|
---|
58 |
|
---|
59 | private static class CustomStatusService extends StatusService {
|
---|
60 |
|
---|
61 | private static final List<MediaType> SUPPORTED_MEDIA_TYPES =
|
---|
62 | Arrays.asList(MediaType.TEXT_PLAIN, MediaType.TEXT_HTML,
|
---|
63 | MediaType.APPLICATION_XHTML);
|
---|
64 |
|
---|
65 | @Override
|
---|
66 | public Representation toRepresentation(Status status,
|
---|
67 | Request request,
|
---|
68 | Response response) {
|
---|
69 | String message = null, stackTrace = null;
|
---|
70 | Throwable throwable = status.getThrowable();
|
---|
71 | if (throwable != null) {
|
---|
72 | if (throwable.getCause() != null) {
|
---|
73 | throwable = throwable.getCause();
|
---|
74 | }
|
---|
75 | message = throwable.getMessage();
|
---|
76 | Configuration config = Configuration.getInstance();
|
---|
77 | if (config.getBoolean(Key.PRINT_STACK_TRACE_ON_ERROR_PAGES, false)) {
|
---|
78 | try (StringWriter sw = new StringWriter()) {
|
---|
79 | throwable.printStackTrace(new PrintWriter(sw));
|
---|
80 | stackTrace = sw.toString();
|
---|
81 | } catch (IOException e) {
|
---|
82 | // We are almost certain to never get here...
|
---|
83 | }
|
---|
84 | }
|
---|
85 | } else if (status.getDescription() != null) {
|
---|
86 | message = status.getDescription();
|
---|
87 | } else if (Status.CLIENT_ERROR_NOT_FOUND.equals(status)) {
|
---|
88 | message = "No resource exists at this URL.";
|
---|
89 | }
|
---|
90 |
|
---|
91 | final Map<String,Object> templateVars =
|
---|
92 | AbstractResource.getCommonTemplateVars(request);
|
---|
93 | templateVars.put("pageTitle", status.getCode() + " " +
|
---|
94 | status.getReasonPhrase());
|
---|
95 | templateVars.put("message", message);
|
---|
96 | templateVars.put("stackTrace", stackTrace);
|
---|
97 |
|
---|
98 | // Negotiate a response representation content type.
|
---|
99 | // Web browsers will usually request `text/html` and
|
---|
100 | // `application/xhtml+xml` in order of priority. In the absence
|
---|
101 | // of either of those, we will prefer to return `text/plain`.
|
---|
102 | MediaType requestedType = request.getClientInfo().
|
---|
103 | getPreferredMediaType(SUPPORTED_MEDIA_TYPES);
|
---|
104 | if (requestedType == null) {
|
---|
105 | requestedType = MediaType.TEXT_PLAIN;
|
---|
106 | }
|
---|
107 |
|
---|
108 | // Use a template that best fits the representation's content type.
|
---|
109 | String template;
|
---|
110 | MediaType mediaType;
|
---|
111 | if (Arrays.asList("text/html", "application/xhtml+xml").
|
---|
112 | contains(requestedType.toString())) {
|
---|
113 | template = "/error.html.vm";
|
---|
114 | mediaType = MediaType.TEXT_HTML;
|
---|
115 | } else {
|
---|
116 | template = "/error.txt.vm";
|
---|
117 | mediaType = MediaType.TEXT_PLAIN;
|
---|
118 | }
|
---|
119 |
|
---|
120 | Representation rep = new AbstractResource() {}.
|
---|
121 | template(template, templateVars);
|
---|
122 | rep.setMediaType(mediaType);
|
---|
123 | return rep;
|
---|
124 | }
|
---|
125 |
|
---|
126 | /**
|
---|
127 | * <p>Returns a {@link Status} appropriate for the given {@link
|
---|
128 | * Throwable}.</p>
|
---|
129 | *
|
---|
130 | * <p>Note that illegal arguments from the client, which would be
|
---|
131 | * intended to produce a 400 status, should be represented
|
---|
132 | * by {@link IllegalClientArgumentException}. In contrast, {@link
|
---|
133 | * IllegalArgumentException} will produce a 500 response.</p>
|
---|
134 | */
|
---|
135 | @Override
|
---|
136 | public Status toStatus(Throwable t,
|
---|
137 | Request request,
|
---|
138 | Response response) {
|
---|
139 | Status status;
|
---|
140 | t = (t.getCause() != null) ? t.getCause() : t;
|
---|
141 |
|
---|
142 | if (t instanceof ResourceException) {
|
---|
143 | status = ((ResourceException) t).getStatus();
|
---|
144 | } else if (t instanceof IllegalClientArgumentException ||
|
---|
145 | t instanceof UnsupportedEncodingException) {
|
---|
146 | status = new Status(Status.CLIENT_ERROR_BAD_REQUEST, t);
|
---|
147 | } else if (t instanceof UnsupportedOutputFormatException) {
|
---|
148 | status = new Status(Status.CLIENT_ERROR_UNSUPPORTED_MEDIA_TYPE, t);
|
---|
149 | } else if (t instanceof FileNotFoundException ||
|
---|
150 | t instanceof NoSuchFileException) {
|
---|
151 | status = new Status(Status.CLIENT_ERROR_NOT_FOUND, t);
|
---|
152 | } else if (t instanceof AccessDeniedException) {
|
---|
153 | status = new Status(Status.CLIENT_ERROR_FORBIDDEN, t);
|
---|
154 | } else if (t instanceof UnsupportedSourceFormatException) {
|
---|
155 | status = new Status(Status.SERVER_ERROR_NOT_IMPLEMENTED, t);
|
---|
156 | } else {
|
---|
157 | status = new Status(Status.SERVER_ERROR_INTERNAL, t);
|
---|
158 | }
|
---|
159 | return status;
|
---|
160 | }
|
---|
161 |
|
---|
162 | }
|
---|
163 |
|
---|
164 | /**
|
---|
165 | * Verifies given user credentials against a single set of stored user
|
---|
166 | * credentials in the application configuration.
|
---|
167 | */
|
---|
168 | private static class ConfigurationVerifier extends LocalVerifier {
|
---|
169 |
|
---|
170 | private final Key userKey;
|
---|
171 | private final Key secretKey;
|
---|
172 |
|
---|
173 | /**
|
---|
174 | * @param userKey Key under which the username is stored.
|
---|
175 | * @param secretKey Key under which the secret is stored.
|
---|
176 | */
|
---|
177 | ConfigurationVerifier(Key userKey, Key secretKey) {
|
---|
178 | this.userKey = userKey;
|
---|
179 | this.secretKey = secretKey;
|
---|
180 | }
|
---|
181 |
|
---|
182 | @Override
|
---|
183 | public char[] getLocalSecret(String givenUser) {
|
---|
184 | final Configuration config = Configuration.getInstance();
|
---|
185 | if (config.getString(userKey).equals(givenUser)) {
|
---|
186 | return config.getString(secretKey).toCharArray();
|
---|
187 | }
|
---|
188 | return null;
|
---|
189 | }
|
---|
190 |
|
---|
191 | /**
|
---|
192 | * Overrides super to disallow an empty stored secret.
|
---|
193 | */
|
---|
194 | @Override
|
---|
195 | public int verify(String identifier, char[] secret) {
|
---|
196 | final Configuration config = Configuration.getInstance();
|
---|
197 | final String configSecret = config.getString(secretKey);
|
---|
198 | if (configSecret == null || configSecret.isEmpty()) {
|
---|
199 | return RESULT_INVALID;
|
---|
200 | }
|
---|
201 | return super.verify(identifier, secret);
|
---|
202 | }
|
---|
203 |
|
---|
204 | }
|
---|
205 |
|
---|
206 | public static final String ADMIN_PATH = "/admin";
|
---|
207 | public static final String ADMIN_CONFIG_PATH = "/admin/configuration";
|
---|
208 | public static final String ADMIN_STATUS_PATH = "/status";
|
---|
209 | public static final String CONFIGURATION_PATH = "/configuration";
|
---|
210 | public static final String IIIF_PATH = "/iiif";
|
---|
211 | public static final String IIIF_1_PATH = "/iiif/1";
|
---|
212 | public static final String IIIF_2_PATH = "/iiif/2";
|
---|
213 | public static final String STATIC_ROOT_PATH = "/static";
|
---|
214 | public static final String TASKS_PATH = "/tasks";
|
---|
215 |
|
---|
216 | public static final String ADMIN_REALM =
|
---|
217 | edu.illinois.library.cantaloupe.Application.getName() + " Control Panel";
|
---|
218 | public static final String API_REALM =
|
---|
219 | edu.illinois.library.cantaloupe.Application.getName() + " API Realm";
|
---|
220 | public static final String PUBLIC_REALM = "Image Realm";
|
---|
221 |
|
---|
222 | public GSRestletApplication() {
|
---|
223 | super();
|
---|
224 |
|
---|
225 | // Tell Restlet to use a custom status service for transforming
|
---|
226 | // uncaught exceptions into error responses.
|
---|
227 | setStatusService(new CustomStatusService());
|
---|
228 |
|
---|
229 | // Enable CORS.
|
---|
230 | // See: http://restlet.com/blog/2015/12/15/understanding-and-using-cors/
|
---|
231 | CorsService corsService = new CorsService();
|
---|
232 | corsService.setAllowedOrigins(new HashSet<>(Collections.singletonList("*")));
|
---|
233 | corsService.setAllowedCredentials(true);
|
---|
234 | getServices().add(corsService);
|
---|
235 |
|
---|
236 | // Disable support for ranging. This will tell Restlet not to honor the
|
---|
237 | // Range request header, as well as not to send an Accept-Ranges header.
|
---|
238 | getRangeService().setEnabled(false);
|
---|
239 | }
|
---|
240 |
|
---|
241 | private Authenticator newAdminAuthenticator() {
|
---|
242 | ChallengeAuthenticator auth = new ChallengeAuthenticator(
|
---|
243 | getContext(), ChallengeScheme.HTTP_BASIC, ADMIN_REALM);
|
---|
244 | auth.setVerifier(new ConfigurationVerifier(
|
---|
245 | Key.ADMIN_USERNAME, Key.ADMIN_SECRET));
|
---|
246 | return auth;
|
---|
247 | }
|
---|
248 |
|
---|
249 | private Authenticator newAPIAuthenticator() {
|
---|
250 | ChallengeAuthenticator auth = new ChallengeAuthenticator(
|
---|
251 | getContext(), ChallengeScheme.HTTP_BASIC, API_REALM);
|
---|
252 | auth.setVerifier(new ConfigurationVerifier(
|
---|
253 | Key.API_USERNAME, Key.API_SECRET));
|
---|
254 | return auth;
|
---|
255 | }
|
---|
256 |
|
---|
257 | private Authenticator newPublicEndpointAuthenticator() {
|
---|
258 | final Configuration config = Configuration.getInstance();
|
---|
259 |
|
---|
260 | if (config.getBoolean(Key.BASIC_AUTH_ENABLED, false)) {
|
---|
261 | getLogger().log(Level.INFO,
|
---|
262 | "Enabling HTTP Basic authentication for public endpoints");
|
---|
263 |
|
---|
264 | final ChallengeAuthenticator auth = new ChallengeAuthenticator(
|
---|
265 | getContext(), ChallengeScheme.HTTP_BASIC, PUBLIC_REALM) {
|
---|
266 | @Override
|
---|
267 | protected int beforeHandle(Request request, Response response) {
|
---|
268 | final String path = request.getResourceRef().getPath();
|
---|
269 | if (path.startsWith(IIIF_PATH)) {
|
---|
270 | return super.beforeHandle(request, response);
|
---|
271 | }
|
---|
272 | response.setStatus(Status.SUCCESS_OK);
|
---|
273 | return CONTINUE;
|
---|
274 | }
|
---|
275 | };
|
---|
276 |
|
---|
277 | auth.setVerifier(new ConfigurationVerifier(
|
---|
278 | Key.BASIC_AUTH_USERNAME, Key.BASIC_AUTH_SECRET));
|
---|
279 | return auth;
|
---|
280 | } else {
|
---|
281 | getLogger().info("Public endpoint authentication is disabled (" +
|
---|
282 | Key.BASIC_AUTH_ENABLED + " = false)");
|
---|
283 | }
|
---|
284 | return null;
|
---|
285 | }
|
---|
286 |
|
---|
287 | /**
|
---|
288 | * Creates a root Restlet that will receive all incoming calls.
|
---|
289 | *
|
---|
290 | * @see <a href="http://iiif.io/api/image/2.0/#uri-syntax">URI Syntax</a>
|
---|
291 | */
|
---|
292 | @Override
|
---|
293 | public Restlet createInboundRoot() {
|
---|
294 | final Router router = new Router(getContext());
|
---|
295 | router.setDefaultMatchingMode(Template.MODE_EQUALS);
|
---|
296 |
|
---|
297 | ////////////////////// IIIF Image API routes ///////////////////////
|
---|
298 |
|
---|
299 | // Redirect IIIF_PATH/ to IIIF_PATH
|
---|
300 | router.attach(IIIF_PATH + "/", TrailingSlashRemovingResource.class);
|
---|
301 |
|
---|
302 | // Redirect IIIF_PATH to IIIF_2_PATH
|
---|
303 | router.attach(IIIF_PATH, RedirectingResource.class);
|
---|
304 |
|
---|
305 | //////////////////// IIIF Image API 1.x routes /////////////////////
|
---|
306 |
|
---|
307 | // landing page
|
---|
308 | router.attach(IIIF_1_PATH,
|
---|
309 | edu.illinois.library.cantaloupe.resource.iiif.v1.LandingResource.class);
|
---|
310 |
|
---|
311 | // Redirect IIIF_1_PATH/ to IIIF_1_PATH
|
---|
312 | router.attach(IIIF_1_PATH + "/", TrailingSlashRemovingResource.class);
|
---|
313 |
|
---|
314 | // image request
|
---|
315 | router.attach(IIIF_1_PATH + "/{identifier}/{region}/{size}/{rotation}/{quality_format}",
|
---|
316 | edu.illinois.library.cantaloupe.resource.iiif.v1.ImageResource.class);
|
---|
317 |
|
---|
318 | // information request
|
---|
319 | router.attach(IIIF_1_PATH + "/{identifier}",
|
---|
320 | edu.illinois.library.cantaloupe.resource.iiif.v1.InformationResource.RedirectingResource.class);
|
---|
321 | router.attach(IIIF_1_PATH + "/{identifier}/info.json",
|
---|
322 | edu.illinois.library.cantaloupe.resource.iiif.v1.InformationResource.class);
|
---|
323 |
|
---|
324 | //////////////////// IIIF Image API 2.x routes /////////////////////
|
---|
325 |
|
---|
326 | // landing page
|
---|
327 | router.attach(IIIF_2_PATH,
|
---|
328 | edu.illinois.library.cantaloupe.resource.iiif.v2.LandingResource.class);
|
---|
329 |
|
---|
330 | // Redirect IIIF_2_PATH/ to IIIF_2_PATH
|
---|
331 | router.attach(IIIF_2_PATH + "/", TrailingSlashRemovingResource.class);
|
---|
332 |
|
---|
333 | // image request
|
---|
334 | router.attach(IIIF_2_PATH + "/{identifier}/{region}/{size}/{rotation}/{quality}.{format}",
|
---|
335 | edu.illinois.library.cantaloupe.resource.iiif.v2.GSImageResource.class);
|
---|
336 |
|
---|
337 | // information request
|
---|
338 | router.attach(IIIF_2_PATH + "/{identifier}",
|
---|
339 | edu.illinois.library.cantaloupe.resource.iiif.v2.GSInformationResource.RedirectingResource.class);
|
---|
340 | router.attach(IIIF_2_PATH + "/{identifier}/info.json",
|
---|
341 | edu.illinois.library.cantaloupe.resource.iiif.v2.GSInformationResource.class);
|
---|
342 |
|
---|
343 | ////////////////////////// Admin routes ///////////////////////////
|
---|
344 |
|
---|
345 | Authenticator adminAuth = newAdminAuthenticator();
|
---|
346 | adminAuth.setNext(AdminResource.class);
|
---|
347 | router.attach(ADMIN_PATH, adminAuth);
|
---|
348 |
|
---|
349 | adminAuth = newAdminAuthenticator();
|
---|
350 | adminAuth.setNext(edu.illinois.library.cantaloupe.resource.admin.ConfigurationResource.class);
|
---|
351 | router.attach(ADMIN_CONFIG_PATH, adminAuth);
|
---|
352 |
|
---|
353 | adminAuth = newAdminAuthenticator();
|
---|
354 | adminAuth.setNext(StatusResource.class);
|
---|
355 | router.attach(ADMIN_STATUS_PATH, adminAuth);
|
---|
356 |
|
---|
357 | /////////////////////////// API routes ////////////////////////////
|
---|
358 |
|
---|
359 | Authenticator apiAuth = newAPIAuthenticator();
|
---|
360 | apiAuth.setNext(edu.illinois.library.cantaloupe.resource.api.ConfigurationResource.class);
|
---|
361 | router.attach(CONFIGURATION_PATH, apiAuth);
|
---|
362 |
|
---|
363 | apiAuth = newAPIAuthenticator();
|
---|
364 | apiAuth.setNext(TasksResource.class);
|
---|
365 | router.attach(TASKS_PATH, apiAuth);
|
---|
366 |
|
---|
367 | apiAuth = newAPIAuthenticator();
|
---|
368 | apiAuth.setNext(TaskResource.class);
|
---|
369 | router.attach(TASKS_PATH + "/{uuid}", apiAuth);
|
---|
370 |
|
---|
371 | ////////////////////////// Other routes ///////////////////////////
|
---|
372 |
|
---|
373 | // Application landing page
|
---|
374 | router.attach("/", GSLandingResource.class);
|
---|
375 |
|
---|
376 | // Hook up the static file server (for images, CSS, & scripts)
|
---|
377 | // This uses Restlet's "CLAP" (Class Loader Access Protocol) mechanism
|
---|
378 | // which must be enabled in web.xml.
|
---|
379 | final Directory dir = new Directory(
|
---|
380 | getContext(), "clap://resources/public_html/");
|
---|
381 | dir.setDeeplyAccessible(true);
|
---|
382 | dir.setListingAllowed(false);
|
---|
383 | dir.setNegotiatingContent(false);
|
---|
384 | router.attach(STATIC_ROOT_PATH, dir);
|
---|
385 |
|
---|
386 | // Hook up public endpoint authentication
|
---|
387 | Authenticator endpointAuth = newPublicEndpointAuthenticator();
|
---|
388 | if (endpointAuth != null) {
|
---|
389 | endpointAuth.setNext(router);
|
---|
390 | return endpointAuth;
|
---|
391 | }
|
---|
392 |
|
---|
393 | return router;
|
---|
394 | }
|
---|
395 |
|
---|
396 | }
|
---|