From 79fb53ad2abe1c196d4171191174445eb99523ba Mon Sep 17 00:00:00 2001 From: Wilson Kurniawan Date: Sat, 10 Jul 2021 22:51:20 +0800 Subject: [PATCH] [#11240] [#11238] Support Docker compose for dependent service + deployment to GAE flex (#11253) --- .github/workflows/component.yml | 5 +- .github/workflows/e2e.yml | 11 +-- .github/workflows/lnp.yml | 6 +- .gitignore | 2 +- build.gradle | 9 +- datastore-dev/Dockerfile | 6 ++ docker-compose.yml | 12 +++ docs/development.md | 12 +++ docs/search.md | 18 ++-- solr/Dockerfile | 5 ++ solr.sh => solr/solr.sh | 0 .../e2e/cases/AdminAccountsPageE2ETest.java | 1 + .../e2e/cases/AdminSessionsPageE2ETest.java | 1 - .../teammates/e2e/cases/BaseE2ETestCase.java | 5 +- ...FeedbackConstSumOptionQuestionE2ETest.java | 1 + ...dbackConstSumRecipientQuestionE2ETest.java | 1 + .../FeedbackContributionQuestionE2ETest.java | 1 + .../e2e/cases/FeedbackMcqQuestionE2ETest.java | 1 + .../e2e/cases/FeedbackMsqQuestionE2ETest.java | 1 + .../FeedbackNumScaleQuestionE2ETest.java | 2 + .../FeedbackRankOptionQuestionE2ETest.java | 1 + .../FeedbackRankRecipientQuestionE2ETest.java | 1 + .../cases/FeedbackRubricQuestionE2ETest.java | 1 + .../cases/InstructorCoursesPageE2ETest.java | 3 + .../e2e/cases/TimezoneSyncerTest.java | 1 - .../teammates/e2e/pageobjects/AppPage.java | 38 ++++----- .../pageobjects/InstructorCoursesPage.java | 45 +++++----- .../e2e/pageobjects/StudentProfilePage.java | 4 +- .../resources/scripts/readTinyMCEContent.js | 15 ++++ src/e2e/resources/scripts/writeToTinyMCE.js | 18 ++++ src/main/appengine/app.template.yaml | 84 ++++++++++++------- src/main/docker/Dockerfile | 6 ++ .../java/teammates/common/util/Config.java | 30 +++++-- .../logic/core/LocalTaskQueueService.java | 2 +- src/main/java/teammates/main/Application.java | 10 +-- .../storage/api/DatastoreEmulatorRunner.java | 2 +- .../ui/webapi/HealthCheckServlet.java | 26 ++++++ .../teammates/ui/webapi/WebPageServlet.java | 20 ----- .../ui/webapi/WebSecurityHeaderFilter.java | 56 +++++++++++++ src/main/resources/build.template.properties | 3 +- src/main/webapp/WEB-INF/web.xml | 35 ++++++++ wait-for-server.sh | 13 +++ 42 files changed, 367 insertions(+), 147 deletions(-) create mode 100644 datastore-dev/Dockerfile create mode 100644 docker-compose.yml create mode 100644 solr/Dockerfile rename solr.sh => solr/solr.sh (100%) mode change 100644 => 100755 create mode 100644 src/e2e/resources/scripts/readTinyMCEContent.js create mode 100644 src/e2e/resources/scripts/writeToTinyMCE.js create mode 100644 src/main/docker/Dockerfile create mode 100644 src/main/java/teammates/ui/webapi/HealthCheckServlet.java create mode 100644 src/main/java/teammates/ui/webapi/WebSecurityHeaderFilter.java create mode 100755 wait-for-server.sh diff --git a/.github/workflows/component.yml b/.github/workflows/component.yml index ece3dc67792..bb2f402ba5c 100644 --- a/.github/workflows/component.yml +++ b/.github/workflows/component.yml @@ -64,10 +64,7 @@ jobs: run: mv src/test/resources/test.ci-${{ matrix.os }}.properties src/test/resources/test.properties - name: Run Solr search service if: matrix.os == 'ubuntu-latest' # Docker does not work well on Windows env - run: | - docker run --name=tm_solr -d -p 8983:8983 solr:8.8.1 - sleep 5 - docker exec $(docker ps -qf "name=tm_solr") /bin/sh -c "$(cat ./solr.sh)" + run: docker-compose run -d -p 8983:8983 solr - name: Run Backend Tests run: | ./gradlew createConfigs componentTests diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index afd573e4b64..d1be3b4a903 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -37,20 +37,17 @@ jobs: run: mv $GITHUB_WORKSPACE/src/e2e/resources/test.ci-$E2E_BROWSER.properties src/e2e/resources/test.properties env: E2E_BROWSER: ${{ matrix.browser }} - - name: Run Solr search service - run: | - docker run --name=tm_solr -d -p 8983:8983 solr:8.8.1 - sleep 5 - docker exec $(docker ps -qf "name=tm_solr") /bin/sh -c "$(cat ./solr.sh)" + - name: Run Solr search service + local Datastore emulator + run: docker-compose up -d - name: Create Config Files run: ./gradlew createConfigs testClasses generateTypes - - name: Run local Datastore emulator - run: ./gradlew runDatastoreEmulator - name: Install Frontend Dependencies run: npm ci - name: Build Frontend Bundle run: npm run build -- --progress=false --serviceWorker=false - name: Start Server run: ./gradlew serverRun & + - name: Wait until server is running + run: ./wait-for-server.sh - name: Start Tests run: xvfb-run --server-args="-screen 0 1024x768x24" ./gradlew e2eTests diff --git a/.github/workflows/lnp.yml b/.github/workflows/lnp.yml index 574c55458ac..3f6d7d9980b 100644 --- a/.github/workflows/lnp.yml +++ b/.github/workflows/lnp.yml @@ -25,9 +25,11 @@ jobs: ${{ runner.os }}-gradle- - name: Create Config Files run: ./gradlew createConfigs - - name: Run local Datastore emulator - run: ./gradlew runDatastoreEmulator + - name: Run Solr search service + local Datastore emulator + run: docker-compose up -d - name: Start Server run: ./gradlew serverRun & + - name: Wait until server is running + run: ./wait-for-server.sh - name: Start Tests run: ./gradlew lnpTests diff --git a/.gitignore b/.gitignore index 93b75151ccd..b2128292508 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,6 @@ src/e2e/resources/gmail-api/ src/e2e/resources/downloads/ src/web/dist/* filestorage-dev/* -datastore-dev/* +datastore-dev/datastore/* !.gitkeep diff --git a/build.gradle b/build.gradle index 8fafc767e90..0f004a5bc1c 100644 --- a/build.gradle +++ b/build.gradle @@ -314,8 +314,13 @@ task copyDistToStagedApp { } appengineStage { - dependsOn explodeWar - finalizedBy removeWarFromStagedApp, copyExplodedWarToStagedApp, copyDistToStagedApp + if (project.hasProperty("flex")) { + dependsOn explodeWar + finalizedBy removeWarFromStagedApp, copyExplodedWarToStagedApp, copyDistToStagedApp + } else { + dependsOn explodeWar, copyDistToExplodedWar + finalizedBy removeWarFromStagedApp, copyExplodedWarToStagedApp + } } // STATIC ANALYSIS TASKS diff --git a/datastore-dev/Dockerfile b/datastore-dev/Dockerfile new file mode 100644 index 00000000000..aeb386353d5 --- /dev/null +++ b/datastore-dev/Dockerfile @@ -0,0 +1,6 @@ +FROM gcr.io/google.com/cloudsdktool/cloud-sdk:alpine + +RUN apk --update add openjdk8-jre +RUN gcloud components install beta cloud-datastore-emulator + +ENTRYPOINT gcloud beta emulators datastore start --host-port 0.0.0.0:8484 --project placeholder diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000000..33a94e51445 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3.9" +services: + datastore: + build: + context: datastore-dev + ports: + - 8484:8484 + solr: + build: + context: solr + ports: + - 8983:8983 diff --git a/docs/development.md b/docs/development.md index d6e64159aa7..7ef71cedddc 100644 --- a/docs/development.md +++ b/docs/development.md @@ -57,6 +57,11 @@ In order for the back-end to properly work, you need to have a running database The details on how to run them locally can be found [here (for local Datastore emulator)](#running-the-datastore-emulator) and [here (for full-text search service)](search.md). +If you have access to Docker, we have a Docker compose definition to run those services: +```sh +docker-compose up -d +``` + ### Starting the dev server To start the server in the background, run the following command @@ -171,6 +176,13 @@ You can use the pre-provided quickstart script which will run a local Datastore The Datastore emulator will be running in the port specified in the `build.properties` file. +### Using Docker-based tooling + +We have a Docker compose definition to run dependent services, including local Datastore emulator. Run it under the `datastore` service name and bind to the container port `8484`: +```sh +docker-compose run -p 8484:8484 datastore +``` + ### Using Cloud SDK Alternatively, you can use `gcloud` command to manage the local Datastore emulator instance directly. For this, you need a working [Google Cloud SDK](https://cloud.google.com/sdk/docs) in your development environment. diff --git a/docs/search.md b/docs/search.md index 11a58279114..38b38600274 100644 --- a/docs/search.md +++ b/docs/search.md @@ -8,18 +8,14 @@ This document will assume Solr version `8.8.1`. ## Setting up Solr using Docker -If you are familiar with Docker, this method is recommended. +If you have access to Docker, this method is straightforward and recommended. -1. Run the `solr:8.8.1` Docker image and bind to the container port `8983`. For example, to run a container named `tm_solr` accessible from `localhost:8983` in the background: - ```sh - docker pull solr:8.8.1 - docker run --name=tm_solr -d -p 8983:8983 solr:8.8.1 - ``` - **Verification:** the Solr admin console should be accessible in `http://localhost:8983`. -1. To initialise Solr for our use cases, we run the [Solr startup script](../solr.sh) located in the project directory in the running container: - ```sh - docker exec $(docker ps -qf "name=tm_solr") /bin/sh -c "$(cat ./solr.sh)" - ``` +We have provided a Docker compose definition to run dependent services, including Solr. Run it under the `solr` service name and bind to the container port `8983`: +```sh +docker-compose run -p 8983:8983 solr +``` + +**Verification:** the Solr admin console should be accessible in `http://localhost:8983`. ## Setting up Solr manually diff --git a/solr/Dockerfile b/solr/Dockerfile new file mode 100644 index 00000000000..7be4f0aba22 --- /dev/null +++ b/solr/Dockerfile @@ -0,0 +1,5 @@ +FROM solr:8.8.1 + +COPY solr.sh solr.sh + +ENTRYPOINT ["/bin/sh", "-c", "bin/solr start && ./solr.sh && tail -f /var/solr/logs/solr.log"] diff --git a/solr.sh b/solr/solr.sh old mode 100644 new mode 100755 similarity index 100% rename from solr.sh rename to solr/solr.sh diff --git a/src/e2e/java/teammates/e2e/cases/AdminAccountsPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/AdminAccountsPageE2ETest.java index 7996ae64620..5512b1b89b2 100644 --- a/src/e2e/java/teammates/e2e/cases/AdminAccountsPageE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/AdminAccountsPageE2ETest.java @@ -64,6 +64,7 @@ public void testAll() { accountsPage.clickDowngradeAccount(); accountsPage.verifyStatusMessage("Instructor account is successfully downgraded to student."); + accountsPage.waitForPageToLoad(); account = getAccount(googleId); assertFalse(account.isInstructor); diff --git a/src/e2e/java/teammates/e2e/cases/AdminSessionsPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/AdminSessionsPageE2ETest.java index e7b045e8913..2c2e55f4102 100644 --- a/src/e2e/java/teammates/e2e/cases/AdminSessionsPageE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/AdminSessionsPageE2ETest.java @@ -74,7 +74,6 @@ public void testAll() { AppUrl sessionsUrl = createUrl(Const.WebPageURIs.ADMIN_SESSIONS_PAGE); AdminSessionsPage sessionsPage = loginAdminToPage(sessionsUrl, AdminSessionsPage.class); - sessionsPage.waitUntilAnimationFinish(); String tableTimezone = sessionsPage.getSessionsTableTimezone(); diff --git a/src/e2e/java/teammates/e2e/cases/BaseE2ETestCase.java b/src/e2e/java/teammates/e2e/cases/BaseE2ETestCase.java index bde1bef5554..558736f9abf 100644 --- a/src/e2e/java/teammates/e2e/cases/BaseE2ETestCase.java +++ b/src/e2e/java/teammates/e2e/cases/BaseE2ETestCase.java @@ -154,7 +154,7 @@ protected void deleteDownloadsFile(String fileName) { */ protected void verifyDownloadedFile(String expectedFileName, List expectedContent) { String filePath = getTestDownloadsFolder() + expectedFileName; - int retryLimit = 5; + int retryLimit = TestProperties.TEST_TIMEOUT; boolean actual = Files.exists(Paths.get(filePath)); while (!actual && retryLimit > 0) { retryLimit--; @@ -174,7 +174,8 @@ protected void verifyDownloadedFile(String expectedFileName, List expect } protected T getNewPageInstance(AppUrl url, Class typeOfPage) { - return AppPage.getNewPageInstance(browser, url, typeOfPage); + browser.goToUrl(url.toAbsoluteString()); + return AppPage.getNewPageInstance(browser, typeOfPage); } /** diff --git a/src/e2e/java/teammates/e2e/cases/FeedbackConstSumOptionQuestionE2ETest.java b/src/e2e/java/teammates/e2e/cases/FeedbackConstSumOptionQuestionE2ETest.java index 24ad964c54a..0b549643433 100644 --- a/src/e2e/java/teammates/e2e/cases/FeedbackConstSumOptionQuestionE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/FeedbackConstSumOptionQuestionE2ETest.java @@ -78,6 +78,7 @@ protected void testEditPage() { questionDetails.setDistributePointsFor("At least some options"); loadedQuestion.questionDetails = questionDetails; feedbackEditPage.editConstSumQuestion(2, questionDetails); + feedbackEditPage.waitForPageToLoad(); feedbackEditPage.verifyConstSumQuestionDetails(2, questionDetails); verifyPresentInDatastore(loadedQuestion); diff --git a/src/e2e/java/teammates/e2e/cases/FeedbackConstSumRecipientQuestionE2ETest.java b/src/e2e/java/teammates/e2e/cases/FeedbackConstSumRecipientQuestionE2ETest.java index cf4118d09d7..0e507147597 100644 --- a/src/e2e/java/teammates/e2e/cases/FeedbackConstSumRecipientQuestionE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/FeedbackConstSumRecipientQuestionE2ETest.java @@ -75,6 +75,7 @@ protected void testEditPage() { questionDetails.setDistributePointsFor("At least some options"); loadedQuestion.questionDetails = questionDetails; feedbackEditPage.editConstSumQuestion(2, questionDetails); + feedbackEditPage.waitForPageToLoad(); feedbackEditPage.verifyConstSumQuestionDetails(2, questionDetails); verifyPresentInDatastore(loadedQuestion); diff --git a/src/e2e/java/teammates/e2e/cases/FeedbackContributionQuestionE2ETest.java b/src/e2e/java/teammates/e2e/cases/FeedbackContributionQuestionE2ETest.java index 195a14a7e5d..d25fdf8b341 100644 --- a/src/e2e/java/teammates/e2e/cases/FeedbackContributionQuestionE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/FeedbackContributionQuestionE2ETest.java @@ -74,6 +74,7 @@ protected void testEditPage() { questionDetails.setNotSureAllowed(false); loadedQuestion.questionDetails = questionDetails; feedbackEditPage.editContributionQuestion(2, questionDetails); + feedbackEditPage.waitForPageToLoad(); feedbackEditPage.verifyContributionQuestionDetails(2, questionDetails); verifyPresentInDatastore(loadedQuestion); diff --git a/src/e2e/java/teammates/e2e/cases/FeedbackMcqQuestionE2ETest.java b/src/e2e/java/teammates/e2e/cases/FeedbackMcqQuestionE2ETest.java index 4de509c8f1d..d2c9bb158de 100644 --- a/src/e2e/java/teammates/e2e/cases/FeedbackMcqQuestionE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/FeedbackMcqQuestionE2ETest.java @@ -79,6 +79,7 @@ protected void testEditPage() { questionDetails.setMcqChoices(choices); loadedQuestion.questionDetails = questionDetails; feedbackEditPage.editMcqQuestion(2, questionDetails); + feedbackEditPage.waitForPageToLoad(); feedbackEditPage.verifyMcqQuestionDetails(2, questionDetails); verifyPresentInDatastore(loadedQuestion); diff --git a/src/e2e/java/teammates/e2e/cases/FeedbackMsqQuestionE2ETest.java b/src/e2e/java/teammates/e2e/cases/FeedbackMsqQuestionE2ETest.java index cfe21614566..ae901fe833f 100644 --- a/src/e2e/java/teammates/e2e/cases/FeedbackMsqQuestionE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/FeedbackMsqQuestionE2ETest.java @@ -82,6 +82,7 @@ protected void testEditPage() { questionDetails.setMsqChoices(choices); loadedQuestion.questionDetails = questionDetails; feedbackEditPage.editMsqQuestion(2, questionDetails); + feedbackEditPage.waitForPageToLoad(); feedbackEditPage.verifyMsqQuestionDetails(2, questionDetails); verifyPresentInDatastore(loadedQuestion); diff --git a/src/e2e/java/teammates/e2e/cases/FeedbackNumScaleQuestionE2ETest.java b/src/e2e/java/teammates/e2e/cases/FeedbackNumScaleQuestionE2ETest.java index 088ee11c00c..d428a413800 100644 --- a/src/e2e/java/teammates/e2e/cases/FeedbackNumScaleQuestionE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/FeedbackNumScaleQuestionE2ETest.java @@ -49,6 +49,7 @@ protected void testEditPage() { // add new question exactly like loaded question loadedQuestion.setQuestionNumber(2); feedbackEditPage.addNumScaleQuestion(loadedQuestion); + feedbackEditPage.waitUntilAnimationFinish(); feedbackEditPage.verifyNumScaleQuestionDetails(2, questionDetails); verifyPresentInDatastore(loadedQuestion); @@ -72,6 +73,7 @@ protected void testEditPage() { questionDetails.setMaxScale(100); loadedQuestion.questionDetails = questionDetails; feedbackEditPage.editNumScaleQuestion(2, questionDetails); + feedbackEditPage.waitForPageToLoad(); feedbackEditPage.verifyNumScaleQuestionDetails(2, questionDetails); verifyPresentInDatastore(loadedQuestion); diff --git a/src/e2e/java/teammates/e2e/cases/FeedbackRankOptionQuestionE2ETest.java b/src/e2e/java/teammates/e2e/cases/FeedbackRankOptionQuestionE2ETest.java index 7b836037565..ea163d4b434 100644 --- a/src/e2e/java/teammates/e2e/cases/FeedbackRankOptionQuestionE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/FeedbackRankOptionQuestionE2ETest.java @@ -81,6 +81,7 @@ protected void testEditPage() { questionDetails.setMinOptionsToBeRanked(1); loadedQuestion.questionDetails = questionDetails; feedbackEditPage.editRankQuestion(2, questionDetails); + feedbackEditPage.waitForPageToLoad(); feedbackEditPage.verifyRankQuestionDetails(2, questionDetails); verifyPresentInDatastore(loadedQuestion); diff --git a/src/e2e/java/teammates/e2e/cases/FeedbackRankRecipientQuestionE2ETest.java b/src/e2e/java/teammates/e2e/cases/FeedbackRankRecipientQuestionE2ETest.java index ccbf6e87319..558b62c0ed3 100644 --- a/src/e2e/java/teammates/e2e/cases/FeedbackRankRecipientQuestionE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/FeedbackRankRecipientQuestionE2ETest.java @@ -77,6 +77,7 @@ protected void testEditPage() { questionDetails.setMinOptionsToBeRanked(Const.POINTS_NO_VALUE); loadedQuestion.questionDetails = questionDetails; feedbackEditPage.editRankQuestion(2, questionDetails); + feedbackEditPage.waitForPageToLoad(); feedbackEditPage.verifyRankQuestionDetails(2, questionDetails); verifyPresentInDatastore(loadedQuestion); diff --git a/src/e2e/java/teammates/e2e/cases/FeedbackRubricQuestionE2ETest.java b/src/e2e/java/teammates/e2e/cases/FeedbackRubricQuestionE2ETest.java index 8783d7dda73..46ea12614ff 100644 --- a/src/e2e/java/teammates/e2e/cases/FeedbackRubricQuestionE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/FeedbackRubricQuestionE2ETest.java @@ -93,6 +93,7 @@ protected void testEditPage() { questionDetails.setRubricWeightsForEachCell(new ArrayList<>()); loadedQuestion.questionDetails = questionDetails; feedbackEditPage.editRubricQuestion(2, questionDetails); + feedbackEditPage.waitForPageToLoad(); feedbackEditPage.verifyRubricQuestionDetails(2, questionDetails); verifyPresentInDatastore(loadedQuestion); diff --git a/src/e2e/java/teammates/e2e/cases/InstructorCoursesPageE2ETest.java b/src/e2e/java/teammates/e2e/cases/InstructorCoursesPageE2ETest.java index 1125e90ba29..11d7278a78f 100644 --- a/src/e2e/java/teammates/e2e/cases/InstructorCoursesPageE2ETest.java +++ b/src/e2e/java/teammates/e2e/cases/InstructorCoursesPageE2ETest.java @@ -110,6 +110,7 @@ public void testAll() { coursesPage.restoreCourse(newCourse.getId()); coursesPage.verifyStatusMessage("The course " + newCourse.getId() + " has been restored."); + coursesPage.waitForPageToLoad(); coursesPage.verifyNumDeletedCourses(1); // No need to call sortByCreationDate() here because it is the default sort in DESC order coursesPage.verifyActiveCoursesDetails(activeCoursesWithNewCourseSortedByCreationDate); @@ -131,6 +132,7 @@ public void testAll() { coursesPage.restoreCourse(newCourse.getId()); coursesPage.verifyStatusMessage("The course " + newCourse.getId() + " has been restored."); + coursesPage.waitForPageToLoad(); coursesPage.verifyNumDeletedCourses(1); coursesPage.verifyArchivedCoursesDetails(archivedCoursesWithNewCourse); assertFalse(BACKDOOR.isCourseInRecycleBin(newCourse.getId())); @@ -151,6 +153,7 @@ public void testAll() { coursesPage.restoreAllCourses(); coursesPage.verifyStatusMessage("All courses have been restored."); + coursesPage.waitForPageToLoad(); coursesPage.sortByCourseId(); coursesPage.verifyActiveCoursesDetails(activeCoursesWithRestored); coursesPage.verifyArchivedCoursesDetails(archivedCourses); diff --git a/src/e2e/java/teammates/e2e/cases/TimezoneSyncerTest.java b/src/e2e/java/teammates/e2e/cases/TimezoneSyncerTest.java index 9a305ccb7f3..ade7e2acd62 100644 --- a/src/e2e/java/teammates/e2e/cases/TimezoneSyncerTest.java +++ b/src/e2e/java/teammates/e2e/cases/TimezoneSyncerTest.java @@ -52,7 +52,6 @@ public void testAll() { String currentTzVersion = timezonePage.getMomentTimezoneVersion(); IanaTimezonePage ianaPage = getNewPageInstance( new AppUrl(IanaTimezonePage.IANA_TIMEZONE_DATABASE_URL), IanaTimezonePage.class); - ianaPage.waitForPageToLoad(); String latestTzVersion = ianaPage.getVersion(); if (!currentTzVersion.equals(latestTzVersion)) { diff --git a/src/e2e/java/teammates/e2e/pageobjects/AppPage.java b/src/e2e/java/teammates/e2e/pageobjects/AppPage.java index 276ceab596c..9a4fba2250f 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/AppPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/AppPage.java @@ -9,6 +9,7 @@ import java.io.File; import java.io.IOException; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.util.List; import java.util.Map; @@ -29,7 +30,6 @@ import teammates.common.datatransfer.FeedbackParticipantType; import teammates.common.util.ThreadHelper; -import teammates.common.util.Url; import teammates.common.util.retry.MaximumRetriesExceededException; import teammates.common.util.retry.RetryManager; import teammates.common.util.retry.RetryableTask; @@ -49,12 +49,16 @@ public abstract class AppPage { private static final String CLEAR_ELEMENT_SCRIPT; private static final String SCROLL_ELEMENT_TO_CENTER_AND_CLICK_SCRIPT; + private static final String READ_TINYMCE_CONTENT_SCRIPT; + private static final String WRITE_TO_TINYMCE_SCRIPT; static { try { CLEAR_ELEMENT_SCRIPT = FileHelper.readFile("src/e2e/resources/scripts/clearElementWithoutEvents.js"); SCROLL_ELEMENT_TO_CENTER_AND_CLICK_SCRIPT = FileHelper .readFile("src/e2e/resources/scripts/scrollElementToCenterAndClick.js"); + READ_TINYMCE_CONTENT_SCRIPT = FileHelper.readFile("src/e2e/resources/scripts/readTinyMCEContent.js"); + WRITE_TO_TINYMCE_SCRIPT = FileHelper.readFile("src/e2e/resources/scripts/writeToTinyMCE.js"); } catch (IOException e) { throw new RuntimeException(e); } @@ -104,27 +108,24 @@ public AppPage(Browser browser) { throw new IllegalStateException("Not in the correct page!"); } - /** - * Fails if the new page content does not match content expected in a page of - * the type indicated by the parameter {@code typeOfPage}. - */ - public static T getNewPageInstance(Browser currentBrowser, Url url, Class typeOfPage) { - currentBrowser.goToUrl(url.toAbsoluteString()); - waitUntilAnimationFinish(currentBrowser); - return getNewPageInstance(currentBrowser, typeOfPage); - } - /** * Fails if the new page content does not match content expected in a page of * the type indicated by the parameter {@code typeOfPage}. */ public static T getNewPageInstance(Browser currentBrowser, Class typeOfPage) { + waitUntilAnimationFinish(currentBrowser); try { Constructor constructor = typeOfPage.getConstructor(Browser.class); T page = constructor.newInstance(currentBrowser); PageFactory.initElements(currentBrowser.driver, page); + page.waitForPageToLoad(); return page; - } catch (Exception e) { + } catch (InvocationTargetException e) { + if (e.getCause() instanceof IllegalStateException) { + throw (IllegalStateException) e.getCause(); + } + throw new RuntimeException(e); + } catch (NoSuchMethodException | IllegalAccessException | InstantiationException e) { throw new RuntimeException(e); } } @@ -347,13 +348,9 @@ protected void fillFileBox(RemoteWebElement fileBoxElement, String fileName) { */ protected String getEditorRichText(WebElement editor) { waitForElementPresence(By.tagName("iframe")); - browser.driver.switchTo().frame(editor.findElement(By.tagName("iframe"))); - - String innerHtml = browser.driver.findElement(By.id("tinymce")).getAttribute("innerHTML"); - // check if editor is empty - innerHtml = innerHtml.contains("data-mce-bogus") ? "" : innerHtml; - browser.driver.switchTo().defaultContent(); - return innerHtml; + String id = editor.findElement(By.tagName("textarea")).getAttribute("id"); + return (String) ((JavascriptExecutor) browser.driver) + .executeAsyncScript(READ_TINYMCE_CONTENT_SCRIPT, id); } /** @@ -362,8 +359,7 @@ protected String getEditorRichText(WebElement editor) { protected void writeToRichTextEditor(WebElement editor, String text) { waitForElementPresence(By.tagName("iframe")); String id = editor.findElement(By.tagName("textarea")).getAttribute("id"); - executeScript(String.format("tinyMCE.get('%s').setContent('%s');" - + " tinyMCE.get('%s').save()", id, text, id)); + ((JavascriptExecutor) browser.driver).executeAsyncScript(WRITE_TO_TINYMCE_SCRIPT, id, text); } /** diff --git a/src/e2e/java/teammates/e2e/pageobjects/InstructorCoursesPage.java b/src/e2e/java/teammates/e2e/pageobjects/InstructorCoursesPage.java index b8875df5b35..ccbc54ae20a 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/InstructorCoursesPage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/InstructorCoursesPage.java @@ -37,21 +37,6 @@ public class InstructorCoursesPage extends AppPage { @FindBy(id = "btn-save-course") private WebElement submitButton; - @FindBy(id = "active-courses-table") - private WebElement activeCoursesTable; - - @FindBy (id = "archived-courses-table") - private WebElement archivedCoursesTable; - - @FindBy (id = "deleted-courses-table") - private WebElement deletedCoursesTable; - - @FindBy(id = "deleted-table-heading") - private WebElement deleteTableHeading; - - @FindBy(id = "archived-table-heading") - private WebElement archiveTableHeading; - public InstructorCoursesPage(Browser browser) { super(browser); } @@ -61,10 +46,22 @@ protected boolean containsExpectedPageContents() { return getPageTitle().contains("Courses"); } + private WebElement getActiveCoursesTable() { + return browser.driver.findElement(By.id("active-courses-table")); + } + + private WebElement getArchivedCoursesTable() { + return browser.driver.findElement(By.id("archived-courses-table")); + } + + private WebElement getDeletedCoursesTable() { + return browser.driver.findElement(By.id("deleted-courses-table")); + } + public void verifyActiveCoursesDetails(CourseAttributes[] courses) { String[][] courseDetails = getCourseDetails(courses); // use verifyTableBodyValues as active courses are sorted - verifyTableBodyValues(activeCoursesTable, courseDetails); + verifyTableBodyValues(getActiveCoursesTable(), courseDetails); } public void verifyActiveCourseStatistics(CourseAttributes course, String numSections, String numTeams, @@ -172,13 +169,13 @@ public void moveArchivedCourseToRecycleBin(String courseId) { public void showDeleteTable() { if (!isElementVisible(By.id("deleted-course-id-0"))) { - click(deleteTableHeading); + click(By.id("deleted-table-heading")); } } public void showArchiveTable() { if (!isElementVisible(By.id("archived-course-id-0"))) { - click(archiveTableHeading); + click(By.id("archived-table-heading")); } } @@ -220,17 +217,17 @@ public void sortByCourseId() { private WebElement getActiveTableRow(String courseId) { int courseRowNumber = getRowNumberOfCourse(courseId); - return activeCoursesTable.findElements(By.cssSelector("tbody tr")).get(courseRowNumber); + return getActiveCoursesTable().findElements(By.cssSelector("tbody tr")).get(courseRowNumber); } private WebElement getArchivedTableRow(String courseId) { int courseRowNumber = getRowNumberOfArchivedCourse(courseId); - return archivedCoursesTable.findElements(By.cssSelector("tbody tr")).get(courseRowNumber); + return getArchivedCoursesTable().findElements(By.cssSelector("tbody tr")).get(courseRowNumber); } private WebElement getDeletedTableRow(String courseId) { int courseRowNumber = getRowNumberOfDeletedCourse(courseId); - return deletedCoursesTable.findElements(By.cssSelector("tbody tr")).get(courseRowNumber); + return getDeletedCoursesTable().findElements(By.cssSelector("tbody tr")).get(courseRowNumber); } private String[][] getCourseDetails(CourseAttributes[] courses) { @@ -317,7 +314,7 @@ private WebElement getDeleteButton(String courseId) { private int getCourseCount() { try { - return activeCoursesTable.findElements(By.cssSelector("tbody tr")).size(); + return getActiveCoursesTable().findElements(By.cssSelector("tbody tr")).size(); } catch (NoSuchElementException e) { return 0; } @@ -325,7 +322,7 @@ private int getCourseCount() { private int getArchivedCourseCount() { try { - return archivedCoursesTable.findElements(By.cssSelector("tbody tr")).size(); + return getArchivedCoursesTable().findElements(By.cssSelector("tbody tr")).size(); } catch (NoSuchElementException e) { return 0; } @@ -333,7 +330,7 @@ private int getArchivedCourseCount() { private int getDeletedCourseCount() { try { - return deletedCoursesTable.findElements(By.cssSelector("tbody tr")).size(); + return getDeletedCoursesTable().findElements(By.cssSelector("tbody tr")).size(); } catch (NoSuchElementException e) { return 0; } diff --git a/src/e2e/java/teammates/e2e/pageobjects/StudentProfilePage.java b/src/e2e/java/teammates/e2e/pageobjects/StudentProfilePage.java index f000fda4c06..a0e88314de6 100644 --- a/src/e2e/java/teammates/e2e/pageobjects/StudentProfilePage.java +++ b/src/e2e/java/teammates/e2e/pageobjects/StudentProfilePage.java @@ -73,11 +73,9 @@ public void editProfileThroughUi(String shortName, String email, String institut submitEditedProfile(); } - private StudentProfilePage submitEditedProfile() { + private void submitEditedProfile() { click(saveProfileButton); waitForConfirmationModalAndClickOk(); - waitForPageToLoad(true); - return changePageType(StudentProfilePage.class); } private void fillShortName(String shortName) { diff --git a/src/e2e/resources/scripts/readTinyMCEContent.js b/src/e2e/resources/scripts/readTinyMCEContent.js new file mode 100644 index 00000000000..7a4a4cf63bf --- /dev/null +++ b/src/e2e/resources/scripts/readTinyMCEContent.js @@ -0,0 +1,15 @@ +var id = arguments[0]; +var callback = arguments[arguments.length - 1]; +var nTries = 25; + +var loadTinyMceInstance = function () { + var tinyMceInstance = tinyMCE.get(id); + if (tinyMceInstance && tinyMceInstance.initialized || nTries < 0) { + callback(tinyMceInstance.getContent()); + } else { + nTries--; + setTimeout(loadTinyMceInstance, 200); + } +}; + +loadTinyMceInstance(); diff --git a/src/e2e/resources/scripts/writeToTinyMCE.js b/src/e2e/resources/scripts/writeToTinyMCE.js new file mode 100644 index 00000000000..fcf09c0c5fe --- /dev/null +++ b/src/e2e/resources/scripts/writeToTinyMCE.js @@ -0,0 +1,18 @@ +var id = arguments[0]; +var content = arguments[1]; +var callback = arguments[arguments.length - 1]; +var nTries = 25; + +var loadTinyMceInstance = function () { + var tinyMceInstance = tinyMCE.get(id); + if (tinyMceInstance && tinyMceInstance.initialized || nTries < 0) { + tinyMceInstance.setContent(content); + tinyMceInstance.save(); + callback('done'); + } else { + nTries--; + setTimeout(loadTinyMceInstance, 200); + } +}; + +loadTinyMceInstance(); diff --git a/src/main/appengine/app.template.yaml b/src/main/appengine/app.template.yaml index 840bf6a8d8d..5da3962621f 100644 --- a/src/main/appengine/app.template.yaml +++ b/src/main/appengine/app.template.yaml @@ -1,4 +1,10 @@ +# This template supports the app.yaml format for both App Engine Standard and Flexible environment. +# In order to deploy to one kind of runtime, the configuration for the other kind must be completely removed/commented out. + +########################################################################### +# App Engine Standard (Java 11) configuration # Reference: https://cloud.google.com/appengine/docs/standard/java11/config/appref +########################################################################### # Run/deploy with Java 11 runtime. runtime: java11 @@ -23,56 +29,43 @@ handlers: # These are the handlers for static files. By specifying them as static files, requests pointing to these URLs will not add up to instance load. # Reference: https://cloud.google.com/appengine/docs/standard/java11/config/appref#handlers_element - # Single HTML page. This will also be equipped with sufficient security headers such as CSP. - - url: /(index\.html|(web/.*)|)$ - static_files: dist/index.html - upload: dist/index.html - expiration: 10m - secure: always - http_headers: - Content-Security-Policy: "default-src 'none'; script-src 'self' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://cdn.jsdelivr.net/; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net/; frame-src 'self' docs.google.com https://www.google.com/recaptcha/; img-src 'self' data: https:; font-src 'self' https://cdn.jsdelivr.net/; connect-src 'self'; manifest-src 'self'; form-action 'none'; frame-ancestors 'self'; base-uri 'self'" - X-Content-Type-Options: nosniff - X-Frame-Options: SAMEORIGIN - X-XSS-Protection: 1; mode=block - Strict-Transport-Security: max-age=31536000 - # Assets and front-end files - url: /assets - static_dir: dist/assets + static_dir: assets expiration: 90d - url: /(.*\.(js|css))$ - static_files: dist/\1 - upload: dist/.*\.(js|css)$ + static_files: \1 + upload: .*\.(js|css)$ expiration: 90d # Progressive web app files - url: /manifest.webmanifest - static_files: dist/manifest.webmanifest - upload: dist/manifest.webmanifest + static_files: manifest.webmanifest + upload: manifest.webmanifest expiration: 10m - url: /ngsw.json - static_files: dist/ngsw.json - upload: dist/ngsw.json + static_files: ngsw.json + upload: ngsw.json expiration: 1d # Crawler-related files # - url: /sitemap.xml - # static_files: dist/sitemap.xml - # upload: dist/sitemap.xml + # static_files: sitemap.xml + # upload: sitemap.xml # expiration: 1d # - url: /robots.txt - # static_files: dist/robots.txt - # upload: dist/robots.txt + # static_files: robots.txt + # upload: robots.txt # expiration: 1d # Webmaster files # - url: /BingSiteAuth.xml - # static_files: dist/BingSiteAuth.xml - # upload: dist/BingSiteAuth.xml + # static_files: BingSiteAuth.xml + # upload: BingSiteAuth.xml # expiration: 1d # - url: /google8c7ef1e995031e09.html - # static_files: dist/google8c7ef1e995031e09.html - # upload: dist/google8c7ef1e995031e09.html + # static_files: google8c7ef1e995031e09.html + # upload: google8c7ef1e995031e09.html # expiration: 1d # All incoming requests will be redirected to HTTPS. @@ -99,3 +92,38 @@ automatic_scaling: # Reference: https://cloud.google.com/appengine/docs/standard/java11/configuring-warmup-requests inbound_services: - warmup + +########################################################################### +# App Engine Flexible (custom runtime) configuration +# Reference: https://cloud.google.com/appengine/docs/flexible/custom-runtimes/configuring-your-app-with-app-yaml +########################################################################### + +# Defines custom runtime for App Engine flexible environment +# runtime: custom +# env: flex + +# resources: +# cpu: 1 +# memory_gb: 1 +# disk_size_gb: 10 + +# automatic_scaling: +# # min_num_instances: 1 +# # max_num_instances: 8 +# # cool_down_period_sec: 120 +# cpu_utilization: +# target_utilization: 0.6 + +# liveness_check: +# path: /ping +# check_interval_sec: 5 +# timeout_sec: 5 +# failure_threshold: 4 +# success_threshold: 2 + +# readiness_check: +# path: /ping +# check_interval_sec: 5 +# timeout_sec: 5 +# failure_threshold: 4 +# success_threshold: 2 diff --git a/src/main/docker/Dockerfile b/src/main/docker/Dockerfile new file mode 100644 index 00000000000..a4a5c7cd4f2 --- /dev/null +++ b/src/main/docker/Dockerfile @@ -0,0 +1,6 @@ +FROM openjdk:11-jre + +COPY dist . +COPY WEB-INF WEB-INF + +ENTRYPOINT java -ea -Djava.util.logging.config.file=WEB-INF/logging.properties -cp "WEB-INF/lib/*:WEB-INF/classes/." teammates.main.Application diff --git a/src/main/java/teammates/common/util/Config.java b/src/main/java/teammates/common/util/Config.java index e2c03c1503e..87fe880da8f 100644 --- a/src/main/java/teammates/common/util/Config.java +++ b/src/main/java/teammates/common/util/Config.java @@ -140,7 +140,18 @@ private Config() { } static String getBaseAppUrl() { - return isDevServer() ? "http://localhost:8080" : "https://" + APP_ID + ".appspot.com"; + return isDevServer() ? "http://localhost:" + getPort() : "https://" + APP_ID + ".appspot.com"; + } + + /** + * Returns the port number at which the system will be run in. + */ + public static int getPort() { + String portEnv = System.getenv("PORT"); + if (portEnv == null || !portEnv.matches("\\d{2,5}")) { + return 8080; + } + return Integer.parseInt(portEnv); } /** @@ -152,17 +163,20 @@ public static boolean isDevServer() { // This means that any developer can replicate this condition in dev server, // but it is their own choice and risk should they choose to do so. - String appName = System.getenv("GAE_APPLICATION"); String version = System.getenv("GAE_VERSION"); - String env = System.getenv("GAE_ENV"); - - if (appName == null || version == null || env == null) { + if (!APP_VERSION.equals(version)) { return true; } - return !appName.endsWith(APP_ID) - || !APP_VERSION.equals(version) - || !"standard".equals(env); + String env = System.getenv("GAE_ENV"); + if ("standard".equals(env)) { + // GAE standard + String appName = System.getenv("GAE_APPLICATION"); + return appName == null || !appName.endsWith(APP_ID); + } + + // GAE flexible; GAE_ENV variable should not exist in GAE flexible environment + return env != null; } /** diff --git a/src/main/java/teammates/logic/core/LocalTaskQueueService.java b/src/main/java/teammates/logic/core/LocalTaskQueueService.java index 64f37979454..837de5a2fde 100644 --- a/src/main/java/teammates/logic/core/LocalTaskQueueService.java +++ b/src/main/java/teammates/logic/core/LocalTaskQueueService.java @@ -39,7 +39,7 @@ public void addDeferredTask(TaskWrapper task, long countdownTime) { return; } HttpPost post = new HttpPost(createBasicUri( - "http://localhost:8080" + task.getWorkerUrl(), task.getParamMap())); + "http://localhost:" + Config.getPort() + task.getWorkerUrl(), task.getParamMap())); if (task.getRequestBody() != null) { StringEntity entity = new StringEntity( diff --git a/src/main/java/teammates/main/Application.java b/src/main/java/teammates/main/Application.java index 42403187cdb..e666a2f742d 100644 --- a/src/main/java/teammates/main/Application.java +++ b/src/main/java/teammates/main/Application.java @@ -11,7 +11,6 @@ import teammates.common.util.Config; import teammates.ui.webapi.DevServerLoginServlet; -import teammates.ui.webapi.WebPageServlet; /** * Entrypoint to the system. @@ -30,7 +29,7 @@ public static void main(String[] args) throws Exception { System.setProperty("org.eclipse.jetty.util.log.class", org.eclipse.jetty.util.log.StdErrLog.class.getName()); System.setProperty("org.eclipse.jetty.LEVEL", "INFO"); - Server server = new Server(8080); + Server server = new Server(Config.getPort()); WebAppContext webapp = new WebAppContext(); webapp.setContextPath("/"); @@ -40,12 +39,7 @@ public static void main(String[] args) throws Exception { ClassList classlist = ClassList.setServerDefault(server); if (Config.isDevServer()) { - // For dev server, we dynamically add servlets to serve the dev server login page and the bundled front-end. - // The front-end needs not be added in production server as it is configured separately in app.yaml. - webapp.setWelcomeFiles(new String[] { "index.html" }); - - ServletHolder webPageServlet = new ServletHolder("WebPageServlet", new WebPageServlet()); - webapp.addServlet(webPageServlet, "/web/*"); + // For dev server, we dynamically add servlet to serve the dev server login page. ServletHolder devServerLoginServlet = new ServletHolder("DevServerLoginServlet", new DevServerLoginServlet()); diff --git a/src/main/java/teammates/storage/api/DatastoreEmulatorRunner.java b/src/main/java/teammates/storage/api/DatastoreEmulatorRunner.java index ed5b984860e..37c0d370644 100644 --- a/src/main/java/teammates/storage/api/DatastoreEmulatorRunner.java +++ b/src/main/java/teammates/storage/api/DatastoreEmulatorRunner.java @@ -22,7 +22,7 @@ public static void main(String[] args) throws IOException, InterruptedException .setConsistency(0.9) // default setting .setPort(Config.APP_LOCALDATASTORE_PORT) .setStoreOnDisk(true) - .setDataDir(Paths.get("datastore-dev")) + .setDataDir(Paths.get("datastore-dev/datastore")) .build(); localDatastoreHelper.start(); } diff --git a/src/main/java/teammates/ui/webapi/HealthCheckServlet.java b/src/main/java/teammates/ui/webapi/HealthCheckServlet.java new file mode 100644 index 00000000000..5dfb429778d --- /dev/null +++ b/src/main/java/teammates/ui/webapi/HealthCheckServlet.java @@ -0,0 +1,26 @@ +package teammates.ui.webapi; + +import java.io.IOException; + +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.http.HttpStatus; + +/** + * Servlet that handles server health check. + * + *

Note that "health" here is only defined as the server being reachable. It does not + * indicate whether other dependent components such as DB connection and Google Cloud libraries + * are working as expected. + */ +public class HealthCheckServlet extends HttpServlet { + + @Override + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { + resp.setStatus(HttpStatus.SC_OK); + resp.getWriter().write("OK"); + } + +} diff --git a/src/main/java/teammates/ui/webapi/WebPageServlet.java b/src/main/java/teammates/ui/webapi/WebPageServlet.java index 3011eb8396a..07578efabf0 100644 --- a/src/main/java/teammates/ui/webapi/WebPageServlet.java +++ b/src/main/java/teammates/ui/webapi/WebPageServlet.java @@ -1,7 +1,6 @@ package teammates.ui.webapi; import java.io.IOException; -import java.util.Arrays; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; @@ -21,27 +20,8 @@ public class WebPageServlet extends HttpServlet { private static final Logger log = Logger.getLogger(); - private static final String CSP_POLICY = String.join("; ", Arrays.asList( - "default-src 'none'", - "script-src 'self' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://cdn.jsdelivr.net/", - "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net/", - "frame-src 'self' docs.google.com https://www.google.com/recaptcha/", - "img-src 'self' data: http: https:", - "font-src 'self' https://cdn.jsdelivr.net/", - "connect-src 'self'", - "manifest-src 'self'", - "form-action 'none'", - "frame-ancestors 'self'", - "base-uri 'self'" - )); - @Override public void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - resp.setHeader("Content-Security-Policy", CSP_POLICY); - resp.setHeader("X-Content-Type-Options", "nosniff"); - resp.setHeader("X-Frame-Options", "SAMEORIGIN"); - resp.setHeader("X-XSS-Protection", "1; mode=block"); - resp.setHeader("Strict-Transport-Security", "max-age=31536000"); try { req.getRequestDispatcher("/index.html").forward(req, resp); } catch (RuntimeException e) { diff --git a/src/main/java/teammates/ui/webapi/WebSecurityHeaderFilter.java b/src/main/java/teammates/ui/webapi/WebSecurityHeaderFilter.java new file mode 100644 index 00000000000..63f4d3f69f5 --- /dev/null +++ b/src/main/java/teammates/ui/webapi/WebSecurityHeaderFilter.java @@ -0,0 +1,56 @@ +package teammates.ui.webapi; + +import java.io.IOException; +import java.util.Arrays; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +/** + * Filter to add web security headers. + */ +public class WebSecurityHeaderFilter implements Filter { + + private static final String CSP_POLICY = String.join("; ", Arrays.asList( + "default-src 'none'", + "script-src 'self' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://cdn.jsdelivr.net/", + "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net/", + "frame-src 'self' docs.google.com https://www.google.com/recaptcha/", + "img-src 'self' data: http: https:", + "font-src 'self' https://cdn.jsdelivr.net/", + "connect-src 'self'", + "manifest-src 'self'", + "form-action 'none'", + "frame-ancestors 'self'", + "base-uri 'self'" + )); + + @Override + public void init(FilterConfig filterConfig) { + // nothing to do + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + HttpServletResponse resp = (HttpServletResponse) response; + resp.setHeader("Content-Security-Policy", CSP_POLICY); + resp.setHeader("X-Content-Type-Options", "nosniff"); + resp.setHeader("X-Frame-Options", "SAMEORIGIN"); + resp.setHeader("X-XSS-Protection", "1; mode=block"); + resp.setHeader("Strict-Transport-Security", "max-age=31536000"); + + chain.doFilter(request, resp); + } + + @Override + public void destroy() { + // nothing to do + } + +} diff --git a/src/main/resources/build.template.properties b/src/main/resources/build.template.properties index 53574a939af..9e1d26969be 100644 --- a/src/main/resources/build.template.properties +++ b/src/main/resources/build.template.properties @@ -23,8 +23,7 @@ app.version = 7-0-0 # This should be used only during development mode where the dev server needs to be whitelisted for CORS. app.frontenddev.url=http\://localhost:4200 -# This is the port to connect with database. -# Use 8080 for dev server; and 8484 for local datastore emulator. +# This is the port to connect with local Datastore emulator. app.localdatastore.port = 8484 # This indicates whether task queues are active (e.g. items added to task queue will be queued for execution). diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 769ae783ab7..5707cb9e249 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -4,6 +4,16 @@ xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1" metadata-complete="true"> + + WebSecurityHeaderFilter + teammates.ui.webapi.WebSecurityHeaderFilter + + + WebSecurityHeaderFilter + /web/* + /index.html + / + RequestTraceFilter teammates.ui.webapi.RequestTraceFilter @@ -41,6 +51,31 @@ teammates.storage.search.SearchManagerStarter + + index.html + + + Servlet that handles the single web page application + WebPageServlet + teammates.ui.webapi.WebPageServlet + 0 + + + WebPageServlet + /web/* + + + + Health Check Servlet + HealthCheckServlet + teammates.ui.webapi.HealthCheckServlet + 0 + + + HealthCheckServlet + /ping + + REST API Servlet WebApiServlet diff --git a/wait-for-server.sh b/wait-for-server.sh new file mode 100755 index 00000000000..3156a71d03e --- /dev/null +++ b/wait-for-server.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +attempt=0 +while [ $attempt -le 60 ]; do + attempt=$(($attempt+1)) + echo "Waiting for server to be up (attempt: $attempt)..." + result=$(curl -svo /dev/null http://localhost:8080/ping 2>&1 | grep "200 OK") + if grep -q "200 OK" <<< $result; then + echo "Server is up!" + break + fi + sleep 1 +done