From 768b4c078daa7b8844dbd4ef761d7a795937ba0a Mon Sep 17 00:00:00 2001 From: "Mark A. Matney, Jr" Date: Wed, 30 Mar 2022 10:08:38 -0700 Subject: [PATCH] Allow sending info.json XHR with Authorization header (#574) Web browser clients implementing IIIF Authentication API 1.0 may send info.json requests with an Authorization header via XMLHttpRequest (XHR). Such requests are "pre-flighted", and the pre-flight response must explicitly state that the Authorization header is allowed in order for the browser to proceed with the request. --- .github/workflows/ci.yml.orig | 56 ------------------- .../cantaloupe/resource/AbstractResource.java | 5 +- .../resource/iiif/v1/InformationResource.java | 19 +++++++ .../resource/iiif/v2/InformationResource.java | 18 ++++++ .../resource/iiif/v3/InformationResource.java | 18 ++++++ .../iiif/v1/InformationResourceTest.java | 5 ++ .../iiif/v2/InformationResourceTest.java | 5 ++ .../iiif/v3/InformationResourceTest.java | 5 ++ 8 files changed, 74 insertions(+), 57 deletions(-) delete mode 100644 .github/workflows/ci.yml.orig diff --git a/.github/workflows/ci.yml.orig b/.github/workflows/ci.yml.orig deleted file mode 100644 index 83de42ad9..000000000 --- a/.github/workflows/ci.yml.orig +++ /dev/null @@ -1,56 +0,0 @@ -name: CI -on: - push: - branches: - - develop - - release/* - pull_request: -jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest] - java: [jdk11, jdk18, graalvm] - fail-fast: false - steps: - - name: Check out the repository - uses: actions/checkout@v4 - - name: Test in Linux JDK 11 - if: matrix.os == 'ubuntu-latest' && matrix.java == 'jdk11' -<<<<<<< HEAD - run: docker compose -f docker/Linux-JDK11/docker-compose.yml up --build --exit-code-from cantaloupe - - name: Test in Linux JDK 18 - if: matrix.os == 'ubuntu-latest' && matrix.java == 'jdk18' - run: docker compose -f docker/Linux-JDK18/docker-compose.yml up --build --exit-code-from cantaloupe - - name: Test in Linux GraalVM - if: matrix.os == 'ubuntu-latest' && matrix.java == 'graalvm' - run: docker compose -f docker/Linux-GraalVM20/docker-compose.yml up --build --exit-code-from cantaloupe - - name: Test in Windows JDK 11 - if: matrix.os == 'windows-latest' && matrix.java == 'jdk11' - run: docker compose -f docker/Windows-JDK11/docker-compose.yml up --build --exit-code-from cantaloupe - - name: Test in Windows JDK 18 - if: matrix.os == 'windows-latest' && matrix.java == 'jdk18' - run: docker compose -f docker/Windows-JDK18/docker-compose.yml up --build --exit-code-from cantaloupe -======= - run: docker-compose -f docker/Linux-JDK11/docker-compose.yml up --build --exit-code-from cantaloupe - - name: Test in Linux JDK 15 - if: matrix.os == 'ubuntu-latest' && matrix.java == 'jdk15' - run: docker-compose -f docker/Linux-JDK15/docker-compose.yml up --build --exit-code-from cantaloupe - - name: Test in Linux JDK 16 - if: matrix.os == 'ubuntu-latest' && matrix.java == 'jdk16' - run: docker-compose -f docker/Linux-JDK16/docker-compose.yml up --build --exit-code-from cantaloupe - - name: Test in Linux GraalVM - if: matrix.os == 'ubuntu-latest' && matrix.java == 'graalvm' - run: docker-compose -f docker/Linux-GraalVM20/docker-compose.yml up --build --exit-code-from cantaloupe - - name: Test in Windows JDK 11 - if: matrix.os == 'windows-latest' && matrix.java == 'jdk11' - run: docker-compose -f docker/Windows-JDK11/docker-compose.yml up --build --exit-code-from cantaloupe - - name: Test in Windows JDK 15 - if: matrix.os == 'windows-latest' && matrix.java == 'jdk15' - run: docker-compose -f docker/Windows-JDK15/docker-compose.yml up --build --exit-code-from cantaloupe - - name: Test in Windows JDK 16 - if: matrix.os == 'windows-latest' && matrix.java == 'jdk16' - run: docker-compose -f docker/Windows-JDK16/docker-compose.yml up --build --exit-code-from cantaloupe ->>>>>>> c540e8d10 (Add --build arguments to docker compose invocations) - # TODO: Windows+GraalVM diff --git a/src/main/java/edu/illinois/library/cantaloupe/resource/AbstractResource.java b/src/main/java/edu/illinois/library/cantaloupe/resource/AbstractResource.java index 7b2c04e8d..91dee3a78 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/resource/AbstractResource.java +++ b/src/main/java/edu/illinois/library/cantaloupe/resource/AbstractResource.java @@ -198,7 +198,10 @@ public void doHEAD() throws Exception { doGET(); } - final void doOPTIONS() { + /** + * May be overridden by implementations that support {@literal OPTIONS}. + */ + protected void doOPTIONS() { Method[] methods = getSupportedMethods(); if (methods.length > 0) { response.setStatus(Status.NO_CONTENT.getCode()); diff --git a/src/main/java/edu/illinois/library/cantaloupe/resource/iiif/v1/InformationResource.java b/src/main/java/edu/illinois/library/cantaloupe/resource/iiif/v1/InformationResource.java index 4a4c776de..ebc5b7415 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/resource/iiif/v1/InformationResource.java +++ b/src/main/java/edu/illinois/library/cantaloupe/resource/iiif/v1/InformationResource.java @@ -1,10 +1,12 @@ package edu.illinois.library.cantaloupe.resource.iiif.v1; +import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import edu.illinois.library.cantaloupe.http.Method; import edu.illinois.library.cantaloupe.http.Status; @@ -18,6 +20,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.servlet.http.HttpServletResponse; + /** * Handles IIIF Image API 1.x information requests. * @@ -42,6 +46,21 @@ public Method[] getSupportedMethods() { return SUPPORTED_METHODS; } + @Override + protected final void doOPTIONS() { + HttpServletResponse response = getResponse(); + Method[] methods = getSupportedMethods(); + if (methods.length > 0) { + response.setStatus(Status.NO_CONTENT.getCode()); + response.setHeader("Access-Control-Allow-Headers", "Authorization"); + response.setHeader("Allow", Arrays.stream(methods) + .map(Method::toString) + .collect(Collectors.joining(","))); + } else { + response.setStatus(Status.METHOD_NOT_ALLOWED.getCode()); + } + } + /** * Writes a JSON-serialized {@link Information} instance to the response. */ diff --git a/src/main/java/edu/illinois/library/cantaloupe/resource/iiif/v2/InformationResource.java b/src/main/java/edu/illinois/library/cantaloupe/resource/iiif/v2/InformationResource.java index 19c695694..c305761f2 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/resource/iiif/v2/InformationResource.java +++ b/src/main/java/edu/illinois/library/cantaloupe/resource/iiif/v2/InformationResource.java @@ -1,10 +1,12 @@ package edu.illinois.library.cantaloupe.resource.iiif.v2; +import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import edu.illinois.library.cantaloupe.http.Method; import edu.illinois.library.cantaloupe.http.Status; @@ -20,6 +22,7 @@ import org.slf4j.LoggerFactory; import javax.script.ScriptException; +import javax.servlet.http.HttpServletResponse; /** * Handles information requests. @@ -45,6 +48,21 @@ public Method[] getSupportedMethods() { return SUPPORTED_METHODS; } + @Override + protected final void doOPTIONS() { + HttpServletResponse response = getResponse(); + Method[] methods = getSupportedMethods(); + if (methods.length > 0) { + response.setStatus(Status.NO_CONTENT.getCode()); + response.setHeader("Access-Control-Allow-Headers", "Authorization"); + response.setHeader("Allow", Arrays.stream(methods) + .map(Method::toString) + .collect(Collectors.joining(","))); + } else { + response.setStatus(Status.METHOD_NOT_ALLOWED.getCode()); + } + } + /** * Writes a JSON-serialized {@link Information} instance to the response. */ diff --git a/src/main/java/edu/illinois/library/cantaloupe/resource/iiif/v3/InformationResource.java b/src/main/java/edu/illinois/library/cantaloupe/resource/iiif/v3/InformationResource.java index 125130cf9..e1c9ed897 100644 --- a/src/main/java/edu/illinois/library/cantaloupe/resource/iiif/v3/InformationResource.java +++ b/src/main/java/edu/illinois/library/cantaloupe/resource/iiif/v3/InformationResource.java @@ -1,10 +1,12 @@ package edu.illinois.library.cantaloupe.resource.iiif.v3; +import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import edu.illinois.library.cantaloupe.http.Method; import edu.illinois.library.cantaloupe.http.Status; @@ -20,6 +22,7 @@ import org.slf4j.LoggerFactory; import javax.script.ScriptException; +import javax.servlet.http.HttpServletResponse; /** * Handles IIIF Image API 3.x information requests. @@ -45,6 +48,21 @@ public Method[] getSupportedMethods() { return SUPPORTED_METHODS; } + @Override + protected final void doOPTIONS() { + HttpServletResponse response = getResponse(); + Method[] methods = getSupportedMethods(); + if (methods.length > 0) { + response.setStatus(Status.NO_CONTENT.getCode()); + response.setHeader("Access-Control-Allow-Headers", "Authorization"); + response.setHeader("Allow", Arrays.stream(methods) + .map(Method::toString) + .collect(Collectors.joining(","))); + } else { + response.setStatus(Status.METHOD_NOT_ALLOWED.getCode()); + } + } + /** * Writes a JSON-serialized {@link Information} instance to the response. */ diff --git a/src/test/java/edu/illinois/library/cantaloupe/resource/iiif/v1/InformationResourceTest.java b/src/test/java/edu/illinois/library/cantaloupe/resource/iiif/v1/InformationResourceTest.java index af9dfa9db..251d2526d 100644 --- a/src/test/java/edu/illinois/library/cantaloupe/resource/iiif/v1/InformationResourceTest.java +++ b/src/test/java/edu/illinois/library/cantaloupe/resource/iiif/v1/InformationResourceTest.java @@ -603,6 +603,11 @@ void testOPTIONSWhenEnabled() throws Exception { assertEquals(2, methods.size()); assertTrue(methods.contains("GET")); assertTrue(methods.contains("OPTIONS")); + + List allowedHeaders = + List.of(StringUtils.split(headers.getFirstValue("Access-Control-Allow-Headers"), ", ")); + assertEquals(1, allowedHeaders.size()); + assertTrue(allowedHeaders.contains("Authorization")); } @Test diff --git a/src/test/java/edu/illinois/library/cantaloupe/resource/iiif/v2/InformationResourceTest.java b/src/test/java/edu/illinois/library/cantaloupe/resource/iiif/v2/InformationResourceTest.java index 021d93927..a8b6e2b0a 100644 --- a/src/test/java/edu/illinois/library/cantaloupe/resource/iiif/v2/InformationResourceTest.java +++ b/src/test/java/edu/illinois/library/cantaloupe/resource/iiif/v2/InformationResourceTest.java @@ -623,6 +623,11 @@ void testOPTIONSWhenEnabled() throws Exception { assertEquals(2, methods.size()); assertTrue(methods.contains("GET")); assertTrue(methods.contains("OPTIONS")); + + List allowedHeaders = + List.of(StringUtils.split(headers.getFirstValue("Access-Control-Allow-Headers"), ", ")); + assertEquals(1, allowedHeaders.size()); + assertTrue(allowedHeaders.contains("Authorization")); } @Test diff --git a/src/test/java/edu/illinois/library/cantaloupe/resource/iiif/v3/InformationResourceTest.java b/src/test/java/edu/illinois/library/cantaloupe/resource/iiif/v3/InformationResourceTest.java index 38e55cec7..687fc9263 100644 --- a/src/test/java/edu/illinois/library/cantaloupe/resource/iiif/v3/InformationResourceTest.java +++ b/src/test/java/edu/illinois/library/cantaloupe/resource/iiif/v3/InformationResourceTest.java @@ -600,6 +600,11 @@ void testOPTIONSWhenEnabled() throws Exception { assertEquals(2, methods.size()); assertTrue(methods.contains("GET")); assertTrue(methods.contains("OPTIONS")); + + List allowedHeaders = + List.of(StringUtils.split(headers.getFirstValue("Access-Control-Allow-Headers"), ", ")); + assertEquals(1, allowedHeaders.size()); + assertTrue(allowedHeaders.contains("Authorization")); } @Test