source: gs3-extensions/iiif-servlet/trunk/src/src/main/java/edu/illinois/library/cantaloupe/GSRestletApplication.java@ 32877

Last change on this file since 32877 was 32877, checked in by davidb, 5 years ago

GSImageResource added in to the mix

File size: 16.4 KB
Line 
1package edu.illinois.library.cantaloupe;
2
3import edu.illinois.library.cantaloupe.config.Configuration;
4import edu.illinois.library.cantaloupe.config.Key;
5import edu.illinois.library.cantaloupe.processor.UnsupportedOutputFormatException;
6import edu.illinois.library.cantaloupe.processor.UnsupportedSourceFormatException;
7import edu.illinois.library.cantaloupe.resource.AbstractResource;
8import edu.illinois.library.cantaloupe.resource.IllegalClientArgumentException;
9import edu.illinois.library.cantaloupe.resource.GSLandingResource;
10import edu.illinois.library.cantaloupe.resource.TrailingSlashRemovingResource;
11import edu.illinois.library.cantaloupe.resource.admin.AdminResource;
12import edu.illinois.library.cantaloupe.resource.admin.StatusResource;
13import edu.illinois.library.cantaloupe.resource.api.TaskResource;
14import edu.illinois.library.cantaloupe.resource.api.TasksResource;
15import edu.illinois.library.cantaloupe.resource.iiif.RedirectingResource;
16import org.restlet.Application;
17import org.restlet.Request;
18import org.restlet.Response;
19import org.restlet.Restlet;
20import org.restlet.data.ChallengeScheme;
21import org.restlet.data.MediaType;
22import org.restlet.data.Status;
23import org.restlet.representation.Representation;
24import org.restlet.resource.Directory;
25import org.restlet.resource.ResourceException;
26import org.restlet.routing.Router;
27import org.restlet.routing.Template;
28import org.restlet.security.Authenticator;
29import org.restlet.security.ChallengeAuthenticator;
30import org.restlet.security.LocalVerifier;
31import org.restlet.service.CorsService;
32import org.restlet.service.StatusService;
33
34import java.io.FileNotFoundException;
35import java.io.IOException;
36import java.io.PrintWriter;
37import java.io.StringWriter;
38import java.io.UnsupportedEncodingException;
39import java.nio.file.AccessDeniedException;
40import java.nio.file.NoSuchFileException;
41import java.util.Arrays;
42import java.util.Collections;
43import java.util.HashSet;
44import java.util.List;
45import java.util.Map;
46import 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 */
57public 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}
Note: See TracBrowser for help on using the repository browser.