Compare commits

..

13 Commits
csrf ... saml

Author SHA1 Message Date
Anthony Stirling
fbc6b3a70e fix 2024-11-29 15:05:10 +00:00
Anthony Stirling
329f755823 Merge branch 'main' into saml 2024-11-29 14:32:04 +00:00
Anthony Stirling
acc50e48c5 debugs 2024-11-29 14:31:15 +00:00
Anthony Stirling
92a571d31c saml stuff 2024-11-29 14:29:54 +00:00
Anthony Stirling
5976e69f54 debugs 2024-11-29 10:40:10 +00:00
Anthony Stirling
e588d8f99e spring dev fix for saml 2024-11-29 09:06:51 +00:00
Anthony Stirling
1c0f423510 remove unused repo 2024-11-29 08:59:29 +00:00
Anthony Stirling
2d6fe55985 info to debug 2024-11-29 08:53:54 +00:00
Anthony Stirling
5171088fca more fixes 2024-11-29 08:43:57 +00:00
Anthony Stirling
b4837df76c ee flag for saml 2024-11-28 19:41:39 +00:00
Anthony Stirling
d20e8f7d54 oauth to saml and compare fixes etc 2024-11-28 19:27:37 +00:00
Anthony Stirling
2885fac30d remove debugs 2024-11-28 15:43:24 +00:00
Anthony Stirling
2a4a19a80f backup 2024-11-28 15:00:37 +00:00
325 changed files with 7233 additions and 51607 deletions

2
.github/CODEOWNERS vendored
View File

@@ -1,2 +1,2 @@
# All PRs to V1 must be approved by Frooodle # All PRs to V1 must be approved by Frooodle
* @Frooodle @reecebrowne @Ludy87 @DarioGii * @Frooodle

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: true blank_issues_enabled: true
contact_links: contact_links:
- name: 💬 Discord Server - name: 💬 Discord Server
url: https://discord.gg/HYmhKj45pU url: https://discord.gg/Cn8pWhQRxZ
about: You can join our Discord server for real time discussion and support about: You can join our Discord server for real time discussion and support

View File

@@ -11,13 +11,7 @@ updates:
interval: "weekly" interval: "weekly"
open-pull-requests-limit: 10 open-pull-requests-limit: 10
rebase-strategy: "auto" rebase-strategy: "auto"
- package-ecosystem: "docker" - package-ecosystem: "docker"
directory: "/" # Location of Dockerfile directory: "/" # Location of Dockerfile
schedule: schedule:
interval: "weekly" interval: "weekly"
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly

View File

@@ -9,7 +9,6 @@ Front End:
- any-glob-to-any-file: 'src/main/resources/templates/**/*' - any-glob-to-any-file: 'src/main/resources/templates/**/*'
- any-glob-to-any-file: 'src/main/resources/static/**/*' - any-glob-to-any-file: 'src/main/resources/static/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/**' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/web/**'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/UI/**/*'
Java: Java:
- changed-files: - changed-files:
@@ -30,10 +29,7 @@ Security:
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/config/security/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/provider/**/*' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/provider/**/*'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/AuthenticationType.java' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/AuthenticationType.java'
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/model/BackupNotFoundException.java'
- any-glob-to-any-file: 'scripts/download-security-jar.sh' - any-glob-to-any-file: 'scripts/download-security-jar.sh'
- any-glob-to-any-file: '.github/workflows/dependency-review.yml'
- any-glob-to-any-file: '.github/workflows/scorecards.yml'
API: API:
- changed-files: - changed-files:
@@ -41,7 +37,6 @@ API:
- any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/**/*' - any-glob-to-any-file: 'src/main/java/stirling/software/SPDF/controller/api/**/*'
- any-glob-to-any-file: 'scripts/png_to_webp.py' - any-glob-to-any-file: 'scripts/png_to_webp.py'
- any-glob-to-any-file: 'split_photos.py' - any-glob-to-any-file: 'split_photos.py'
- any-glob-to-any-file: '.github/workflows/swagger.yml'
Documentation: Documentation:
- changed-files: - changed-files:
@@ -51,25 +46,17 @@ Documentation:
Docker: Docker:
- changed-files: - changed-files:
- any-glob-to-any-file: '.github/workflows/build.yml'
- any-glob-to-any-file: '.github/workflows/push-docker.yml'
- any-glob-to-any-file: 'Dockerfile' - any-glob-to-any-file: 'Dockerfile'
- any-glob-to-any-file: 'Dockerfile.*' - any-glob-to-any-file: 'Dockerfile-*'
- any-glob-to-any-file: 'exampleYmlFiles/*.yml' - any-glob-to-any-file: 'exampleYmlFiles/*.yml'
- any-glob-to-any-file: 'scripts/download-security-jar.sh'
- any-glob-to-any-file: 'scripts/init.sh' - any-glob-to-any-file: 'scripts/init.sh'
- any-glob-to-any-file: 'scripts/init-without-ocr.sh' - any-glob-to-any-file: 'scripts/init-without-ocr.sh'
- any-glob-to-any-file: 'scripts/installFonts.sh' - any-glob-to-any-file: 'scripts/installFonts.sh'
- any-glob-to-any-file: 'test.sh'
- any-glob-to-any-file: 'test2.sh'
Test: Test:
- changed-files: - changed-files:
- any-glob-to-any-file: 'cucumber/**/*' - any-glob-to-any-file: 'cucumber/**/*'
- any-glob-to-any-file: 'src/test**/*' - any-glob-to-any-file: 'src/test**/*'
- any-glob-to-any-file: '.pre-commit-config'
- any-glob-to-any-file: '.github/workflows/pre_commit.yml'
- any-glob-to-any-file: '.github/workflows/scorecards.yml'
Github: Github:
- changed-files: - changed-files:

4
.github/release.yml vendored
View File

@@ -9,7 +9,7 @@ changelog:
- title: Bug Fixes - title: Bug Fixes
labels: labels:
- Bug - Bug
- title: Enhancements - title: Enhancements
labels: labels:
- enhancement - enhancement
@@ -26,7 +26,7 @@ changelog:
- title: Translation Changes - title: Translation Changes
labels: labels:
- Translation - Translation
- title: Other Changes - title: Other Changes
labels: labels:
- "*" - "*"

View File

@@ -4,30 +4,23 @@ on:
issue_comment: issue_comment:
types: [created] types: [created]
permissions:
contents: read
jobs: jobs:
check-comment: check-comment:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
pull-requests: read
issues: read
if: | if: |
github.event.issue.pull_request && github.event.issue.pull_request &&
( (
contains(github.event.comment.body, 'prdeploy') || contains(github.event.comment.body, 'prdeploy') ||
contains(github.event.comment.body, 'deploypr') contains(github.event.comment.body, 'deploypr')
) )
&& &&
( (
github.event.comment.user.login == 'frooodle' || github.event.comment.user.login == 'frooodle' ||
github.event.comment.user.login == 'sf298' || github.event.comment.user.login == 'sf298' ||
github.event.comment.user.login == 'Ludy87' || github.event.comment.user.login == 'Ludy87' ||
github.event.comment.user.login == 'LaserKaspar' || github.event.comment.user.login == 'LaserKaspar' ||
github.event.comment.user.login == 'sbplat' || github.event.comment.user.login == 'sbplat' ||
github.event.comment.user.login == 'reecebrowne' || github.event.comment.user.login == 'reecebrowne'
github.event.comment.user.login == 'DarioGii'
) )
outputs: outputs:
pr_number: ${{ steps.get-pr.outputs.pr_number }} pr_number: ${{ steps.get-pr.outputs.pr_number }}
@@ -35,14 +28,9 @@ jobs:
pr_ref: ${{ steps.get-pr-info.outputs.ref }} pr_ref: ${{ steps.get-pr-info.outputs.ref }}
steps: steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: Get PR data - name: Get PR data
id: get-pr id: get-pr
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 uses: actions/github-script@v7
with: with:
script: | script: |
const prNumber = context.payload.issue.number; const prNumber = context.payload.issue.number;
@@ -51,49 +39,41 @@ jobs:
- name: Get PR repository and ref - name: Get PR repository and ref
id: get-pr-info id: get-pr-info
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 uses: actions/github-script@v7
with: with:
script: | script: |
const { owner, repo } = context.repo; const { owner, repo } = context.repo;
const prNumber = context.payload.issue.number; const prNumber = context.payload.issue.number;
const { data: pr } = await github.rest.pulls.get({ const { data: pr } = await github.rest.pulls.get({
owner, owner,
repo, repo,
pull_number: prNumber, pull_number: prNumber,
}); });
// For forks, use the full repository name, for internal PRs use the current repo // For forks, use the full repository name, for internal PRs use the current repo
const repository = pr.head.repo.fork ? pr.head.repo.full_name : `${owner}/${repo}`; const repository = pr.head.repo.fork ? pr.head.repo.full_name : `${owner}/${repo}`;
console.log(`PR Repository: ${repository}`); console.log(`PR Repository: ${repository}`);
console.log(`PR Branch: ${pr.head.ref}`); console.log(`PR Branch: ${pr.head.ref}`);
core.setOutput('repository', repository); core.setOutput('repository', repository);
core.setOutput('ref', pr.head.ref); core.setOutput('ref', pr.head.ref);
deploy-pr: deploy-pr:
needs: check-comment needs: check-comment
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
steps: steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: Checkout PR - name: Checkout PR
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@v4
with: with:
repository: ${{ needs.check-comment.outputs.pr_repository }} repository: ${{ needs.check-comment.outputs.pr_repository }}
ref: ${{ needs.check-comment.outputs.pr_ref }} ref: ${{ needs.check-comment.outputs.pr_ref }}
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 uses: actions/setup-java@v4
with: with:
java-version: '17' java-version: '17'
distribution: 'temurin' distribution: 'temurin'
@@ -104,22 +84,20 @@ jobs:
DOCKER_ENABLE_SECURITY: false DOCKER_ENABLE_SECURITY: false
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 uses: docker/setup-buildx-action@v3
- name: Get version number - name: Get version number
id: versionNumber id: versionNumber
run: | run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }} password: ${{ secrets.DOCKER_HUB_API }}
- name: Build and push PR-specific image - name: Build and push PR-specific image
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
@@ -168,10 +146,10 @@ jobs:
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH' ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
# Create PR-specific directories # Create PR-specific directories
mkdir -p /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/{data,config,logs} mkdir -p /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/{data,config,logs}
# Move docker-compose file to correct location # Move docker-compose file to correct location
mv /tmp/docker-compose.yml /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/docker-compose.yml mv /tmp/docker-compose.yml /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/docker-compose.yml
# Start or restart the container # Start or restart the container
cd /stirling/PR-${{ needs.check-comment.outputs.pr_number }} cd /stirling/PR-${{ needs.check-comment.outputs.pr_number }}
docker-compose pull docker-compose pull
@@ -180,7 +158,7 @@ jobs:
- name: Post deployment URL to PR - name: Post deployment URL to PR
if: success() if: success()
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 uses: actions/github-script@v7
with: with:
script: | script: |
const { GITHUB_REPOSITORY } = process.env; const { GITHUB_REPOSITORY } = process.env;

View File

@@ -5,7 +5,8 @@ on:
types: [opened, synchronize, reopened, closed] types: [opened, synchronize, reopened, closed]
permissions: permissions:
contents: read contents: write
pull-requests: write
env: env:
SERVER_IP: ${{ secrets.VPS_IP }} # Add this to your GitHub secrets SERVER_IP: ${{ secrets.VPS_IP }} # Add this to your GitHub secrets
@@ -14,17 +15,9 @@ env:
jobs: jobs:
cleanup: cleanup:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
if: github.event.action == 'closed' if: github.event.action == 'closed'
steps: steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: Set up SSH - name: Set up SSH
run: | run: |
mkdir -p ~/.ssh/ mkdir -p ~/.ssh/
@@ -37,20 +30,20 @@ jobs:
CLEANUP_STATUS=$(ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH' CLEANUP_STATUS=$(ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
if [ -d "/stirling/PR-${{ github.event.pull_request.number }}" ]; then if [ -d "/stirling/PR-${{ github.event.pull_request.number }}" ]; then
echo "Found PR directory, proceeding with cleanup..." echo "Found PR directory, proceeding with cleanup..."
# Stop and remove containers # Stop and remove containers
cd /stirling/PR-${{ github.event.pull_request.number }} cd /stirling/PR-${{ github.event.pull_request.number }}
docker-compose down || true docker-compose down || true
# Go back to root before removal # Go back to root before removal
cd / cd /
# Remove PR-specific directories # Remove PR-specific directories
rm -rf /stirling/PR-${{ github.event.pull_request.number }} rm -rf /stirling/PR-${{ github.event.pull_request.number }}
# Remove the Docker image # Remove the Docker image
docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ github.event.pull_request.number }} || true docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ github.event.pull_request.number }} || true
echo "PERFORMED_CLEANUP" echo "PERFORMED_CLEANUP"
else else
echo "PR directory not found, nothing to clean up" echo "PR directory not found, nothing to clean up"
@@ -58,7 +51,7 @@ jobs:
fi fi
ENDSSH ENDSSH
) )
if [[ $CLEANUP_STATUS == *"PERFORMED_CLEANUP"* ]]; then if [[ $CLEANUP_STATUS == *"PERFORMED_CLEANUP"* ]]; then
echo "cleanup_performed=true" >> $GITHUB_OUTPUT echo "cleanup_performed=true" >> $GITHUB_OUTPUT
else else
@@ -67,7 +60,7 @@ jobs:
- name: Post cleanup notice to PR - name: Post cleanup notice to PR
if: steps.cleanup.outputs.cleanup_performed == 'true' if: steps.cleanup.outputs.cleanup_performed == 'true'
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 uses: actions/github-script@v7
with: with:
script: | script: |
const { GITHUB_REPOSITORY } = process.env; const { GITHUB_REPOSITORY } = process.env;

View File

@@ -3,24 +3,17 @@ on:
pull_request_target: pull_request_target:
types: [opened, synchronize] types: [opened, synchronize]
permissions:
contents: read
jobs: jobs:
labeler: labeler:
runs-on: ubuntu-latest
permissions: permissions:
contents: read
pull-requests: write pull-requests: write
runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - uses: actions/checkout@v4
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Apply Labels - name: Apply Labels
uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 uses: actions/labeler@v5
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/labeler-config.yml configuration-path: .github/labeler-config.yml

View File

@@ -6,15 +6,13 @@ on:
pull_request: pull_request:
branches: ["main"] branches: ["main"]
permissions:
contents: read
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
actions: read actions: read
contents: read
security-events: write security-events: write
strategy: strategy:
@@ -23,40 +21,22 @@ jobs:
jdk-version: [17, 21] jdk-version: [17, 21]
steps: steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@v4
- name: Set up JDK ${{ matrix.jdk-version }} - name: Set up JDK ${{ matrix.jdk-version }}
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 uses: actions/setup-java@v4
with: with:
java-version: ${{ matrix.jdk-version }} java-version: ${{ matrix.jdk-version }}
distribution: "temurin" distribution: "temurin"
- name: Build with Gradle and no spring security - name: Set up Gradle
run: ./gradlew clean build uses: gradle/actions/setup-gradle@v4
env:
DOCKER_ENABLE_SECURITY: false
- name: Build with Gradle and with spring security
run: ./gradlew clean build
env:
DOCKER_ENABLE_SECURITY: true
- name: Upload Test Reports
if: always()
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with: with:
name: test-reports-jdk-${{ matrix.jdk-version }} gradle-version: 8.7
path: |
build/reports/tests/ - name: Build with Gradle
build/test-results/ run: ./gradlew build --no-build-cache
build/reports/problems/
retention-days: 3
docker-compose-tests: docker-compose-tests:
# if: github.event_name == 'push' && github.ref == 'refs/heads/main' || # if: github.event_name == 'push' && github.ref == 'refs/heads/main' ||
@@ -76,36 +56,31 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@v4
- name: Set up Java 17 - name: Set up Java 17
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 uses: actions/setup-java@v4
with: with:
java-version: "17" java-version: "17"
distribution: "adopt" distribution: "adopt"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 uses: docker/setup-buildx-action@v3
- name: Install Docker Compose - name: Install Docker Compose
run: | run: |
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.32.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo curl -SL "https://github.com/docker/compose/releases/download/v2.29.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose
- name: Set up Python - name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 uses: actions/setup-python@v5
with: with:
python-version: "3.12" python-version: "3.7"
- name: Pip requirements - name: Pip requirements
run: | run: |
pip install --require-hashes -r ./cucumber/requirements.txt pip install -r ./cucumber/requirements.txt
- name: Run Docker Compose Tests - name: Run Docker Compose Tests
run: | run: |

View File

@@ -1,55 +1,42 @@
name: Check Properties Files on PR name: Check Properties Files
on: on:
pull_request_target: pull_request_target:
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
paths: paths:
- "src/main/resources/messages_*.properties" - "src/main/resources/messages_*.properties"
push:
permissions: branches: ["main"]
contents: read # Allow read access to repository content paths:
- "src/main/resources/messages_en_GB.properties"
jobs: jobs:
check-files: check-files:
if: github.event_name == 'pull_request_target' if: github.event_name == 'pull_request_target'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
issues: write # Allow posting comments on issues/PRs
pull-requests: write
steps: steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: Checkout main branch first - name: Checkout main branch first
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@v4
with:
ref: main
path: main-branch
fetch-depth: 0
- name: Checkout PR branch
uses: actions/checkout@v4
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{ github.event.pull_request.head.ref }}
path: pr-branch
fetch-depth: 0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 uses: actions/setup-python@v5
with: with:
python-version: "3.12" python-version: "3.x"
- name: Get PR data - name: Install GitHub CLI
id: get-pr-data run: sudo apt-get update && sudo apt-get install -y gh
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
script: |
const prNumber = context.payload.pull_request.number;
const repoOwner = context.payload.repository.owner.login;
const repoName = context.payload.repository.name;
const branch = context.payload.pull_request.head.ref;
console.log(`PR Number: ${prNumber}`);
console.log(`Repo Owner: ${repoOwner}`);
console.log(`Repo Name: ${repoName}`);
console.log(`Branch: ${branch}`);
core.setOutput("pr_number", prNumber);
core.setOutput("repo_owner", repoOwner);
core.setOutput("repo_name", repoName);
core.setOutput("branch", branch);
continue-on-error: true
- name: Fetch PR changed files - name: Fetch PR changed files
id: fetch-pr-changes id: fetch-pr-changes
@@ -57,125 +44,45 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
echo "Fetching PR changed files..." echo "Fetching PR changed files..."
cd pr-branch
gh repo set-default ${{ github.repository }}
# Store files in a safe way, only allowing valid properties files
echo "Getting list of changed files from PR..." echo "Getting list of changed files from PR..."
gh pr view ${{ steps.get-pr-data.outputs.pr_number }} --json files -q ".files[].path" | grep -E '^src/main/resources/messages_[a-zA-Z_]+\.properties$' > changed_files.txt # Filter only matching property files gh pr view ${{ github.event.pull_request.number }} --json files -q ".files[].path" | grep -E '^src/main/resources/messages_[a-zA-Z_]+\.properties$' > ../changed_files.txt
cd ..
- name: Determine reference file test echo "Processing changed files..."
mapfile -t CHANGED_FILES < changed_files.txt
CHANGED_FILES_STR="${CHANGED_FILES[*]}"
echo "CHANGED_FILES=${CHANGED_FILES_STR}" >> $GITHUB_ENV
echo "Changed files: ${CHANGED_FILES_STR}"
- name: Determine reference file
id: determine-file id: determine-file
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 run: |
with: echo "Determining reference file..."
script: | if grep -Fxq "src/main/resources/messages_en_GB.properties" changed_files.txt; then
const fs = require("fs"); echo "Using PR branch reference file"
const path = require("path"); echo "REFERENCE_FILE=pr-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
else
echo "Using main branch reference file"
echo "REFERENCE_FILE=main-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
fi
const prNumber = ${{ steps.get-pr-data.outputs.pr_number }}; - name: Show REFERENCE_FILE
const repoOwner = "${{ steps.get-pr-data.outputs.repo_owner }}"; run: echo "Reference file is set to ${REFERENCE_FILE}"
const repoName = "${{ steps.get-pr-data.outputs.repo_name }}";
const prRepoOwner = "${{ github.event.pull_request.head.repo.owner.login }}";
const prRepoName = "${{ github.event.pull_request.head.repo.name }}";
const branch = "${{ steps.get-pr-data.outputs.branch }}";
console.log(`Determining reference file for PR #${prNumber}`);
// Validate inputs
const validateInput = (input, regex, name) => {
if (!regex.test(input)) {
throw new Error(`Invalid ${name}: ${input}`);
}
};
validateInput(repoOwner, /^[a-zA-Z0-9_-]+$/, "repository owner");
validateInput(repoName, /^[a-zA-Z0-9._-]+$/, "repository name");
validateInput(branch, /^[a-zA-Z0-9._/-]+$/, "branch name");
// Get the list of changed files in the PR
const { data: files } = await github.rest.pulls.listFiles({
owner: repoOwner,
repo: repoName,
pull_number: prNumber,
});
// Filter for relevant files based on the PR changes
const changedFiles = files
.map(file => file.filename)
.filter(file => /^src\/main\/resources\/messages_[a-zA-Z_]+\.properties$/.test(file));
console.log("Changed files:", changedFiles);
// Create a temporary directory for PR files
const tempDir = "pr-branch";
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// Download and save each changed file
for (const file of changedFiles) {
const { data: fileContent } = await github.rest.repos.getContent({
owner: prRepoOwner,
repo: prRepoName,
path: file,
ref: branch,
});
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
const filePath = path.join(tempDir, file);
const dirPath = path.dirname(filePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
fs.writeFileSync(filePath, content);
console.log(`Saved file: ${filePath}`);
}
// Output the list of changed files for further processing
const fileList = changedFiles.join(" ");
core.exportVariable("FILES_LIST", fileList);
console.log("Files saved and listed in FILES_LIST.");
// Determine reference file
let referenceFilePath;
if (changedFiles.includes("src/main/resources/messages_en_GB.properties")) {
console.log("Using PR branch reference file.");
const { data: fileContent } = await github.rest.repos.getContent({
owner: prRepoOwner,
repo: prRepoName,
path: "src/main/resources/messages_en_GB.properties",
ref: branch,
});
referenceFilePath = "pr-branch-messages_en_GB.properties";
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
fs.writeFileSync(referenceFilePath, content);
} else {
console.log("Using main branch reference file.");
const { data: fileContent } = await github.rest.repos.getContent({
owner: repoOwner,
repo: repoName,
path: "src/main/resources/messages_en_GB.properties",
ref: "main",
});
referenceFilePath = "main-branch-messages_en_GB.properties";
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
fs.writeFileSync(referenceFilePath, content);
}
console.log(`Reference file path: ${referenceFilePath}`);
core.exportVariable("REFERENCE_FILE", referenceFilePath);
- name: Run Python script to check files - name: Run Python script to check files
id: run-check id: run-check
run: | run: |
echo "Running Python script to check files..." echo "Running Python script to check files..."
python .github/scripts/check_language_properties.py \ python main-branch/.github/scripts/check_language_properties.py \
--actor ${{ github.event.pull_request.user.login }} \ --actor ${{ github.event.pull_request.user.login }} \
--reference-file "${REFERENCE_FILE}" \ --reference-file "${REFERENCE_FILE}" \
--branch "pr-branch" \ --branch pr-branch \
--files "${FILES_LIST[@]}" > result.txt --files "${CHANGED_FILES[@]}" > result.txt || true
continue-on-error: true # Continue the job even if this step fails
- name: Capture output - name: Capture output
id: capture-output id: capture-output
@@ -188,7 +95,7 @@ jobs:
echo "EOF" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV
echo "${SCRIPT_OUTPUT}" echo "${SCRIPT_OUTPUT}"
# Determine job failure based on script output # Set FAIL_JOB to true if SCRIPT_OUTPUT contains ❌
if [[ "$SCRIPT_OUTPUT" == *"❌"* ]]; then if [[ "$SCRIPT_OUTPUT" == *"❌"* ]]; then
echo "FAIL_JOB=true" >> $GITHUB_ENV echo "FAIL_JOB=true" >> $GITHUB_ENV
else else
@@ -202,23 +109,23 @@ jobs:
- name: Post comment on PR - name: Post comment on PR
if: env.SCRIPT_OUTPUT != '' if: env.SCRIPT_OUTPUT != ''
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 uses: actions/github-script@v7
with: with:
script: | script: |
const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env; const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env;
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/'); const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
const issueNumber = context.issue.number; const prNumber = context.issue.number;
// Find existing comment // Find existing comment
const comments = await github.rest.issues.listComments({ const comments = await github.rest.issues.listComments({
owner: repoOwner, owner: repoOwner,
repo: repoName, repo: repoName,
issue_number: issueNumber issue_number: prNumber
}); });
const comment = comments.data.find(c => c.body.includes("## 🚀 Translation Verification Summary")); const comment = comments.data.find(c => c.body.includes("## 🚀 Translation Verification Summary"));
// Only update or create comments by the action user // Only allow the action user to update comments
const expectedActor = "github-actions[bot]"; const expectedActor = "github-actions[bot]";
if (comment && comment.user.login === expectedActor) { if (comment && comment.user.login === expectedActor) {
@@ -235,7 +142,7 @@ jobs:
await github.rest.issues.createComment({ await github.rest.issues.createComment({
owner: repoOwner, owner: repoOwner,
repo: repoName, repo: repoName,
issue_number: issueNumber, issue_number: prNumber,
body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n` body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
}); });
console.log("Created new comment."); console.log("Created new comment.");
@@ -248,3 +155,59 @@ jobs:
run: | run: |
echo "Failing the job because errors were detected." echo "Failing the job because errors were detected."
exit 1 exit 1
update-translations-main:
if: github.event_name == 'push'
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Run Python script to check files
id: run-check
run: |
echo "Running Python script to check files..."
python .github/scripts/check_language_properties.py \
--reference-file src/main/resources/messages_en_GB.properties \
--branch main
- name: Set up git config
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Add translation keys
run: |
git add src/main/resources/messages_*.properties
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
- name: Create Pull Request
id: cpr
if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Update translation files"
committer: GitHub Action <action@github.com>
author: GitHub Action <action@github.com>
signoff: true
branch: update_translation_files
title: "Update translation files"
add-paths: |
src/main/resources/messages_*.properties
body: |
Auto-generated by [create-pull-request][1]
[1]: https://github.com/peter-evans/create-pull-request
labels: Translation
draft: false
delete-branch: true
sign-commits: true

View File

@@ -1,79 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
#disable for now
#on:
# push:
# branches: ["main"]
# pull_request:
# The branches below must be a subset of the branches above
# branches: ["main"]
# schedule:
# - cron: "0 0 * * 1"
permissions:
contents: read
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ["java"]
# CodeQL supports [ $supported-codeql-languages ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
with:
category: "/language:${{matrix.language}}"

View File

@@ -1,27 +0,0 @@
# Dependency Review Action
#
# This Action will scan dependency manifest files that change as part of a Pull Request,
# surfacing known-vulnerable versions of the packages declared or updated in the PR.
# Once installed, if the workflow run is marked as required,
# PRs introducing known-vulnerable packages will be blocked from merging.
#
# Source repository: https://github.com/actions/dependency-review-action
name: 'Dependency Review'
on: [pull_request]
permissions:
contents: read
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: 'Checkout Repository'
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: 'Dependency Review'
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0

View File

@@ -8,30 +8,24 @@ on:
- "build.gradle" - "build.gradle"
permissions: permissions:
contents: read contents: write
pull-requests: write
jobs: jobs:
generate-license-report: generate-license-report:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
steps:
- name: Check out code - name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@v4
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 uses: actions/setup-java@v4
with: with:
java-version: "17" java-version: "17"
distribution: "adopt" distribution: "adopt"
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2 - uses: gradle/actions/setup-gradle@v4
- name: Run Gradle Command - name: Run Gradle Command
run: ./gradlew clean generateLicenseReport run: ./gradlew clean generateLicenseReport
@@ -53,7 +47,7 @@ jobs:
- name: Create Pull Request - name: Create Pull Request
id: cpr id: cpr
if: env.CHANGES_DETECTED == 'true' if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6 uses: peter-evans/create-pull-request@v6
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Update 3rd Party Licenses" commit-message: "Update 3rd Party Licenses"
@@ -66,10 +60,9 @@ jobs:
Auto-generated by [create-pull-request][1] Auto-generated by [create-pull-request][1]
[1]: https://github.com/peter-evans/create-pull-request [1]: https://github.com/peter-evans/create-pull-request
labels: licenses,github-actions labels: licenses
draft: false draft: false
delete-branch: true delete-branch: true
sign-commits: true
- name: Auto approve - name: Auto approve
if: steps.cpr.outputs.pull-request-operation == 'created' if: steps.cpr.outputs.pull-request-operation == 'created'
@@ -79,7 +72,7 @@ jobs:
- name: Enable auto-merge - name: Enable auto-merge
if: steps.cpr.outputs.pull-request-operation == 'created' if: steps.cpr.outputs.pull-request-operation == 'created'
uses: peter-evans/enable-pull-request-automerge@a660677d5469627102a1c1e11409dd063606628d # v3.0.0 uses: peter-evans/enable-pull-request-automerge@v3
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
pull-request-number: ${{ steps.cpr.outputs.pull-request-number }} pull-request-number: ${{ steps.cpr.outputs.pull-request-number }}

View File

@@ -6,25 +6,19 @@ on:
permissions: permissions:
contents: read contents: read
issues: write
jobs: jobs:
labeler: labeler:
name: Labeler name: Labeler
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
issues: write
steps: steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: Check out the repository - name: Check out the repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@v4
- name: Run Labeler - name: Run Labeler
uses: crazy-max/ghaction-github-labeler@b54af0c25861143e7c8813d7cbbf46d2c341680c # v5.1.0 uses: crazy-max/ghaction-github-labeler@v5
with: with:
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
yaml-file: .github/labels.yml yaml-file: .github/labels.yml
skip-delete: true skip-delete: true

View File

@@ -1,105 +0,0 @@
name: Test Installers Build
on:
workflow_dispatch:
release:
types: [created]
permissions:
contents: read
jobs:
build-installers:
strategy:
matrix:
include:
- os: windows-latest
platform: win
ext: exe
#- os: macos-latest
# platform: mac
# ext: dmg
#- os: ubuntu-latest
# platform: linux
# ext: deb
runs-on: ${{ matrix.os }}
permissions:
contents: write
packages: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK 21
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
with:
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
with:
gradle-version: 8.12
# Install Windows dependencies
- name: Install WiX Toolset
if: matrix.os == 'windows-latest'
run: |
curl -L -o wix.exe https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314.exe
.\wix.exe /install /quiet
# Install Linux dependencies
- name: Install Linux Dependencies
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y fakeroot rpm
# Get version number
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
shell: bash
- name: Get version number mac
id: versionNumberMac
run: echo "versionNumberMac=$(./gradlew printMacVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
shell: bash
# Build installer
- name: Build Installer
run: ./gradlew build jpackage -x test --info
env:
DOCKER_ENABLE_SECURITY: false
STIRLING_PDF_DESKTOP_UI: true
# Rename and collect artifacts based on OS
- name: Prepare artifacts
id: prepare
shell: bash
run: |
if [ "${{ matrix.os }}" = "windows-latest" ]; then
mv "build/jpackage/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.exe" "Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}"
elif [ "${{ matrix.os }}" = "macos-latest" ]; then
mv "build/jpackage/Stirling-PDF-${{ steps.versionNumberMac.outputs.versionNumberMac }}.dmg" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}"
else
mv "build/jpackage/stirling-pdf_${{ steps.versionNumber.outputs.versionNumber }}-1_amd64.deb" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}"
fi
# Upload installer as artifact for testing
- name: Upload Installer Artifact
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}
path: Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}
retention-days: 1
if-no-files-found: error
- name: Upload binaries to release
uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0
with:
files: ./Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}

View File

@@ -1,55 +0,0 @@
name: Pre-commit
on:
push:
branches: [main]
permissions:
contents: read
jobs:
update:
if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' }}
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: 3.12
- name: Run Pre-Commit Hooks
uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
continue-on-error: true
- name: Set up git config
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: git add
run: |
git add .
git diff --staged --quiet || git commit -m ":file_folder: pre-commit
> Made via .github/workflows/pre_commit.yml" || echo "pre-commit: no changes"
- name: Create Pull Request
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "ci: 🤖 format everything with pre-commit"
committer: GitHub Action <action@github.com>
author: GitHub Action <action@github.com>
signoff: true
branch: pre-commit
title: "🤖 format everything with pre-commit by <github-actions[bot]>"
body: |
Auto-generated by [create-pull-request][1]
[1]: https://github.com/peter-evans/create-pull-request
draft: false
delete-branch: true
labels: github-actions
sign-commits: true

View File

@@ -9,65 +9,52 @@ on:
permissions: permissions:
contents: read contents: read
packages: write
jobs: jobs:
push: push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
packages: write
id-token: write
steps: steps:
- name: Harden Runner - uses: actions/checkout@v4
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 uses: actions/setup-java@v4
with: with:
java-version: "17" java-version: "17"
distribution: "temurin" distribution: "temurin"
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2 - uses: gradle/actions/setup-gradle@v4
with: with:
gradle-version: 8.12 gradle-version: 8.7
- name: Run Gradle Command - name: Run Gradle Command
run: ./gradlew clean build run: ./gradlew clean build
env: env:
DOCKER_ENABLE_SECURITY: false DOCKER_ENABLE_SECURITY: false
- name: Install cosign
if: github.ref == 'refs/heads/master'
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
with:
cosign-release: 'v2.4.1'
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0 uses: docker/setup-buildx-action@v3
- name: Get version number - name: Get version number
id: versionNumber id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }} password: ${{ secrets.DOCKER_HUB_API }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ github.token }} password: ${{ github.token }}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0 uses: docker/setup-qemu-action@v3
- name: Convert repository owner to lowercase - name: Convert repository owner to lowercase
id: repoowner id: repoowner
@@ -75,7 +62,7 @@ jobs:
- name: Generate tags - name: Generate tags
id: meta id: meta
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1 uses: docker/metadata-action@v5
with: with:
images: | images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf ${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
@@ -88,8 +75,7 @@ jobs:
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }} type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push main Dockerfile - name: Build and push main Dockerfile
id: build-push-regular uses: docker/build-push-action@v6
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
with: with:
builder: ${{ steps.buildx.outputs.name }} builder: ${{ steps.buildx.outputs.name }}
context: . context: .
@@ -101,26 +87,10 @@ jobs:
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }} build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8 platforms: linux/amd64,linux/arm64/v8
provenance: true
sbom: true
- name: Sign regular images
if: github.ref == 'refs/heads/master'
env:
DIGEST: ${{ steps.build-push-regular.outputs.digest }}
TAGS: ${{ steps.meta.outputs.tags }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
cosign sign --yes \
--key env://COSIGN_PRIVATE_KEY \
"${tag}@${DIGEST}"
done
- name: Generate tags ultra-lite - name: Generate tags ultra-lite
id: meta2 id: meta2
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1 uses: docker/metadata-action@v5
if: github.ref != 'refs/heads/main' if: github.ref != 'refs/heads/main'
with: with:
images: | images: |
@@ -133,12 +103,11 @@ jobs:
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }} type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
- name: Build and push Dockerfile-ultra-lite - name: Build and push Dockerfile-ultra-lite
id: build-push-lite uses: docker/build-push-action@v6
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
if: github.ref != 'refs/heads/main' if: github.ref != 'refs/heads/main'
with: with:
context: . context: .
file: ./Dockerfile.ultra-lite file: ./Dockerfile-ultra-lite
push: true push: true
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
@@ -146,12 +115,10 @@ jobs:
labels: ${{ steps.meta2.outputs.labels }} labels: ${{ steps.meta2.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }} build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8 platforms: linux/amd64,linux/arm64/v8
provenance: true
sbom: true
- name: Generate tags fat - name: Generate tags fat
id: meta3 id: meta3
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1 uses: docker/metadata-action@v5
if: github.ref != 'refs/heads/main' if: github.ref != 'refs/heads/main'
with: with:
images: | images: |
@@ -164,13 +131,12 @@ jobs:
type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }} type=raw,value=latest-fat,enable=${{ github.ref == 'refs/heads/master' }}
- name: Build and push main Dockerfile fat - name: Build and push main Dockerfile fat
id: build-push-fat uses: docker/build-push-action@v6
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
if: github.ref != 'refs/heads/main' if: github.ref != 'refs/heads/main'
with: with:
builder: ${{ steps.buildx.outputs.name }} builder: ${{ steps.buildx.outputs.name }}
context: . context: .
file: ./Dockerfile.fat file: ./Dockerfile-fat
push: true push: true
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
@@ -178,17 +144,3 @@ jobs:
labels: ${{ steps.meta3.outputs.labels }} labels: ${{ steps.meta3.outputs.labels }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }} build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8 platforms: linux/amd64,linux/arm64/v8
provenance: true
sbom: true
- name: Sign fat images
if: github.ref == 'refs/heads/master'
env:
DIGEST: ${{ steps.build-push-fat.outputs.digest }}
TAGS: ${{ steps.meta3.outputs.tags }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
echo "$TAGS" | tr ',' '\n' | while read -r tag; do
cosign sign --key env://COSIGN_PRIVATE_KEY --yes "${tag}@${DIGEST}"
done

View File

@@ -4,16 +4,12 @@ on:
workflow_dispatch: workflow_dispatch:
release: release:
types: [created] types: [created]
permissions: permissions:
contents: read contents: write
packages: write
jobs: jobs:
push: push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
packages: write
strategy: strategy:
matrix: matrix:
enable_security: [true, false] enable_security: [true, false]
@@ -23,55 +19,49 @@ jobs:
- enable_security: false - enable_security: false
file_suffix: "" file_suffix: ""
steps: steps:
- name: Harden Runner - uses: actions/checkout@v4
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 uses: actions/setup-java@v4
with: with:
java-version: "17" java-version: "17"
distribution: "temurin" distribution: "temurin"
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2 - uses: gradle/actions/setup-gradle@v4
with: with:
gradle-version: 8.12 gradle-version: 8.7
- name: Generate jar (With Security=${{ matrix.enable_security }}) - name: Generate jar (With Security=${{ matrix.enable_security }})
run: ./gradlew clean createExe run: ./gradlew clean createExe
env: env:
DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }} DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }}
STIRLING_PDF_DESKTOP_UI: false
- name: Get version number - name: Get version number
id: versionNumber id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
- name: Rename binarie - name: Rename binarie
run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe if: matrix.file_suffix != ''
run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
- name: Upload Assets binarie - name: Upload Assets binarie
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 uses: actions/upload-artifact@v4
with: with:
path: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe path: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
name: Stirling-PDF-Server${{ matrix.file_suffix }}.exe name: Stirling-PDF${{ matrix.file_suffix }}.exe
overwrite: true overwrite: true
retention-days: 1 retention-days: 1
if-no-files-found: error if-no-files-found: error
- name: Upload binaries to release - name: Upload binaries to release
uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 uses: softprops/action-gh-release@v2
with: with:
files: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe files: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
- name: Rename jar binaries - name: Rename jar binaries
run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
- name: Upload Assets jar binaries - name: Upload Assets jar binaries
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 uses: actions/upload-artifact@v4
with: with:
path: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar path: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
name: Stirling-PDF${{ matrix.file_suffix }}.jar name: Stirling-PDF${{ matrix.file_suffix }}.jar
@@ -80,6 +70,6 @@ jobs:
if-no-files-found: error if-no-files-found: error
- name: Upload jar binaries to release - name: Upload jar binaries to release
uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 uses: softprops/action-gh-release@v2
with: with:
files: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar files: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar

View File

@@ -1,79 +0,0 @@
# This workflow uses actions that are not certified by GitHub. They are provided
# by a third-party and are governed by separate terms of service, privacy
# policy, and support documentation.
name: Scorecard supply-chain security
on:
# For Branch-Protection check. Only the default branch is supported. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
branch_protection_rule:
# To guarantee Maintained check is occasionally updated. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
schedule:
- cron: '20 7 * * 2'
push:
branches: ["main"]
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
# Needed to publish results and get a badge (see publish_results below).
id-token: write
contents: read
actions: read
# To allow GraphQL ListCommits to work
issues: read
pull-requests: read
# To detect SAST tools
checks: read
steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: "Checkout code"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
with:
results_file: results.sarif
results_format: sarif
# (Optional) "write" PAT token. Uncomment the `repo_token` line below if:
# - you want to enable the Branch-Protection check on a *public* repository, or
# - you are installing Scorecards on a *private* repository
# To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat.
# repo_token: ${{ secrets.SCORECARD_TOKEN }}
# Public repositories:
# - Publish results to OpenSSF REST API for easy access by consumers
# - Allows the repository to include the Scorecard badge.
# - See https://github.com/ossf/scorecard-action#publishing-results.
# For private repositories:
# - `publish_results` will always be set to `false`, regardless
# of the value entered here.
publish_results: true
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: SARIF file
path: results.sarif
retention-days: 5
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
with:
sarif_file: results.sarif

View File

@@ -5,9 +5,6 @@ on:
- cron: "30 0 * * *" - cron: "30 0 * * *"
workflow_dispatch: workflow_dispatch:
permissions:
contents: read
jobs: jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -15,13 +12,8 @@ jobs:
issues: write issues: write
pull-requests: write pull-requests: write
steps: steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: 30 days stale issues - name: 30 days stale issues
uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 uses: actions/stale@v9
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 30 days-before-stale: 30

View File

@@ -6,27 +6,19 @@ on:
branches: branches:
- master - master
permissions:
contents: read
jobs: jobs:
push: push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - uses: actions/checkout@v4
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0 uses: actions/setup-java@v4
with: with:
java-version: "17" java-version: "17"
distribution: "temurin" distribution: "temurin"
- uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2 - uses: gradle/actions/setup-gradle@v4
- name: Generate Swagger documentation - name: Generate Swagger documentation
run: ./gradlew generateOpenApiDocs run: ./gradlew generateOpenApiDocs

View File

@@ -10,25 +10,18 @@ on:
- "scripts/ignore_translation.toml" - "scripts/ignore_translation.toml"
permissions: permissions:
contents: read contents: write
pull-requests: write
jobs: jobs:
sync-readme: sync-readme:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps: steps:
- name: Harden Runner - uses: actions/checkout@v4
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 uses: actions/setup-python@v5
with: with:
python-version: "3.12" python-version: "3.x"
- name: Install dependencies - name: Install dependencies
run: pip install tomlkit run: pip install tomlkit
- name: Sync README - name: Sync README
@@ -43,7 +36,7 @@ jobs:
git diff --staged --quiet || git commit -m ":memo: Sync README git diff --staged --quiet || git commit -m ":memo: Sync README
> Made via sync_files.yml" || echo "no changes" > Made via sync_files.yml" || echo "no changes"
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6 uses: peter-evans/create-pull-request@v6
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Update files commit-message: Update files
@@ -59,4 +52,3 @@ jobs:
draft: false draft: false
delete-branch: true delete-branch: true
labels: Documentation,Translation,github-actions labels: Documentation,Translation,github-actions
sign-commits: true

View File

@@ -1,72 +0,0 @@
name: Update Translations
on:
push:
branches: ["main"]
paths:
- "src/main/resources/messages_en_GB.properties"
permissions:
contents: read
jobs:
update-translations-main:
if: github.event_name == 'push'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@0080882f6c36860b6ba35c610c98ce87d4e2f26f # v2.10.2
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Python
uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0
with:
python-version: "3.12"
- name: Run Python script to check files
id: run-check
run: |
echo "Running Python script to check files..."
python .github/scripts/check_language_properties.py \
--reference-file src/main/resources/messages_en_GB.properties \
--branch main
- name: Set up git config
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Add translation keys
run: |
git add src/main/resources/messages_*.properties
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
- name: Create Pull Request
id: cpr
if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Update translation files"
committer: GitHub Action <action@github.com>
author: GitHub Action <action@github.com>
signoff: true
branch: update_translation_files
title: "Update translation files"
add-paths: |
src/main/resources/messages_*.properties
body: |
Auto-generated by [create-pull-request][1]
[1]: https://github.com/peter-evans/create-pull-request
draft: false
delete-branch: true
labels: Translation,github-actions
sign-commits: true

21
.gitignore vendored
View File

@@ -146,37 +146,18 @@ out/
# cucumber # cucumber
/cucumber/reports/** /cucumber/reports/**
# Certs and Security Files # Certs
*.p12 *.p12
*.pk8
*.pem *.pem
*.crt *.crt
*.cer *.cer
*.cert
*.der *.der
*.key *.key
*.csr *.csr
*.kdbx
*.jks
*.asc
# SSH Keys
*.pub
*.priv
id_rsa
id_rsa.pub
id_ecdsa
id_ecdsa.pub
id_ed25519
id_ed25519.pub
.ssh/
*ssh
# cache # cache
.cache
.ruff_cache .ruff_cache
.mypy_cache .mypy_cache
.pytest_cache .pytest_cache
.ipynb_checkpoints .ipynb_checkpoints
**/jcef-bundle/

View File

@@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.4 rev: v0.2.1
hooks: hooks:
- id: ruff - id: ruff
args: args:
@@ -12,7 +12,7 @@ repos:
files: ^((.github/scripts|scripts)/.+)?[^/]+\.py$ files: ^((.github/scripts|scripts)/.+)?[^/]+\.py$
exclude: (split_photos.py) exclude: (split_photos.py)
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.3.0 rev: v2.2.6
hooks: hooks:
- id: codespell - id: codespell
args: args:
@@ -21,25 +21,6 @@ repos:
- --quiet-level=2 - --quiet-level=2
files: \.(properties|html|css|js|py|md)$ files: \.(properties|html|css|js|py|md)$
exclude: (.vscode|.devcontainer|src/main/resources|Dockerfile) exclude: (.vscode|.devcontainer|src/main/resources|Dockerfile)
- repo: https://github.com/gitleaks/gitleaks
rev: v8.22.0
hooks:
- id: gitleaks
- repo: https://github.com/jumanjihouse/pre-commit-hooks
rev: 3.0.0
hooks:
- id: shellcheck
files: ^.*(\.bash|\.sh|\.ksh|\.zsh)$
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: end-of-file-fixer
files: ^.*(\.js|\.java|\.py|\.yml)$
exclude: ^(.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js$)
- id: trailing-whitespace
files: ^.*(\.js|\.java|\.py|\.yml)$
exclude: ^(.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js$)
- repo: local - repo: local
hooks: hooks:
- id: check-duplicate-properties-keys - id: check-duplicate-properties-keys
@@ -47,11 +28,12 @@ repos:
entry: python .github/scripts/check_duplicates.py entry: python .github/scripts/check_duplicates.py
language: python language: python
files: ^(src)/.+\.properties$ files: ^(src)/.+\.properties$
- repo: local
hooks:
- id: check-html-tabs - id: check-html-tabs
name: Check HTML for tabs name: Check HTML for tabs
description: Ensures HTML/CSS/JS files do not contain tab characters
# args: ["--replace_with= "] # args: ["--replace_with= "]
entry: python .github/scripts/check_tabulator.py entry: python .github/scripts/check_tabulator.py
language: python language: python
exclude: ^(.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js$) exclude: ^(src/main/resources/static/pdfjs|src/main/resources/static/pdfjs-legacy)
files: ^.*(\.html|\.css|\.js)$ files: ^.*(\.html|\.css|\.js)$

View File

@@ -49,7 +49,5 @@
"editor.indentSize": "tabSize", "editor.indentSize": "tabSize",
"editor.stickyScroll.enabled": false, "editor.stickyScroll.enabled": false,
"editor.minimap.enabled": false, "editor.minimap.enabled": false,
"editor.formatOnSave": true, "editor.formatOnSave": true
"java.format.settings.google.mode": "jar-file",
"java.format.settings.google.extra": "--aosp --skip-sorting-imports"
} }

View File

@@ -18,9 +18,9 @@ For a detailed pull request tutorial, see [this guide](https://www.digitalocean.
Please make sure your Pull Request adheres to the following guidelines: Please make sure your Pull Request adheres to the following guidelines:
- Use the PR template provided. - Use the PR template provided.
- Keep your Pull Request title succinct, detailed, and to the point. - Keep your Pull Request title succinct, detailed and to the point.
- Keep commits atomic. One commit should contain one change. If you want to make multiple changes, submit multiple Pull Requests. - Keep commits atomic. One commit should contain one change. If you want to make multiple changes, submit multiple Pull Requests.
- Commits should be clear, concise, and easy to understand. - Commits should be clear, concise and easy to understand.
- References to the Issue number in the Pull Request and/or Commit message. - References to the Issue number in the Pull Request and/or Commit message.
## Translations ## Translations
@@ -29,15 +29,15 @@ If you would like to add or modify a translation, please see [How to add new lan
## Docs ## Docs
Documentation for Stirling-PDF is handled in a separate repository. Please see [Docs repository](https://github.com/Stirling-Tools/Stirling-Tools.github.io) or use the "edit this page"-button at the bottom of each page at [https://docs.stirlingpdf.com/](https://docs.stirlingpdf.com/). Documentation for Stirling-PDF is handled in a separate repository. Please see [Docs repository](https://github.com/Stirling-Tools/Stirling-Tools.github.io) or use "edit this page"-button at the bottom of each page at [https://docs.stirlingpdf.com/](https://docs.stirlingpdf.com/).
## Fixing Bugs or Adding a New Feature ## Fixing Bugs or Adding a New Feature
First, make sure you've read the section [Pull Requests](#pull-requests). First, make sure you've read the section [Pull Requests](#pull-requests).
If, at any point in time, you have a question, please feel free to ask in the same issue thread or in our [Discord](https://discord.gg/FJUSXUSYec). To build from source, please follow this [Guide](LocalRunGuide.md).
Developers should review our [Developer Guide](DeveloperGuide.md) If, at any point of time, you have a question, please feel free to ask in the same issue thread or in our [Discord](https://discord.gg/FJUSXUSYec).
## License ## License

View File

@@ -1,5 +1,12 @@
# New Database Backup and Import Functionality # New Database Backup and Import Functionality
> [!IMPORTANT]
> **Full activation will take place on approximately January 5th, 2025!**
Why is the waiting time six months?
There are users who only install updates sporadically; if they skip the preparation, it can/will lead to data loss in the database.
## Functionality Overview ## Functionality Overview
The newly introduced feature enhances the application with robust database backup and import capabilities. This feature is designed to ensure data integrity and provide a straightforward way to manage database backups. Here's how it works: The newly introduced feature enhances the application with robust database backup and import capabilities. This feature is designed to ensure data integrity and provide a straightforward way to manage database backups. Here's how it works:

View File

@@ -2,7 +2,7 @@
## 1. Introduction ## 1. Introduction
Stirling-PDF is a robust, locally hosted, web-based PDF manipulation tool. This guide focuses on Docker-based development and testing, which is the recommended approach for working with the full version of Stirling-PDF. Stirling-PDF is a robust, locally hosted web-based PDF manipulation tool. This guide focuses on Docker-based development and testing, which is the recommended approach for working with the full version of Stirling-PDF.
## 2. Project Overview ## 2. Project Overview
@@ -25,7 +25,7 @@ Stirling-PDF is built using:
- Docker - Docker
- Git - Git
- Java JDK 17 or later - Java JDK 17 or later
- Gradle 7.0 or later (Included within the repo) - Gradle 7.0 or later (Included within repo)
### Setup Steps ### Setup Steps
@@ -38,14 +38,14 @@ Stirling-PDF is built using:
2. Install Docker and JDK17 if not already installed. 2. Install Docker and JDK17 if not already installed.
3. Install a recommended Java IDE such as Eclipse, IntelliJ, or VSCode 3. Install a recommended Java IDE such as Eclipse, IntelliJ or VSCode
4. Lombok Setup 4. Lombok Setup
Stirling-PDF uses Lombok to reduce boilerplate code. Some IDEs, like Eclipse, don't support Lombok out of the box. To set up Lombok in your development environment: Stirling-PDF uses Lombok to reduce boilerplate code. Some IDEs, like Eclipse, don't support Lombok out of the box. To set up Lombok in your development environment:
Visit the [Lombok website](https://projectlombok.org/setup/) for installation instructions specific to your IDE. Visit the [Lombok website](https://projectlombok.org/setup/) for installation instructions specific to your IDE.
5. Add environment variable 5. Add environment variable
For local testing, you should generally be testing the full 'Security' version of Stirling-PDF. To do this, you must add the environment flag DOCKER_ENABLE_SECURITY=true to your system and/or IDE build/run step. For local testing you should generally be testing the full 'Security' version of Stirling-PDF to do this you must add the environment flag DOCKER_ENABLE_SECURITY=true to your system and/or IDE build/run step
## 4. Project Structure ## 4. Project Structure
@@ -86,8 +86,8 @@ Stirling-PDF/
│ └── SPDF/ │ └── SPDF/
├── build.gradle # Gradle build configuration ├── build.gradle # Gradle build configuration
├── Dockerfile # Main Dockerfile ├── Dockerfile # Main Dockerfile
├── Dockerfile.ultra-lite # Dockerfile for ultra-lite version ├── Dockerfile-ultra-lite # Dockerfile for ultra-lite version
├── Dockerfile.fat # Dockerfile for fat version ├── Dockerfile-fat # Dockerfile for fat version
├── docker-compose.yml # Docker Compose configuration ├── docker-compose.yml # Docker Compose configuration
└── test.sh # Test script to deploy all docker versions and run cuke tests └── test.sh # Test script to deploy all docker versions and run cuke tests
``` ```
@@ -102,7 +102,7 @@ Stirling-PDF offers several Docker versions:
### Example Docker Compose Files ### Example Docker Compose Files
Stirling-PDF provides several example Docker Compose files in the `exampleYmlFiles` directory, such as: Stirling-PDF provides several example Docker Compose files in the `exampleYmlFiles` directory such as :
- `docker-compose-latest.yml`: Latest version without security features - `docker-compose-latest.yml`: Latest version without security features
- `docker-compose-latest-security.yml`: Latest version with security features enabled - `docker-compose-latest-security.yml`: Latest version with security features enabled
@@ -179,14 +179,14 @@ Stirling-PDF uses different Docker images for various configurations. The build
For the ultra-lite version: For the ultra-lite version:
```bash ```bash
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite . docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite .
``` ```
For the fat version (with security enabled): For the fat version (with security enabled):
```bash ```bash
export DOCKER_ENABLE_SECURITY=true export DOCKER_ENABLE_SECURITY=true
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat . docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile-fat .
``` ```
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase
@@ -205,9 +205,9 @@ To run the test script:
This script performs the following actions: This script performs the following actions:
1. Builds all Docker images (full, ultra-lite, fat). 1. Builds all Docker images (full, ultra-lite, fat)
2. Runs each version to ensure it starts correctly. 2. Runs each version to ensure it starts correctly
3. Executes Cucumber tests against the main version and ensures feature compatibility. In the event these tests fail, your PR will not be merged. 3. Executes Cucumber tests against main version and ensures feature compatibility, in the event these tests fail your PR will not be merged
Note: The `test.sh` script will run automatically when you raise a PR. However, it's recommended to run it locally first to save resources and catch any issues early. Note: The `test.sh` script will run automatically when you raise a PR. However, it's recommended to run it locally first to save resources and catch any issues early.
@@ -229,7 +229,7 @@ For quick iterations and development of Java backend, JavaScript, and UI compone
To run Stirling-PDF locally: To run Stirling-PDF locally:
1. Compile and run the project using built-in IDE methods or by running: 1. Compile and run the project using built in IDE methods or by running:
```bash ```bash
./gradlew bootRun ./gradlew bootRun
@@ -261,7 +261,7 @@ Important notes:
6. Push your changes to your fork. 6. Push your changes to your fork.
7. Submit a pull request to the main repository. 7. Submit a pull request to the main repository.
8. See additional [contributing guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md). 8. See additional [contributing guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
When you raise a PR: When you raise a PR:
@@ -317,7 +317,7 @@ Remember to test your changes thoroughly to ensure they don't break any existing
### Overview of Thymeleaf ### Overview of Thymeleaf
Thymeleaf is a server-side Java HTML template engine. It is used in Stirling-PDF to render dynamic web pages. Thymeleaf integrates heavily with Spring Boot. Thymeleaf is a server-side Java HTML template engine. It is used in Stirling-PDF to render dynamic web pages. Thymeleaf integrates heavily with Spring Boot
### Thymeleaf overview ### Thymeleaf overview
@@ -327,24 +327,22 @@ Some examples of this are:
```html ```html
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block> <th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
```
or or
```html
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block> <th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
``` ```
Where it uses the `th:block`, `th:` indicating it's a special Thymeleaf element to be used server-side in generating the HTML, and block being the actual element type. Where it uses the th:block, th: indicating its a special thymeleaf element to be used serverside in generating the html, and block being the actual element type.
In this case, we are inserting the `navbar` entry within the `fragments/navbar.html` fragment into the `th:block` element. In this case we are inserting the ``navbar`` entry within the ``fragments/navbar.html`` fragment into the ``th:block`` element.
They can be more complex, such as: They can be more complex such as:
```html ```html
<th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}"></th:block> <th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}"></th:block>
``` ```
Which is the same as above but passes the parameters title and header into the fragment `common.html` to be used in its HTML generation. Which is the same as above but passes the parameters title and header into the fragment common.html to be used in its HTML generation
Thymeleaf can also be used to loop through objects or pass things from the Java side into the HTML side. Thymeleaf can also be used to loop through objects or pass things from java side into html side.
```java ```java
@GetMapping @GetMapping
@@ -354,7 +352,7 @@ Thymeleaf can also be used to loop through objects or pass things from the Java
} }
``` ```
In the above example, if exampleData is a list of plain java objects of class Person and within it, you had id, name, age, etc. You can reference it like so in above example if exampleData is a list of plain java objects of class Person and within it you had id, name, age etc. You can reference it like so
```html ```html
<tbody> <tbody>
@@ -454,7 +452,7 @@ This would generate n entries of tr for each person in exampleData
1. **Create a New Thymeleaf Template:** 1. **Create a New Thymeleaf Template:**
- Create a new HTML file in the `src/main/resources/templates` directory. - Create a new HTML file in the `src/main/resources/templates` directory.
- Use Thymeleaf attributes to dynamically generate content. - Use Thymeleaf attributes to dynamically generate content.
- Use `extract-page.html` as a base example for the HTML template, which is useful to ensure importing of the general layout, navbar, and footer. - Use `extract-page.html` as a base example for the HTML template, useful to ensure importing of the general layout, navbar and footer.
```html ```html
<!DOCTYPE html> <!DOCTYPE html>

View File

@@ -10,18 +10,6 @@ COPY build/libs/*.jar app.jar
ARG VERSION_TAG ARG VERSION_TAG
LABEL org.opencontainers.image.title="Stirling-PDF"
LABEL org.opencontainers.image.description="A powerful locally hosted web-based PDF manipulation tool supporting 50+ operations including merging, splitting, conversion, OCR, watermarking, and more."
LABEL org.opencontainers.image.source="https://github.com/Stirling-Tools/Stirling-PDF"
LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.vendor="Stirling-Tools"
LABEL org.opencontainers.image.url="https://www.stirlingpdf.com"
LABEL org.opencontainers.image.documentation="https://docs.stirlingpdf.com"
LABEL maintainer="Stirling-Tools"
LABEL org.opencontainers.image.authors="Stirling-Tools"
LABEL org.opencontainers.image.version="${VERSION_TAG}"
LABEL org.opencontainers.image.keywords="PDF, manipulation, merge, split, convert, OCR, watermark"
# Set Environment Variables # Set Environment Variables
ENV DOCKER_ENABLE_SECURITY=false \ ENV DOCKER_ENABLE_SECURITY=false \
VERSION_TAG=$VERSION_TAG \ VERSION_TAG=$VERSION_TAG \
@@ -31,7 +19,6 @@ ENV DOCKER_ENABLE_SECURITY=false \
PGID=1000 \ PGID=1000 \
UMASK=022 UMASK=022
# JDK for app # JDK for app
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \ echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \
@@ -70,7 +57,8 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
# User permissions # User permissions
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \ chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
tesseract --list-langs
EXPOSE 8080/tcp EXPOSE 8080/tcp

View File

@@ -1,5 +1,5 @@
# Build the application # Build the application
FROM gradle:8.12-jdk17 AS build FROM gradle:8.11-jdk17 AS build
# Set the working directory # Set the working directory
WORKDIR /app WORKDIR /app
@@ -73,7 +73,8 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
# User permissions # User permissions
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \ chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
tesseract --list-langs
EXPOSE 8080/tcp EXPOSE 8080/tcp

View File

@@ -1,5 +1,5 @@
# use alpine # use alpine
FROM alpine:3.21.0@sha256:21dc6063fd678b478f57c0e13f47560d0ea4eeba26dfc947b2a4f81f686b9f45 FROM alpine:3.20.3
ARG VERSION_TAG ARG VERSION_TAG

46
Endpoint-groups.md Normal file
View File

@@ -0,0 +1,46 @@
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | qpdf | Java | Javascript | Unoconv | tesseract |
| ------------------- | ------- | ------- | -------- | ----- | --- | ------ | ------ | ----------- | -------- | ---- | ---------- | ------- | ----------- |
| adjust-contrast | ✔️ | | | | | | | | | | ✔️ | | |
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | | | |
| crop | ✔️ | | | | | | | | | ✔️ | | | |
| extract-page | ✔️ | | | | | | | | | ✔️ | | | |
| merge-pdfs | ✔️ | | | | | | | | | ✔️ | | | |
| multi-page-layout | ✔️ | | | | | | | | | ✔️ | | | |
| pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ | | |
| pdf-to-single-page | ✔️ | | | | | | | | | ✔️ | | | |
| remove-pages | ✔️ | | | | | | | | | ✔️ | | | |
| rotate-pdf | ✔️ | | | | | | | | | ✔️ | | | |
| scale-pages | ✔️ | | | | | | | | | ✔️ | | | |
| split-pdfs | ✔️ | | | | | | | | | ✔️ | | | |
| file-to-pdf | | ✔️ | | | ✔️ | ✔️ | | ✔️ | | | | ✔️ | |
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | | | |
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
| pdf-to-img | | ✔️ | | | | ✔️ | | | | ✔️ | | | |
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | | | |
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | | | |
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
| pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
| pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
| add-password | | | ✔️ | | | | | | | ✔️ | | | |
| add-watermark | | | ✔️ | | | | | | | ✔️ | | | |
| cert-sign | | | ✔️ | | | | | | | ✔️ | | | |
| remove-cert-sign | | | ✔️ | | | | | | | ✔️ | | | |
| change-permissions | | | ✔️ | | | | | | | ✔️ | | | |
| remove-password | | | ✔️ | | | | | | | ✔️ | | | |
| sanitize-pdf | | | ✔️ | | | | | | | ✔️ | | | |
| add-image | | | | ✔️ | | | | | | ✔️ | | | |
| add-page-numbers | | | | ✔️ | | | | | | ✔️ | | | |
| auto-rename | | | | ✔️ | | | | | | ✔️ | | | |
| change-metadata | | | | ✔️ | | | | | | ✔️ | | | |
| compare | | | | ✔️ | | | | | | | ✔️ | | |
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | |
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | |
| extract-images | | | | ✔️ | | | | | | ✔️ | | | |
| flatten | | | | ✔️ | | | | | | | ✔️ | | |
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | | | |
| ocr-pdf | | | | ✔️ | ✔️ | | | | | | | | ✔ |
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | |
| repair | | | | ✔️ | ✔️ | | | ✔️ | ✔ | | | | |
| show-javascript | | | | ✔️ | | | | | | | ✔️ | | |
| sign | | | | ✔️ | | | | | | | ✔️ | | |

41
FolderScanning.md Normal file
View File

@@ -0,0 +1,41 @@
## User Guide for Local Directory Scanning and File Processing
### Setting Up Watched Folders
- Create a folder where you want your files to be monitored. This is your 'watched folder'.
- The default directory for this is `./pipeline/watchedFolders/`.
- Place any directories you want to be scanned into this folder. This folder should contain multiple folders, each for their own tasks and pipelines.
### Configuring Processing with JSON Files
- In each directory you want processed (e.g., `./pipeline/watchedFolders/officePrinter`), include a JSON configuration file.
- This JSON file should specify how you want the files in the directory to be handled (e.g., what operations to perform on them). This can be made, configured, and downloaded from the Stirling-PDF Pipeline interface.
### Automatic Scanning and Processing
- The system automatically checks the watched folder every minute for new directories and files to process.
- When a directory with a valid JSON configuration file is found, it begins processing the files inside according to the configuration.
### Processing Steps
- Files in each directory are processed according to the instructions in the JSON file.
- This might involve file conversions, data filtering, renaming files, etc. If the output of a step is a zip, this zip will be automatically unzipped as it passes to the next process.
### Results and Output
- After processing, the results are saved in a specified output location. This could be a different folder or location as defined in the JSON file or the default location `./pipeline/finishedFolders/`.
- Each processed file is named and organized according to the rules set in the JSON configuration.
### Completion and Cleanup
- Once processing is complete, the original files in the watched folder's directory are removed.
- You can find the processed files in the designated output location.
### Error Handling
- If there's an error during processing, the system will not delete the original files, allowing you to check and retry if necessary.
### User Interaction
- As a user, your main tasks are to set up the watched folders, place directories with files for processing, and create the corresponding JSON configuration files.
- The system handles the rest, including scanning, processing, and outputting results.

View File

@@ -92,9 +92,8 @@ Verify installation:
``tesseract --list-langs`` ``tesseract --list-langs``
You must then edit your ``/configs/settings.yml`` and change the system.tessdataDir to match the directory containing lang files You must then edit your ``/configs/settings.yml`` and change the system.tessdataDir to match the directory containing lang files
``` ```
system: system:
tessdataDir: C:/Program Files/Tesseract-OCR/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored. tessdataDir: C:/Program Files/Tesseract-OCR/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
``` ```

327
LocalRunGuide.md Normal file
View File

@@ -0,0 +1,327 @@
To run the application without Docker/Podman, you will need to manually install all dependencies and build the necessary components.
Note that some dependencies might not be available in the standard repositories of all Linux distributions, and may require additional steps to install.
The following guide assumes you have a basic understanding of using a command line interface in your operating system.
It should work on most Linux distributions and MacOS. For Windows, you might need to use Windows Subsystem for Linux (WSL) for certain steps. The amount of dependencies is to actually reduce overall size, i.e., installing LibreOffice subcomponents rather than the full LibreOffice package.
You could theoretically use a Distrobox/Toolbox if your distribution has old or not all packages. But you might just as well use the Docker container then.
### Step 1: Prerequisites
Install the following software, if not already installed:
- Java 17 or later (21 recommended)
- Gradle 7.0 or later (included within repo so not needed on server)
- Git
- Python 3.8 (with pip)
- Make
- GCC/G++
- Automake
- Autoconf
- libtool
- pkg-config
- zlib1g-dev
- libleptonica-dev
For Debian-based systems, you can use the following command:
```bash
sudo apt-get update
sudo apt-get install -y git automake autoconf libtool libleptonica-dev pkg-config zlib1g-dev make g++ openjdk-21-jdk python3 python3-pip
```
For Fedora-based systems use this command:
```bash
sudo dnf install -y git automake autoconf libtool leptonica-devel pkg-config zlib-devel make gcc-c++ java-21-openjdk python3 python3-pip
```
For non-root users with Nix Package Manager, use the following command:
```bash
nix-channel --update
nix-env -iA nixpkgs.jdk21 nixpkgs.git nixpkgs.python38 nixpkgs.gnumake nixpkgs.libgcc nixpkgs.automake nixpkgs.autoconf nixpkgs.libtool nixpkgs.pkg-config nixpkgs.zlib nixpkgs.leptonica
```
### Step 2: Clone and Build jbig2enc (Only required for certain OCR functionality)
For Debian and Fedora, you can build it from source using the following commands:
```bash
mkdir ~/.git
cd ~/.git && \
git clone https://github.com/agl/jbig2enc.git && \
cd jbig2enc && \
./autogen.sh && \
./configure && \
make && \
sudo make install
```
For Nix, you will face `Leptonica not detected`. Bypass this by installing it directly using the following command:
```bash
nix-env -iA nixpkgs.jbig2enc
```
### Step 3: Install Additional Software
Next we need to install LibreOffice for conversions, qpdf for OCR, and OpenCV for pattern recognition functionality.
Install the following software:
- libreoffice-core
- libreoffice-common
- libreoffice-writer
- libreoffice-calc
- libreoffice-impress
- python3-uno
- unoconv
- pngquant
- unpaper
- qpdf
- opencv-python-headless
For Debian-based systems, you can use the following command:
```bash
sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper qpdf
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint --break-system-packages
```
For Fedora:
```bash
sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper qpdf
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
```
For Nix:
```bash
nix-env -iA nixpkgs.unpaper nixpkgs.libreoffice nixpkgs.qpdf nixpkgs.poppler_utils
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
```
### Step 4: Clone and Build Stirling-PDF
```bash
cd ~/.git && \
git clone https://github.com/Stirling-Tools/Stirling-PDF.git && \
cd Stirling-PDF && \
chmod +x ./gradlew && \
./gradlew build
```
### Step 5: Move Jar to Desired Location
After the build process, a `.jar` file will be generated in the `build/libs` directory. You can move this file to a desired location, for example, `/opt/Stirling-PDF/`. You must also move the Script folder within the Stirling-PDF repo that you have downloaded to this directory. This folder is required for the Python scripts using OpenCV.
```bash
sudo mkdir /opt/Stirling-PDF && \
sudo mv ./build/libs/Stirling-PDF-*.jar /opt/Stirling-PDF/ && \
sudo mv scripts /opt/Stirling-PDF/ && \
echo "Scripts installed."
```
For non-root users, you can just keep the jar in the main directory of Stirling-PDF using the following command:
```bash
mv ./build/libs/Stirling-PDF-*.jar ./Stirling-PDF-*.jar
```
### Step 6: Other Files
#### OCR
If you plan to use the OCR (Optical Character Recognition) functionality, you might need to install language packs for Tesseract if running non-English scanning.
##### Installing Language Packs
The easiest method is to use the language packs provided by your repositories. Skip the other steps if they are available.
**Manual:**
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tessdata`
**IMPORTANT:** DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED.
**Debian-based systems**, install languages with this command:
```bash
sudo apt update && \
# All languages
# sudo apt install -y 'tesseract-ocr-*'
# Find languages:
apt search tesseract-ocr-
# View installed languages:
dpkg-query -W tesseract-ocr- | sed 's/tesseract-ocr-//g'
```
**Fedora:**
```bash
# All languages
# sudo dnf install -y tesseract-langpack-*
# Find languages:
dnf search -C tesseract-langpack-
# View installed languages:
rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g'
```
**Nix:**
```bash
nix-env -iA nixpkgs.tesseract
```
**Note:** Nix Package Manager pre-installs almost all the language packs when Tesseract is installed.
### Step 7: Run Stirling-PDF
Those who have pushed to the root directory, run the following commands:
```bash
./gradlew bootRun
or
java -jar /opt/Stirling-PDF/Stirling-PDF-*.jar
```
Since LibreOffice, soffice, and conversion tools have their dbus_tmp_dir set as `dbus_tmp_dir="/run/user/$(id -u)/libreoffice-dbus"`, you might get the following error when using their endpoints:
```
[Thread-7] INFO s.s.SPDF.utils.ProcessExecutor - mkdir: cannot create directory /run/user/1501: Permission denied
```
To resolve this, before starting Stirling-PDF, you have to set the environment variable to a directory you have write access to by using the following commands:
```bash
mkdir temp
export DBUS_SESSION_BUS_ADDRESS="unix:path=./temp"
./gradlew bootRun
or
java -jar ./Stirling-PDF-*.jar
```
### Step 8: Adding a Desktop Icon
This will add a modified app starter to your app menu.
```bash
location=$(pwd)/gradlew
image=$(pwd)/docs/stirling-transparent.svg
cat > ~/.local/share/applications/Stirling-PDF.desktop <<EOF
[Desktop Entry]
Name=Stirling PDF;
GenericName=Launch StirlingPDF and open its WebGUI;
Category=Office;
Exec=xdg-open http://localhost:8080 && nohup $location bootRun &;
Icon=$image;
Keywords=pdf;
Type=Application;
NoDisplay=false;
Terminal=true;
EOF
```
Note: Currently the app will run in the background until manually closed.
### Optional: Changing the Host and Port of the Application
To override the default configuration, you can add the following to `/.git/Stirling-PDF/configs/custom_settings.yml` file:
```yaml
server:
host: 0.0.0.0 # Not working - use instead address
address: 0.0.0.0
port: 3000
```
`-Djava.net.preferIPv4Stack=true` --> To force IPv4 only in the Java starting command
**Note:** This file is created after the first application launch. To have it before that, you can create the directory and add the file yourself.
### Optional: Run Stirling-PDF as a Service (requires root)
First create a `.env` file, where you can store environment variables:
```bash
touch /opt/Stirling-PDF/.env
```
In this file, you can add all variables, one variable per line, as stated in the main readme (for example `SYSTEM_DEFAULTLOCALE="de-DE"`).
Create a new file where we store our service settings and open it with the nano editor:
```bash
nano /etc/systemd/system/stirlingpdf.service
```
Paste this content, make sure to update the filename of the jar file. Press `Ctrl+S` and `Ctrl+X` to save and exit the nano editor:
```ini
[Unit]
Description=Stirling-PDF service
After=syslog.target network.target
[Service]
SuccessExitStatus=143
User=root
Group=root
Type=simple
EnvironmentFile=/opt/Stirling-PDF/.env
WorkingDirectory=/opt/Stirling-PDF
ExecStart=/usr/bin/java -jar Stirling-PDF-0.17.2.jar
ExecStop=/bin/kill -15 $MAINPID
[Install]
WantedBy=multi-user.target
```
Notify systemd that it has to rebuild its internal service database (you have to run this command every time you make a change in the service file):
```bash
sudo systemctl daemon-reload
```
Enable the service to tell it to start automatically:
```bash
sudo systemctl enable stirlingpdf.service
```
See the status of the service:
```bash
sudo systemctl status stirlingpdf.service
```
Manually start/stop/restart the service:
```bash
sudo systemctl start stirlingpdf.service
sudo systemctl stop stirlingpdf.service
sudo systemctl restart stirlingpdf.service
```
---
Remember to set the necessary environment variables before running the project if you want to customize the application. The list can be seen in the main readme.
You can do this in the terminal by using the `export` command or `-D` argument to the Java `-jar` command:
```bash
export APP_HOME_NAME="Stirling PDF"
or
-DAPP_HOME_NAME="Stirling PDF"

42
PipelineFeature.md Normal file
View File

@@ -0,0 +1,42 @@
# Pipeline Configuration and Usage Tutorial
- Configure the pipeline config file and input files to run files against it.
- For reuse, download the config file and re-upload it when needed, or place it in `/pipeline/defaultWebUIConfigs/` to auto-load in the web UI for all users.
## Steps to Configure and Use Your Pipeline
1. **Access Configuration**
- Upon entering the screen, click on the **Configure** button.
2. **Enter Pipeline Name**
- Provide a name for your pipeline in the designated field.
3. **Select Operations**
- Choose the operations for your pipeline (e.g., **Split Pages**), then click **Add Operation**.
4. **Configure Operation Settings**
- Input the necessary settings for each added operation. Settings are highlighted in yellow if customization is needed.
5. **Add More Operations**
- You can add and adjust the order of multiple operations. Ensure each operation's settings are customized.
6. **Save Settings**
- Click **Save Operation Settings** after customizing settings for each operation.
7. **Validate Pipeline**
- Use the **Validation** button to check your pipeline. A green indicator signifies correct setup; a pop-out error indicates issues.
8. **Download Pipeline Configuration**
- To use the configuration for folder scanning (or save it for future use and re-upload it), download a JSON file in this menu. You can also pre-load it for future use by placing it in `/pipeline/defaultWebUIConfigs/`. It will then appear in the dropdown menu for all users to use.
9. **Submit Files for Processing**
- If your pipeline is correctly set up, close the configure menu, input the files, and hit **Submit**.
10. **Note on Web UI Limitations**
- The current web UI version does not support operations that require multiple different types of inputs, such as adding a separate image to a PDF.
### Current Limitations
- Cannot have more than one of the same operation.
- Cannot input additional files via UI.
- All files and operations run in serial mode.

397
README.md
View File

@@ -2,9 +2,8 @@
<h1 align="center">Stirling-PDF</h1> <h1 align="center">Stirling-PDF</h1>
[![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf) [![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf)
[![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/HYmhKj45pU) [![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/Cn8pWhQRxZ)
[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/) [![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/Stirling-Tools/Stirling-PDF/badge)](https://scorecard.dev/viewer/?uri=github.com/Stirling-Tools/Stirling-PDF)
[![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](https://github.com/Stirling-Tools/stirling-pdf) [![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](https://github.com/Stirling-Tools/stirling-pdf)
<a href="https://www.producthunt.com/posts/stirling-pdf?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-stirling&#0045;pdf" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=641239&theme=light" alt="Stirling&#0032;PDF - Open&#0032;source&#0032;locally&#0032;hosted&#0032;web&#0032;PDF&#0032;editor | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a> <a href="https://www.producthunt.com/posts/stirling-pdf?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-stirling&#0045;pdf" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=641239&theme=light" alt="Stirling&#0032;PDF - Open&#0032;source&#0032;locally&#0032;hosted&#0032;web&#0032;PDF&#0032;editor | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
@@ -12,31 +11,29 @@
[Stirling-PDF](https://www.stirlingpdf.com) is a robust, locally hosted web-based PDF manipulation tool using Docker. It enables you to carry out various operations on PDF files, including splitting, merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application has evolved to encompass a comprehensive set of features, addressing all your PDF requirements. [Stirling-PDF](https://www.stirlingpdf.com) is a robust, locally hosted web-based PDF manipulation tool using Docker. It enables you to carry out various operations on PDF files, including splitting, merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application has evolved to encompass a comprehensive set of features, addressing all your PDF requirements.
Stirling-PDF does not initiate any outbound calls for record-keeping or tracking purposes.
All files and PDFs exist either exclusively on the client side, reside in server memory only during task execution, or temporarily reside in a file solely for the execution of the task. Any file downloaded by the user will have been deleted from the server by that point. All files and PDFs exist either exclusively on the client side, reside in server memory only during task execution, or temporarily reside in a file solely for the execution of the task. Any file downloaded by the user will have been deleted from the server by that point.
Homepage: [https://stirlingpdf.com](https://stirlingpdf.com)
All documentation available at [https://docs.stirlingpdf.com/](https://docs.stirlingpdf.com/)
![stirling-home](images/stirling-home.jpg) ![stirling-home](images/stirling-home.jpg)
## Features ## Features
- 50+ PDF Operations - Enterprise features like SSO Check [here](https://docs.stirlingpdf.com/Enterprise%20Edition)
- Parallel file processing and downloads
- Dark mode support - Dark mode support
- Custom download options - Custom download options
- Custom 'Pipelines' to run multiple features in a automated queue - Parallel file processing and downloads
- Custom 'Pipelines' to run multiple features in a queue
- API for integration with external scripts - API for integration with external scripts
- Optional Login and Authentication support (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/System%20and%20Security) for documentation) - Optional Login and Authentication support (see [here](https://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation)
- Database Backup and Import (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/DATABASE) for documentation) - Database Backup and Import (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DATABASE.md) for documentation)
- Enterprise features like SSO see [here](https://docs.stirlingpdf.com/Enterprise%20Edition)
## PDF Features ## PDF Features
### Page Operations ### Page Operations
- View and modify PDFs - View multi-page PDFs with custom viewing, sorting, and searching. Plus, on-page edit features like annotating, drawing, and adding text and images. (Using PDF.js with Joxit and Liberation fonts) - View and modify PDFs - View multi-page PDFs with custom viewing, sorting, and searching. Plus on-page edit features like annotate, draw, and adding text and images. (Using PDF.js with Joxit and Liberation fonts)
- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages - Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages
- Merge multiple PDFs into a single resultant file - Merge multiple PDFs into a single resultant file
- Split PDFs into multiple files at specified page numbers or extract all pages as individual files - Split PDFs into multiple files at specified page numbers or extract all pages as individual files
@@ -47,11 +44,11 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
- Scale page contents size by set percentage - Scale page contents size by set percentage
- Adjust contrast - Adjust contrast
- Crop PDF - Crop PDF
- Auto-split PDF (with physically scanned page dividers) - Auto split PDF (with physically scanned page dividers)
- Extract page(s) - Extract page(s)
- Convert PDF to a single page - Convert PDF to a single page
- Overlay PDFs on top of each other - Overlay PDFs on top of each other
- PDF to a single page - PDF to single page
- Split PDF by sections - Split PDF by sections
### Conversion Operations ### Conversion Operations
@@ -60,7 +57,7 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
- Convert any common file to PDF (using LibreOffice) - Convert any common file to PDF (using LibreOffice)
- Convert PDF to Word/PowerPoint/others (using LibreOffice) - Convert PDF to Word/PowerPoint/others (using LibreOffice)
- Convert HTML to PDF - Convert HTML to PDF
- Convert PDF to XML - Convert PDF to xml
- Convert PDF to CSV - Convert PDF to CSV
- URL to PDF - URL to PDF
- Markdown to PDF - Markdown to PDF
@@ -88,87 +85,343 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
- Extract images from scans - Extract images from scans
- Remove annotations - Remove annotations
- Add page numbers - Add page numbers
- Auto-rename files by detecting PDF header text - Auto rename file by detecting PDF header text
- OCR on PDF (using Tesseract OCR) - OCR on PDF (using tesseract)
- PDF/A conversion (using LibreOffice) - PDF/A conversion (using libreoffice)
- Edit metadata - Edit metadata
- Flatten PDFs - Flatten PDFs
- Get all information on a PDF to view or export as JSON - Get all information on a PDF to view or export as JSON
- Show/detect embedded JavaScript - Show/detect embedded JavaScript
For an overview of the tasks and the technology each uses, please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md).
A demo of the app is available [here](https://stirlingpdf.io).
## Technologies Used
# 📖 Get Started - Spring Boot + Thymeleaf
- [PDFBox](https://github.com/apache/pdfbox/tree/trunk)
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
- [qpdf](https://github.com/qpdf/qpdf)
- HTML, CSS, JavaScript
- Docker
- [PDF.js](https://github.com/mozilla/pdf.js)
- [PDF-LIB.js](https://github.com/Hopding/pdf-lib)
Visit our comprehensive documentation at [docs.stirlingpdf.com](https://docs.stirlingpdf.com) for: ## How to Use
- Installation guides for all platforms ### Windows
- Configuration options
- Feature documentation
- API reference
- Security setup
- Enterprise features
For Windows users, download the latest Stirling-PDF.exe from our [release](https://github.com/Stirling-Tools/Stirling-PDF/releases) section or by clicking [here](https://github.com/Stirling-Tools/Stirling-PDF/releases/latest/download/Stirling-PDF.exe).
### Locally
Please view the [LocalRunGuide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LocalRunGuide.md).
### Docker / Podman
> [!NOTE]
> <https://hub.docker.com/r/stirlingtools/stirling-pdf>
Stirling-PDF has three different versions: a full version, an ultra-lite version, and a 'fat' version. Depending on the types of features you use, you may want a smaller image to save on space. To see what the different versions offer, please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md). For people that don't mind space optimization, just use the latest tag.
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/stirlingtools/stirling-pdf/latest?label=Stirling-PDF%20Full)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/stirlingtools/stirling-pdf/latest-ultra-lite?label=Stirling-PDF%20Ultra-Lite)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/stirlingtools/stirling-pdf/latest-fat?label=Stirling-PDF%20Fat)
Please note in the examples below, you may need to change the volume paths as needed, e.g., `./extraConfigs:/configs` to `/opt/stirlingpdf/extraConfigs:/configs`.
### Docker Run
```bash
docker run -d \
-p 8080:8080 \
-v ./trainingData:/usr/share/tessdata \
-v ./extraConfigs:/configs \
-v ./logs:/logs \
# Optional customization (not required)
# -v /location/of/customFiles:/customFiles \
-e DOCKER_ENABLE_SECURITY=false \
-e INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
-e LANGS=en_GB \
--name stirling-pdf \
stirlingtools/stirling-pdf:latest
```
### Docker Compose
```yaml
version: '3.3'
services:
stirling-pdf:
image: stirlingtools/stirling-pdf:latest
ports:
- '8080:8080'
volumes:
- ./trainingData:/usr/share/tessdata # Required for extra OCR languages
- ./extraConfigs:/configs
# - ./customFiles:/customFiles/
# - ./logs:/logs/
environment:
- DOCKER_ENABLE_SECURITY=false
- INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
- LANGS=en_GB
```
Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "podman".
### Kubernetes
See the kubernetes helm chart [here](https://github.com/Stirling-Tools/Stirling-PDF-chart)
## Enable OCR/Compression Feature
Please view the [HowToUseOCR.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md).
## Reuse Stored Files
Certain functionality like `Sign` supports pre-saved files stored at `/customFiles/signatures/`. Image files placed within here will be accessible to be used via the web UI. Currently, this supports two folder types:
- `/customFiles/signatures/ALL_USERS`: Accessible to all users, useful for organizations where many users use the same files or for users not using authentication
- `/customFiles/signatures/{username}`: Such as `/customFiles/signatures/froodle`, accessible only to the `froodle` username, private for all others
## Supported Languages ## Supported Languages
Stirling-PDF currently supports 38 languages! Stirling-PDF currently supports 37 languages!
| Language | Progress | | Language | Progress |
| -------------------------------------------- | -------------------------------------- | | -------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![91%](https://geps.dev/progress/91) | | Arabic (العربية) (ar_AR) | ![98%](https://geps.dev/progress/98) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![89%](https://geps.dev/progress/89) | | Azerbaijani (Azərbaycan Dili) (az_AZ) | ![97%](https://geps.dev/progress/97) |
| Basque (Euskara) (eu_ES) | ![51%](https://geps.dev/progress/51) | | Basque (Euskara) (eu_ES) | ![54%](https://geps.dev/progress/54) |
| Bulgarian (Български) (bg_BG) | ![87%](https://geps.dev/progress/87) | | Bulgarian (Български) (bg_BG) | ![94%](https://geps.dev/progress/94) |
| Catalan (Català) (ca_CA) | ![81%](https://geps.dev/progress/81) | | Catalan (Català) (ca_CA) | ![88%](https://geps.dev/progress/88) |
| Croatian (Hrvatski) (hr_HR) | ![88%](https://geps.dev/progress/88) | | Croatian (Hrvatski) (hr_HR) | ![96%](https://geps.dev/progress/96) |
| Czech (Česky) (cs_CZ) | ![88%](https://geps.dev/progress/88) | | Czech (Česky) (cs_CZ) | ![95%](https://geps.dev/progress/95) |
| Danish (Dansk) (da_DK) | ![87%](https://geps.dev/progress/87) | | Danish (Dansk) (da_DK) | ![94%](https://geps.dev/progress/94) |
| Dutch (Nederlands) (nl_NL) | ![86%](https://geps.dev/progress/86) | | Dutch (Nederlands) (nl_NL) | ![94%](https://geps.dev/progress/94) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | | English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) | | English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| French (Français) (fr_FR) | ![93%](https://geps.dev/progress/93) | | French (Français) (fr_FR) | ![97%](https://geps.dev/progress/97) |
| German (Deutsch) (de_DE) | ![96%](https://geps.dev/progress/96) | | German (Deutsch) (de_DE) | ![97%](https://geps.dev/progress/97) |
| Greek (Ελληνικά) (el_GR) | ![87%](https://geps.dev/progress/87) | | Greek (Ελληνικά) (el_GR) | ![95%](https://geps.dev/progress/95) |
| Hindi (हिंदी) (hi_IN) | ![85%](https://geps.dev/progress/85) | | Hindi (हिंदी) (hi_IN) | ![93%](https://geps.dev/progress/93) |
| Hungarian (Magyar) (hu_HU) | ![88%](https://geps.dev/progress/88) | | Hungarian (Magyar) (hu_HU) | ![95%](https://geps.dev/progress/95) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![88%](https://geps.dev/progress/88) | | Indonesian (Bahasa Indonesia) (id_ID) | ![95%](https://geps.dev/progress/95) |
| Irish (Gaeilge) (ga_IE) | ![80%](https://geps.dev/progress/80) | | Irish (Gaeilge) (ga_IE) | ![86%](https://geps.dev/progress/86) |
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) | | Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
| Japanese (日本語) (ja_JP) | ![90%](https://geps.dev/progress/90) | | Japanese (日本語) (ja_JP) | ![83%](https://geps.dev/progress/83) |
| Korean (한국어) (ko_KR) | ![86%](https://geps.dev/progress/86) | | Korean (한국어) (ko_KR) | ![93%](https://geps.dev/progress/93) |
| Norwegian (Norsk) (no_NB) | ![80%](https://geps.dev/progress/80) | | Norwegian (Norsk) (no_NB) | ![85%](https://geps.dev/progress/85) |
| Persian (فارسی) (fa_IR) | ![95%](https://geps.dev/progress/95) | | Polish (Polski) (pl_PL) | ![94%](https://geps.dev/progress/94) |
| Polish (Polski) (pl_PL) | ![87%](https://geps.dev/progress/87) | | Portuguese (Português) (pt_PT) | ![95%](https://geps.dev/progress/95) |
| Portuguese (Português) (pt_PT) | ![87%](https://geps.dev/progress/87) | | Portuguese Brazilian (Português) (pt_BR) | ![96%](https://geps.dev/progress/96) |
| Portuguese Brazilian (Português) (pt_BR) | ![95%](https://geps.dev/progress/95) | | Romanian (Română) (ro_RO) | ![88%](https://geps.dev/progress/88) |
| Romanian (Română) (ro_RO) | ![82%](https://geps.dev/progress/82) | | Russian (Русский) (ru_RU) | ![95%](https://geps.dev/progress/95) |
| Russian (Русский) (ru_RU) | ![87%](https://geps.dev/progress/87) | | Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![68%](https://geps.dev/progress/68) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![64%](https://geps.dev/progress/64) | | Simplified Chinese (简体中文) (zh_CN) | ![89%](https://geps.dev/progress/89) |
| Simplified Chinese (简体中文) (zh_CN) | ![90%](https://geps.dev/progress/90) | | Slovakian (Slovensky) (sk_SK) | ![80%](https://geps.dev/progress/80) |
| Slovakian (Slovensky) (sk_SK) | ![75%](https://geps.dev/progress/75) | | Spanish (Español) (es_ES) | ![96%](https://geps.dev/progress/96) |
| Spanish (Español) (es_ES) | ![88%](https://geps.dev/progress/88) | | Swedish (Svenska) (sv_SE) | ![94%](https://geps.dev/progress/94) |
| Swedish (Svenska) (sv_SE) | ![88%](https://geps.dev/progress/88) | | Thai (ไทย) (th_TH) | ![94%](https://geps.dev/progress/94) |
| Thai (ไทย) (th_TH) | ![87%](https://geps.dev/progress/87) |
| Tibetan (བོད་ཡིག་) (zh_BO) | ![96%](https://geps.dev/progress/96) |
| Traditional Chinese (繁體中文) (zh_TW) | ![96%](https://geps.dev/progress/96) | | Traditional Chinese (繁體中文) (zh_TW) | ![96%](https://geps.dev/progress/96) |
| Turkish (Türkçe) (tr_TR) | ![83%](https://geps.dev/progress/83) | | Turkish (Türkçe) (tr_TR) | ![90%](https://geps.dev/progress/90) |
| Ukrainian (Українська) (uk_UA) | ![73%](https://geps.dev/progress/73) | | Ukrainian (Українська) (uk_UA) | ![78%](https://geps.dev/progress/78) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![80%](https://geps.dev/progress/80) | | Vietnamese (Tiếng Việt) (vi_VN) | ![86%](https://geps.dev/progress/86) |
## Contributing (Creating Issues, Translations, Fixing Bugs, etc.)
Please see our [Contributing Guide](CONTRIBUTING.md).
## Stirling PDF Enterprise ## Stirling PDF Enterprise
Stirling PDF offers an Enterprise edition of its software. This is the same great software but with added features, support and comforts. Stirling PDF offers a Enterprise edition of its software, This is the same great software but with added features and comforts
Check out our [Enterprise docs](https://docs.stirlingpdf.com/Enterprise%20Edition)
### Whats included
- Prioritised Support tickets via support@stirlingpdf.com to reach directly to Stirling-PDF team for support and 1:1 meetings where applicable (Provided they come from same email domain registered with us)
- Prioritised Enhancements to Stirling-PDF where applicable
- Base SSO support
- Advanced SSO such as automated login handling (Coming very soon)
- SAML SSO (Coming very soon)
- Custom automated metadata handling
- Advanced user configurations (Coming soon)
- Plus other exciting features to come
Check out of [docs](https://docs.stirlingpdf.com/Enterprise%20Edition) on it or our official [website](https://www.stirlingpdf.com)
## Customization
Stirling-PDF allows easy customization of the app, including things like:
- Custom application name
- Custom slogans, icons, HTML, images, CSS, etc. (via file overrides)
There are two options for this, either using the generated settings file `settings.yml`, which is located in the `/configs` directory and follows standard YAML formatting, or using environment variables, which would override the settings file.
For example, in `settings.yml`, you might have:
```yaml
security:
enableLogin: 'true'
```
To have this via an environment variable, you would use `SECURITY_ENABLELOGIN`.
The current list of settings is:
```yaml
security:
enableLogin: false # set to 'true' to enable login
csrfDisabled: true # set to 'true' to disable CSRF protection (not recommended for production)
loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
loginMethod: all # 'all' (Login Username/Password and OAuth2[must be enabled and configured]), 'normal'(only Login with Username/Password) or 'oauth2'(only Login with OAuth2)
initialLogin:
username: '' # initial username for the first login
password: '' # initial password for the first login
oauth2:
enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
client:
keycloak:
issuer: '' # URL of the Keycloak realm's OpenID Connect Discovery endpoint
clientId: '' # client ID for Keycloak OAuth2
clientSecret: '' # client secret for Keycloak OAuth2
scopes: openid, profile, email # scopes for Keycloak OAuth2
useAsUsername: preferred_username # field to use as the username for Keycloak OAuth2
google:
clientId: '' # client ID for Google OAuth2
clientSecret: '' # client secret for Google OAuth2
scopes: https://www.googleapis.com/auth/userinfo.email, https://www.googleapis.com/auth/userinfo.profile # scopes for Google OAuth2
useAsUsername: email # field to use as the username for Google OAuth2
github:
clientId: '' # client ID for GitHub OAuth2
clientSecret: '' # client secret for GitHub OAuth2
scopes: read:user # scope for GitHub OAuth2
useAsUsername: login # field to use as the username for GitHub OAuth2
issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) endpoint
clientId: '' # client ID from your provider
clientSecret: '' # client secret from your provider
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
useAsUsername: email # default is 'email'; custom fields can be used as the username
scopes: openid, profile, email # specify the scopes for which the application will request permissions
provider: google # set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
saml2:
enabled: false # currently in alpha, not recommended for use yet, enableAlphaFunctionality must be set to true
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
registrationId: stirling
idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata
idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml
idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml
idpIssuer: http://www.okta.com/externalKey
idpCert: classpath:okta.crt
privateKey: classpath:saml-private-key.key
spCert: classpath:saml-public-cert.crt
enterpriseEdition:
enabled: false # set to 'true' to enable enterprise edition
key: 00000000-0000-0000-0000-000000000000
CustomMetadata:
autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values
author: username # supports text such as 'John Doe' or types such as username to autopopulate with user's username
creator: Stirling-PDF # supports text such as 'Company-PDF'
producer: Stirling-PDF # supports text such as 'Company-PDF'
legal:
termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms). Empty string to disable or filename to load from local file in static folder
privacyPolicy: https://www.stirlingpdf.com/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy). Empty string to disable or filename to load from local file in static folder
accessibilityStatement: '' # URL to the accessibility statement of your application (e.g. https://example.com/accessibility). Empty string to disable or filename to load from local file in static folder
cookiePolicy: '' # URL to the cookie policy of your application (e.g. https://example.com/cookie). Empty string to disable or filename to load from local file in static folder
impressum: '' # URL to the impressum of your application (e.g. https://example.com/impressum). Empty string to disable or filename to load from local file in static folder
system:
defaultLocale: en-US # set the default language (e.g. 'de-DE', 'fr-FR', etc)
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
enableAlphaFunctionality: false # set to enable functionality which might need more testing before it fully goes live (this feature might make no changes)
showUpdate: false # see when a new update is available
showUpdateOnlyAdmin: false # only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template HTML files
tessdataDir: /usr/share/tessdata # path to the directory containing the Tessdata files. This setting is relevant for Windows systems. For Windows users, this path should be adjusted to point to the appropriate directory where the Tessdata files are stored.
enableAnalytics: undefined # set to 'true' to enable analytics, set to 'false' to disable analytics; for enterprise users, this is set to true
ui:
appName: '' # application's visible name
homeDescription: '' # short description or tagline shown on the homepage
appNameNavbar: '' # name displayed on the navigation bar
endpoints:
toRemove: [] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
groupsToRemove: [] # list groups to disable (e.g. ['LibreOffice'])
metrics:
enabled: true # 'true' to enable Info APIs (`/api/*`) endpoints, 'false' to disable
# Automatically Generated Settings (Do Not Edit Directly)
AutomaticallyGenerated:
key: example
UUID: example
```
There is an additional config file `/configs/custom_settings.yml` where users familiar with Java and Spring `application.properties` can input their own settings on top of Stirling-PDF's existing ones.
## 🤝 Looking to contribute?
Join our community: ### Extra Notes
- [Contribution Guidelines](CONTRIBUTING.md)
- [Translation Guide (How to add custom languages)](HowToAddNewLanguage.md) - **Endpoints**: Currently, the `ENDPOINTS_TO_REMOVE` and `GROUPS_TO_REMOVE` endpoints can include comma-separated lists of endpoints and groups to disable. For example, `ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages` would disable both image-to-pdf and remove pages, while `GROUPS_TO_REMOVE=LibreOffice` would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md).
- [Issue Tracker](https://github.com/Stirling-Tools/Stirling-PDF/issues) - **customStaticFilePath**: Customize static files such as the app logo by placing files in the `/customFiles/static/` directory. An example of customizing the app logo is placing `/customFiles/static/favicon.svg` to override the current SVG. This can be used to change any `images/icons/css/fonts/js`, etc. in Stirling-PDF.
- [Discord Community](https://discord.gg/HYmhKj45pU)
- [Developer Guide](DeveloperGuide.md) ### Environment-Only Parameters
- `SYSTEM_ROOTURIPATH` - Set the application's root URI (e.g. `/pdf-app` to set the root URI to `localhost:8080/pdf-app`)
- `SYSTEM_CONNECTIONTIMEOUTMINUTES` - Set custom connection timeout values
- `DOCKER_ENABLE_SECURITY` - Set to `true` to download security jar (required for authentication login)
- `INSTALL_BOOK_AND_ADVANCED_HTML_OPS` - Download Calibre onto Stirling-PDF to enable PDF to/from book and advanced HTML conversion
- `LANGS` - Define custom font libraries to install for document conversions
## API
For those wanting to use Stirling-PDF's backend API to link with their own custom scripting to edit PDFs, you can view all existing API documentation [here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/), or navigate to `/swagger-ui/index.html` of your Stirling-PDF instance for your version's documentation (or by following the API button in the settings of Stirling-PDF).
## Login Authentication
![stirling-login](images/login-light.png)
### Prerequisites
- User must have the folder `./configs` volumed within Docker so that it is retained during updates.
- Docker users must download the security jar version by setting `DOCKER_ENABLE_SECURITY` to `true` in environment variables.
- Then either enable login via the `settings.yml` file or set `SECURITY_ENABLE_LOGIN` to `true`.
- Now the initial user will be generated with username `admin` and password `stirling`. On login, you will be forced to change the password to a new one. You can also use the environment variables `SECURITY_INITIALLOGIN_USERNAME` and `SECURITY_INITIALLOGIN_PASSWORD` to set your own credentials straight away (recommended to remove them after user creation).
Once the above has been done, on restart, a new `stirling-pdf-DB.mv.db` will show if everything worked.
When you log in to Stirling-PDF, you will be redirected to the `/login` page to log in with those default credentials. After login, everything should function as normal.
To access your account settings, go to Account Settings in the settings cog menu (top right in the navbar). This Account Settings menu is also where you find your API key.
To add new users, go to the bottom of Account Settings and hit 'Admin Settings'. Here you can add new users. The different roles mentioned within this are for rate limiting. This is a work in progress and will be expanded on more in the future.
For API usage, you must provide a header with `X-API-Key` and the associated API key for that user.
## FAQ
### Q1: What are your planned features?
- Progress bar/tracking
- Full custom logic pipelines to combine multiple operations together
- Folder support with auto-scanning to perform operations on
- Redact text (via UI, not just automated)
- Add forms
- Multi-page layout (stitch PDF pages together) support x rows y columns and custom page sizing
- Fill forms manually or automatically
### Q2: Why is my application downloading .htm files? Why am i getting HTTP error 413?
This is an issue commonly caused by your NGINX configuration. The default file upload size for NGINX is 1MB. You need to add the following in your Nginx sites-available file: `client_max_body_size SIZE;` (where "SIZE" is 50M for example for 50MB files).
### Q3: Why is my download timing out?
NGINX has timeout values by default, so if you are running Stirling-PDF behind NGINX, you may need to set a timeout value, such as adding the config `proxy_read_timeout 3600;`.

View File

@@ -1,63 +0,0 @@
# Security Policy
## Reporting a Vulnerability
The Stirling-PDF team takes security vulnerabilities seriously. We appreciate your efforts to responsibly disclose your findings.
### How to Report
You can report security vulnerabilities through two channels:
1. **GitHub Security Advisory**:
- Navigate to the [Security tab](https://github.com/Stirling-Tools/Stirling-PDF/security) in our repository
- Click on "Report a vulnerability"
- Provide a detailed description of the vulnerability
2. **Direct Email**:
- Send your report to security@stirlingpdf.com
- Please include as much information as possible about the vulnerability
### What to Include
When reporting a vulnerability, please provide:
- A clear description of the vulnerability
- Steps to reproduce the issue
- Any potential impact
- If possible, suggestions for addressing the vulnerability
- Your contact information for follow-up questions
### Response Time
We aim to acknowledge receipt of your vulnerability report within 48 hours
### Process
1. Submit your report through one of the channels above
2. Receive an acknowledgment from our team
3. Our team will investigate and validate the issue
4. We will work on a fix and keep you updated on our progress
5. Once resolved, we will publish the fix and acknowledge your contribution (if desired)
### Bug Bounty
At this time, we do not offer a bug bounty program. However, we greatly appreciate your efforts in making Stirling-PDF more secure and will acknowledge your contribution in our release notes (unless you prefer to remain anonymous).
## Supported Versions
Only the latest version of Stirling-PDF is supported for security updates. We do not backport security fixes to older versions.
| Version | Supported |
| ------- | ------------------ |
| Latest | :white_check_mark: |
| Older | :x: |
**Please note:** Before reporting a security issue, ensure you are using the latest version of Stirling-PDF. Security reports for older versions will not be accepted.
## Security Best Practices
When deploying Stirling-PDF:
1. Always use the latest version
2. Follow our deployment guidelines
3. Regularly check for and apply updates

68
Version-groups.md Normal file
View File

@@ -0,0 +1,68 @@
|All versions in a Docker environment can download Calibre as a optional extra at runtime to support `book-to-pdf` and `pdf-to-book` using parameter ``INSTALL_BOOK_AND_ADVANCED_HTML_OPS``.
The 'Fat' container contains all those found in 'Full' with security jar along with this Calibre install.
| Technology | Ultra-Lite | Full |
| ---------- | :--------: | :---: |
| Java | ✔️ | ✔️ |
| JavaScript | ✔️ | ✔️ |
| Libre | | ✔️ |
| Python | | ✔️ |
| OpenCV | | ✔️ |
| qpdf | | ✔️ |
| Operation | Ultra-Lite | Full |
| ---------------------- | ---------- | ---- |
| add-page-numbers | ✔️ | ✔️ |
| add-password | ✔️ | ✔️ |
| add-image | ✔️ | ✔️ |
| add-watermark | ✔️ | ✔️ |
| adjust-contrast | ✔️ | ✔️ |
| auto-split-pdf | ✔️ | ✔️ |
| auto-redact | ✔️ | ✔️ |
| auto-rename | ✔️ | ✔️ |
| cert-sign | ✔️ | ✔️ |
| remove-cert-sign | ✔️ | ✔️ |
| crop | ✔️ | ✔️ |
| change-metadata | ✔️ | ✔️ |
| change-permissions | ✔️ | ✔️ |
| compare | ✔️ | ✔️ |
| extract-page | ✔️ | ✔️ |
| extract-images | ✔️ | ✔️ |
| flatten | ✔️ | ✔️ |
| get-info-on-pdf | ✔️ | ✔️ |
| img-to-pdf | ✔️ | ✔️ |
| markdown-to-pdf | ✔️ | ✔️ |
| merge-pdfs | ✔️ | ✔️ |
| multi-page-layout | ✔️ | ✔️ |
| overlay-pdf | ✔️ | ✔️ |
| pdf-organizer | ✔️ | ✔️ |
| pdf-to-csv | ✔️ | ✔️ |
| pdf-to-img | ✔️ | ✔️ |
| pdf-to-single-page | ✔️ | ✔️ |
| remove-pages | ✔️ | ✔️ |
| remove-password | ✔️ | ✔️ |
| rotate-pdf | ✔️ | ✔️ |
| sanitize-pdf | ✔️ | ✔️ |
| scale-pages | ✔️ | ✔️ |
| sign | ✔️ | ✔️ |
| show-javascript | ✔️ | ✔️ |
| split-by-size-or-count | ✔️ | ✔️ |
| split-pdf-by-sections | ✔️ | ✔️ |
| split-pdfs | ✔️ | ✔️ |
| compress-pdf | | ✔️ |
| extract-image-scans | | ✔️ |
| ocr-pdf | | ✔️ |
| pdf-to-pdfa | | ✔️ |
| remove-blanks | | ✔️ |
pdf-to-text | ✔️ | ✔️
pdf-to-html | | ✔️
pdf-to-word | | ✔️
pdf-to-presentation | | ✔️
pdf-to-xml | | ✔️
remove-annotations | ✔️ | ✔️
remove-cert-sign | ✔️ | ✔️
remove-image-pdf | ✔️ | ✔️
file-to-pdf | | ✔️
html-to-pdf | | ✔️
url-to-pdf | | ✔️
repair | | ✔️

View File

@@ -1,14 +1,13 @@
plugins { plugins {
id "java" id "java"
id "org.springframework.boot" version "3.4.1" id "org.springframework.boot" version "3.4.0"
id "io.spring.dependency-management" version "1.1.7" id "io.spring.dependency-management" version "1.1.6"
id "org.springdoc.openapi-gradle-plugin" version "1.8.0" id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
id "io.swagger.swaggerhub" version "1.3.2" id "io.swagger.swaggerhub" version "1.3.2"
id "edu.sc.seis.launch4j" version "3.0.6" id "edu.sc.seis.launch4j" version "3.0.6"
id "com.diffplug.spotless" version "6.25.0" id "com.diffplug.spotless" version "6.25.0"
id "com.github.jk1.dependency-license-report" version "2.9" id "com.github.jk1.dependency-license-report" version "2.9"
//id "nebula.lint" version "19.0.3" //id "nebula.lint" version "19.0.3"
id("org.panteleyev.jpackageplugin") version "1.6.0"
} }
@@ -16,18 +15,18 @@ plugins {
import com.github.jk1.license.render.* import com.github.jk1.license.render.*
ext { ext {
springBootVersion = "3.4.1" springBootVersion = "3.4.0"
pdfboxVersion = "3.0.3" pdfboxVersion = "3.0.3"
logbackVersion = "1.5.7" logbackVersion = "1.5.7"
imageioVersion = "3.12.0" imageioVersion = "3.12.0"
lombokVersion = "1.18.36" lombokVersion = "1.18.36"
bouncycastleVersion = "1.79" bouncycastleVersion = "1.79"
springSecuritySamlVersion = "6.4.2" springSecuritySamlVersion = "6.4.1"
openSamlVersion = "4.3.2" openSamlVersion = "4.3.2"
} }
group = "stirling.software" group = "stirling.software"
version = "0.36.6" version = "0.35.0"
java { java {
@@ -38,9 +37,10 @@ java {
repositories { repositories {
mavenCentral() mavenCentral()
maven { url "https://jitpack.io" } maven { url "https://jitpack.io" }
maven { url "https://build.shibboleth.net/maven/releases" } maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
maven { url "https://maven.pkg.github.com/jcefmaven/jcefmaven" } maven {
url 'https://build.shibboleth.net/maven/releases'
}
} }
licenseReport { licenseReport {
@@ -52,48 +52,18 @@ sourceSets {
java { java {
if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") { if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") {
exclude "stirling/software/SPDF/config/security/**" exclude "stirling/software/SPDF/config/security/**"
exclude "stirling/software/SPDF/controller/api/DatabaseController.java"
exclude "stirling/software/SPDF/controller/api/UserController.java" exclude "stirling/software/SPDF/controller/api/UserController.java"
exclude "stirling/software/SPDF/controller/api/H2SQLCondition.java" exclude "stirling/software/SPDF/controller/api/DatabaseController.java"
exclude "stirling/software/SPDF/controller/web/AccountWebController.java" exclude "stirling/software/SPDF/controller/web/AccountWebController.java"
exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java" exclude "stirling/software/SPDF/controller/web/DatabaseWebController.java"
exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java" exclude "stirling/software/SPDF/model/ApiKeyAuthenticationToken.java"
exclude "stirling/software/SPDF/model/AttemptCounter.java" exclude "stirling/software/SPDF/model/AttemptCounter.java"
exclude "stirling/software/SPDF/model/Authority.java" exclude "stirling/software/SPDF/model/Authority.java"
exclude "stirling/software/SPDF/model/BackupNotFoundException.java"
exclude "stirling/software/SPDF/model/PersistentLogin.java" exclude "stirling/software/SPDF/model/PersistentLogin.java"
exclude "stirling/software/SPDF/model/SessionEntity.java" exclude "stirling/software/SPDF/model/SessionEntity.java"
exclude "stirling/software/SPDF/model/User.java" exclude "stirling/software/SPDF/model/User.java"
exclude "stirling/software/SPDF/repository/**" exclude "stirling/software/SPDF/repository/**"
} }
if (System.getenv("STIRLING_PDF_DESKTOP_UI") == "false") {
exclude "stirling/software/SPDF/UI/impl/**"
}
}
}
test {
java {
if (System.getenv("DOCKER_ENABLE_SECURITY") == "false") {
exclude "stirling/software/SPDF/config/security/**"
exclude "stirling/software/SPDF/controller/api/UserControllerTest.java"
exclude "stirling/software/SPDF/controller/api/DatabaseControllerTest.java"
exclude "stirling/software/SPDF/controller/web/AccountWebControllerTest.java"
exclude "stirling/software/SPDF/controller/web/DatabaseWebControllerTest.java"
exclude "stirling/software/SPDF/model/ApiKeyAuthenticationTokenTest.java"
exclude "stirling/software/SPDF/model/AttemptCounterTest.java"
exclude "stirling/software/SPDF/model/AuthorityTest.java"
exclude "stirling/software/SPDF/model/PersistentLoginTest.java"
exclude "stirling/software/SPDF/model/SessionEntityTest.java"
exclude "stirling/software/SPDF/model/UserTest.java"
exclude "stirling/software/SPDF/repository/**"
}
if (System.getenv("STIRLING_PDF_DESKTOP_UI") == "false") {
exclude "stirling/software/SPDF/UI/impl/**"
}
} }
} }
} }
@@ -104,155 +74,16 @@ openApi {
outputFileName = "SwaggerDoc.json" outputFileName = "SwaggerDoc.json"
} }
//0.11.5 to 2024.11.5
def getMacVersion(String version) {
def currentYear = java.time.Year.now().getValue()
def versionParts = version.split("\\.", 2)
return "${currentYear}.${versionParts.length > 1 ? versionParts[1] : versionParts[0]}"
}
jpackage {
input = "build/libs"
appName = "Stirling-PDF"
appVersion = project.version
vendor = "Stirling-Software"
appDescription = "Stirling PDF - Your Local PDF Editor"
mainJar = "Stirling-PDF-${project.version}.jar"
mainClass = "org.springframework.boot.loader.launch.JarLauncher"
icon = "src/main/resources/static/favicon.ico"
// JVM Options
javaOptions = [
"-DBROWSER_OPEN=true",
"-DSTIRLING_PDF_DESKTOP_UI=true",
"-Djava.awt.headless=false",
"-Dapple.awt.UIElement=true",
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
"--add-opens", "java.desktop/java.awt.event=ALL-UNNAMED",
"--add-opens", "java.desktop/sun.awt=ALL-UNNAMED"
]
verbose = true
destination = "${projectDir}/build/jpackage"
// Windows-specific configuration
windows {
launcherAsService = false
appVersion = project.version
winConsole = false
winMenu = true // Creates start menu entry
winShortcut = true // Creates desktop shortcut
winShortcutPrompt = true // Lets user choose whether to create shortcuts
winDirChooser = true // Allows users to choose installation directory
winPerUserInstall = false
winMenuGroup = "Stirling Software"
winUpgradeUuid = "2a43ed0c-b8c2-40cf-89e1-751129b87641" // Unique identifier for updates
winHelpUrl = "https://github.com/Stirling-Tools/Stirling-PDF"
winUpdateUrl = "https://github.com/Stirling-Tools/Stirling-PDF/releases"
type = "exe"
installDir = "C:/Program Files/Stirling-PDF"
}
// macOS-specific configuration
mac {
appVersion = getMacVersion(project.version.toString())
icon = "src/main/resources/static/favicon.icns"
type = "dmg"
macPackageIdentifier = "com.stirling.software.pdf"
macPackageName = "Stirling-PDF"
macAppCategory = "public.app-category.productivity"
macSign = false // Enable signing
macAppStore = false // Not targeting App Store initially
//installDir = "Applications"
// Add license and other documentation to DMG
/*macDmgContent = [
"README.md",
"LICENSE",
"CHANGELOG.md"
]*/
// Enable Mac-specific entitlements
//macEntitlements = "entitlements.plist" // You'll need to create this file
}
// Linux-specific configuration
linux {
appVersion = project.version
icon = "src/main/resources/static/favicon.png"
type = "deb" // Can also use "rpm" for Red Hat-based systems
// Debian package configuration
//linuxPackageName = "stirlingpdf"
linuxDebMaintainer = "support@stirlingpdf.com"
linuxMenuGroup = "Office;PDF;Productivity"
linuxAppCategory = "Office"
linuxAppRelease = "1"
linuxPackageDeps = true
installDir = "/opt/Stirling-PDF"
// RPM-specific settings
//linuxRpmLicenseType = "MIT"
}
// Common additional options
//jLinkOptions = [
// "--strip-debug",
// "--compress=2",
// "--no-header-files",
// "--no-man-pages"
//]
// Add any additional modules required
/*addModules = [
"java.base",
"java.desktop",
"java.logging",
"java.sql",
"java.xml",
"jdk.crypto.ec"
]*/
// Add copyright and license information
copyright = "Copyright © 2024 Stirling Software"
licenseFile = "LICENSE"
}
launch4j { launch4j {
icon = "${projectDir}/src/main/resources/static/favicon.ico" icon = "${projectDir}/src/main/resources/static/favicon.ico"
outfile="Stirling-PDF.exe" outfile="Stirling-PDF.exe"
headerType="console"
if(System.getenv("STIRLING_PDF_DESKTOP_UI") == 'true') {
headerType = "gui"
} else {
headerType = "console"
}
jarTask = tasks.bootJar jarTask = tasks.bootJar
errTitle="Encountered error, Do you have Java 21?" errTitle="Encountered error, Do you have Java 21?"
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe" downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe"
variables=["BROWSER_OPEN=true"]
if(System.getenv("STIRLING_PDF_DESKTOP_UI") == 'true') {
variables=["BROWSER_OPEN=true", "STIRLING_PDF_DESKTOP_UI=true"]
} else {
variables=["BROWSER_OPEN=true"]
}
jreMinVersion="17" jreMinVersion="17"
mutexName="Stirling-PDF" mutexName="Stirling-PDF"
@@ -269,7 +100,7 @@ spotless {
java { java {
target project.fileTree('src/main/java') target project.fileTree('src/main/java')
googleJavaFormat("1.25.2").aosp().reorderImports(false) googleJavaFormat("1.22.0").aosp().reorderImports(false)
importOrder("java", "javax", "org", "com", "net", "io") importOrder("java", "javax", "org", "com", "net", "io")
toggleOffOn() toggleOffOn()
@@ -283,7 +114,7 @@ spotless {
// rules=['unused-dependency'] // rules=['unused-dependency']
// } // }
tasks.wrapper { tasks.wrapper {
gradleVersion = "8.12" gradleVersion = "8.7"
} }
//tasks.withType(JavaCompile) { //tasks.withType(JavaCompile) {
// options.compilerArgs << "-Xlint:deprecation" // options.compilerArgs << "-Xlint:deprecation"
@@ -292,17 +123,10 @@ configurations.all {
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat" exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
} }
dependencies { dependencies {
if (System.getenv("STIRLING_PDF_DESKTOP_UI") != "false") {
implementation "me.friwi:jcefmaven:127.3.1"
implementation "org.openjfx:javafx-controls:21"
implementation "org.openjfx:javafx-swing:21"
}
//security updates //security updates
implementation "org.springframework:spring-webmvc:6.2.1" implementation "org.springframework:spring-webmvc:6.2.0"
implementation("io.github.pixee:java-security-toolkit:1.2.1") implementation("io.github.pixee:java-security-toolkit:1.2.0")
// implementation "org.yaml:snakeyaml:2.2" // implementation "org.yaml:snakeyaml:2.2"
implementation 'com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4' implementation 'com.github.Carleslc.Simple-YAML:Simple-Yaml:1.8.4'
@@ -318,17 +142,15 @@ dependencies {
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") { if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE" implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE"
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
implementation "org.springframework.session:spring-session-core:$springBootVersion" implementation "org.springframework.session:spring-session-core:$springBootVersion"
implementation "org.springframework:spring-jdbc:6.2.1"
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5' implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
// Don't upgrade h2database // Don't upgrade h2database
runtimeOnly "com.h2database:h2:2.3.232" runtimeOnly "com.h2database:h2:2.3.232"
runtimeOnly "org.postgresql:postgresql:42.7.4"
constraints { constraints {
implementation "org.opensaml:opensaml-core:$openSamlVersion" implementation "org.opensaml:opensaml-core:$openSamlVersion"
implementation "org.opensaml:opensaml-saml-api:$openSamlVersion" implementation "org.opensaml:opensaml-saml-api:$openSamlVersion"
@@ -396,7 +218,7 @@ dependencies {
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
implementation "io.micrometer:micrometer-core:1.14.2" implementation "io.micrometer:micrometer-core:1.14.1"
implementation group: "com.google.zxing", name: "core", version: "3.5.3" implementation group: "com.google.zxing", name: "core", version: "3.5.3"
// https://mvnrepository.com/artifact/org.commonmark/commonmark // https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation "org.commonmark:commonmark:0.24.0" implementation "org.commonmark:commonmark:0.24.0"
@@ -449,14 +271,7 @@ jar {
tasks.named("test") { tasks.named("test") {
useJUnitPlatform() useJUnitPlatform()
} }
task printVersion {
doLast {
println project.version
}
}
task printMacVersion { task printVersion {
doLast { println project.version
println getMacVersion(project.version.toString())
}
} }

View File

@@ -1,7 +1,7 @@
@general @general
Feature: API Validation Feature: API Validation
@split-pdf-by-sections @positive @split-pdf-by-sections @positive
Scenario Outline: split-pdf-by-sections with different parameters Scenario Outline: split-pdf-by-sections with different parameters
Given I generate a PDF file as "fileInput" Given I generate a PDF file as "fileInput"
@@ -66,7 +66,7 @@ Feature: API Validation
| pageNumbers | file_count | | pageNumbers | file_count |
| 1,3,5-9 | 8 | | 1,3,5-9 | 8 |
| all | 20 | | all | 20 |
| 2n+1 | 10 | | 2n+1 | 11 |
| 3n | 7 | | 3n | 7 |
@@ -106,9 +106,9 @@ Feature: API Validation
And the response ZIP should contain 2 files And the response ZIP should contain 2 files
And the response file should have size greater than 0 And the response file should have size greater than 0
And the response status code should be 200 And the response status code should be 200
Examples: Examples:
| format | | format |
| png | | png |
| gif | | gif |
| jpeg | | jpeg |

View File

@@ -15,10 +15,6 @@ import shutil
import re import re
from PIL import Image, ImageDraw from PIL import Image, ImageDraw
API_HEADERS = {
'X-API-KEY': '123456789'
}
######### #########
# GIVEN # # GIVEN #
######### #########
@@ -231,7 +227,7 @@ def save_generated_pdf(context, filename):
def step_send_get_request(context, endpoint): def step_send_get_request(context, endpoint):
base_url = "http://localhost:8080" base_url = "http://localhost:8080"
full_url = f"{base_url}{endpoint}" full_url = f"{base_url}{endpoint}"
response = requests.get(full_url, headers=API_HEADERS) response = requests.get(full_url)
context.response = response context.response = response
@when('I send a GET request to "{endpoint}" with parameters') @when('I send a GET request to "{endpoint}" with parameters')
@@ -239,7 +235,7 @@ def step_send_get_request_with_params(context, endpoint):
base_url = "http://localhost:8080" base_url = "http://localhost:8080"
params = {row['parameter']: row['value'] for row in context.table} params = {row['parameter']: row['value'] for row in context.table}
full_url = f"{base_url}{endpoint}" full_url = f"{base_url}{endpoint}"
response = requests.get(full_url, params=params, headers=API_HEADERS) response = requests.get(full_url, params=params)
context.response = response context.response = response
@when('I send the API request to the endpoint "{endpoint}"') @when('I send the API request to the endpoint "{endpoint}"')
@@ -260,7 +256,7 @@ def step_send_api_request(context, endpoint):
print(f"form_data {file.name} with {mime_type}") print(f"form_data {file.name} with {mime_type}")
form_data.append((key, (file.name, file, mime_type))) form_data.append((key, (file.name, file, mime_type)))
response = requests.post(url, files=form_data, headers=API_HEADERS) response = requests.post(url, files=form_data)
context.response = response context.response = response
######## ########

View File

@@ -1,5 +0,0 @@
behave
requests
PyPDF2
reportlab
PyCryptodome

View File

@@ -1,255 +1,5 @@
# behave
# This file is autogenerated by pip-compile with Python 3.10 requests
# by the following command: PyPDF2
# reportlab
# pip-compile --generate-hashes --output-file='cucumber\requirements.txt' 'cucumber\requirements.in' PyCryptodome
#
behave==1.2.6 \
--hash=sha256:b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86 \
--hash=sha256:ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c
# via -r cucumber\requirements.in
certifi==2024.12.14 \
--hash=sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56 \
--hash=sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db
# via requests
chardet==5.2.0 \
--hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \
--hash=sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970
# via reportlab
charset-normalizer==3.4.1 \
--hash=sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537 \
--hash=sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa \
--hash=sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a \
--hash=sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294 \
--hash=sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b \
--hash=sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd \
--hash=sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601 \
--hash=sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd \
--hash=sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4 \
--hash=sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d \
--hash=sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2 \
--hash=sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313 \
--hash=sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd \
--hash=sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa \
--hash=sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8 \
--hash=sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1 \
--hash=sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2 \
--hash=sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496 \
--hash=sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d \
--hash=sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b \
--hash=sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e \
--hash=sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a \
--hash=sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4 \
--hash=sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca \
--hash=sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78 \
--hash=sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408 \
--hash=sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5 \
--hash=sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3 \
--hash=sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f \
--hash=sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a \
--hash=sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765 \
--hash=sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6 \
--hash=sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146 \
--hash=sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6 \
--hash=sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9 \
--hash=sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd \
--hash=sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c \
--hash=sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f \
--hash=sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545 \
--hash=sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176 \
--hash=sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770 \
--hash=sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824 \
--hash=sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f \
--hash=sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf \
--hash=sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487 \
--hash=sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d \
--hash=sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd \
--hash=sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b \
--hash=sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534 \
--hash=sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f \
--hash=sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b \
--hash=sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9 \
--hash=sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd \
--hash=sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125 \
--hash=sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9 \
--hash=sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de \
--hash=sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11 \
--hash=sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d \
--hash=sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35 \
--hash=sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f \
--hash=sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda \
--hash=sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7 \
--hash=sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a \
--hash=sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971 \
--hash=sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8 \
--hash=sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41 \
--hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d \
--hash=sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f \
--hash=sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757 \
--hash=sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a \
--hash=sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886 \
--hash=sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77 \
--hash=sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76 \
--hash=sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247 \
--hash=sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 \
--hash=sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb \
--hash=sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7 \
--hash=sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e \
--hash=sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6 \
--hash=sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037 \
--hash=sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1 \
--hash=sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e \
--hash=sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807 \
--hash=sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407 \
--hash=sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c \
--hash=sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12 \
--hash=sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3 \
--hash=sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089 \
--hash=sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd \
--hash=sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e \
--hash=sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00 \
--hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616
# via requests
idna==3.10 \
--hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
--hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
# via requests
parse==1.20.2 \
--hash=sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558 \
--hash=sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce
# via
# behave
# parse-type
parse-type==0.6.4 \
--hash=sha256:5e1ec10440b000c3f818006033372939e693a9ec0176f446d9303e4db88489a6 \
--hash=sha256:83d41144a82d6b8541127bf212dd76c7f01baff680b498ce8a4d052a7a5bce4c
# via behave
pillow==11.1.0 \
--hash=sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83 \
--hash=sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96 \
--hash=sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65 \
--hash=sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a \
--hash=sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352 \
--hash=sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f \
--hash=sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20 \
--hash=sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c \
--hash=sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114 \
--hash=sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49 \
--hash=sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91 \
--hash=sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0 \
--hash=sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2 \
--hash=sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5 \
--hash=sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884 \
--hash=sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e \
--hash=sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c \
--hash=sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196 \
--hash=sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756 \
--hash=sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861 \
--hash=sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269 \
--hash=sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1 \
--hash=sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb \
--hash=sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a \
--hash=sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081 \
--hash=sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1 \
--hash=sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8 \
--hash=sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90 \
--hash=sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc \
--hash=sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5 \
--hash=sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1 \
--hash=sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3 \
--hash=sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35 \
--hash=sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f \
--hash=sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c \
--hash=sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2 \
--hash=sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2 \
--hash=sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf \
--hash=sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65 \
--hash=sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b \
--hash=sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442 \
--hash=sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2 \
--hash=sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade \
--hash=sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482 \
--hash=sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe \
--hash=sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc \
--hash=sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a \
--hash=sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec \
--hash=sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3 \
--hash=sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a \
--hash=sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07 \
--hash=sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6 \
--hash=sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f \
--hash=sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e \
--hash=sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192 \
--hash=sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0 \
--hash=sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6 \
--hash=sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73 \
--hash=sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f \
--hash=sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6 \
--hash=sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547 \
--hash=sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9 \
--hash=sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457 \
--hash=sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8 \
--hash=sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26 \
--hash=sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5 \
--hash=sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab \
--hash=sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070 \
--hash=sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71 \
--hash=sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9 \
--hash=sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761
# via reportlab
pycryptodome==3.21.0 \
--hash=sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8 \
--hash=sha256:0fa0a05a6a697ccbf2a12cec3d6d2650b50881899b845fac6e87416f8cb7e87d \
--hash=sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0 \
--hash=sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93 \
--hash=sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4 \
--hash=sha256:26412b21df30b2861424a6c6d5b1d8ca8107612a4cfa4d0183e71c5d200fb34a \
--hash=sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764 \
--hash=sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca \
--hash=sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e \
--hash=sha256:3ba4cc304eac4d4d458f508d4955a88ba25026890e8abff9b60404f76a62c55e \
--hash=sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd \
--hash=sha256:590ef0898a4b0a15485b05210b4a1c9de8806d3ad3d47f74ab1dc07c67a6827f \
--hash=sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6 \
--hash=sha256:6cce52e196a5f1d6797ff7946cdff2038d3b5f0aba4a43cb6bf46b575fd1b5bb \
--hash=sha256:7cb087b8612c8a1a14cf37dd754685be9a8d9869bed2ffaaceb04850a8aeef7e \
--hash=sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1 \
--hash=sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6 \
--hash=sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a \
--hash=sha256:8acd7d34af70ee63f9a849f957558e49a98f8f1634f86a59d2be62bb8e93f71c \
--hash=sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2 \
--hash=sha256:a1752eca64c60852f38bb29e2c86fca30d7672c024128ef5d70cc15868fa10f4 \
--hash=sha256:a3804675283f4764a02db05f5191eb8fec2bb6ca34d466167fc78a5f05bbe6b3 \
--hash=sha256:a4e74c522d630766b03a836c15bff77cb657c5fdf098abf8b1ada2aebc7d0819 \
--hash=sha256:a915597ffccabe902e7090e199a7bf7a381c5506a747d5e9d27ba55197a2c568 \
--hash=sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53 \
--hash=sha256:cc2269ab4bce40b027b49663d61d816903a4bd90ad88cb99ed561aadb3888dd3 \
--hash=sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8 \
--hash=sha256:dad9bf36eda068e89059d1f07408e397856be9511d7113ea4b586642a429a4fd \
--hash=sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b \
--hash=sha256:f35e442630bc4bc2e1878482d6f59ea22e280d7121d7adeaedba58c23ab6386b \
--hash=sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297 \
--hash=sha256:ff99f952db3db2fbe98a0b355175f93ec334ba3d01bbde25ad3a5a33abc02b58
# via -r cucumber\requirements.in
pypdf2==3.0.1 \
--hash=sha256:a74408f69ba6271f71b9352ef4ed03dc53a31aa404d29b5d31f53bfecfee1440 \
--hash=sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928
# via -r cucumber\requirements.in
reportlab==4.2.5 \
--hash=sha256:5cf35b8fd609b68080ac7bbb0ae1e376104f7d5f7b2d3914c7adc63f2593941f \
--hash=sha256:eb2745525a982d9880babb991619e97ac3f661fae30571b7d50387026ca765ee
# via -r cucumber\requirements.in
requests==2.32.3 \
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
# via -r cucumber\requirements.in
six==1.17.0 \
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
# via
# behave
# parse-type
urllib3==2.3.0 \
--hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \
--hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d
# via requests

View File

@@ -1,63 +0,0 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Security-Fat-Postgres
image: stirlingtools/stirling-pdf:latest-fat-postgres
deploy:
resources:
limits:
memory: 4G
depends_on:
- db
healthcheck:
test: [ "CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'" ]
interval: 5s
timeout: 10s
retries: 16
ports:
- 8080:8080
volumes:
- ./stirling/latest/data:/usr/share/tessdata:rw
- ./stirling/latest/config:/configs:rw
- ./stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "false"
PUID: 1002
PGID: 1002
UMASK: "022"
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest-fat with Security and PostgreSQL
UI_APPNAMENAVBAR: Stirling-PDF Latest-fat-PostgreSQL
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
SYSTEM_DATASOURCE_ENABLECUSTOMDATABASE: "true"
SYSTEM_DATASOURCE_CUSTOMDATABASEURL: "jdbc:postgresql://db:5432/stirling_pdf"
SYSTEM_DATASOURCE_USERNAME: "admin"
SYSTEM_DATASOURCE_PASSWORD: "stirling"
restart: on-failure:5
db:
image: 'postgres:17.2-alpine'
restart: on-failure:5
container_name: db
ports:
- "5432:5432"
environment:
POSTGRES_DB: "stirling_pdf"
POSTGRES_USER: "admin"
POSTGRES_PASSWORD: "stirling"
shm_size: "512mb"
deploy:
resources:
limits:
memory: 512m
cpus: "0.5"
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U admin stirling_pdf" ]
interval: 1s
timeout: 5s
retries: 10
volumes:
- ./stirling/latest/data:/pgdata

View File

@@ -14,9 +14,9 @@ services:
ports: ports:
- 8080:8080 - 8080:8080
volumes: volumes:
- ./stirling/latest/data:/usr/share/tessdata:rw - /stirling/latest/data:/usr/share/tessdata:rw
- ./stirling/latest/config:/configs:rw - /stirling/latest/config:/configs:rw
- ./stirling/latest/logs:/logs:rw - /stirling/latest/logs:/logs:rw
environment: environment:
DOCKER_ENABLE_SECURITY: "true" DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "false" SECURITY_ENABLELOGIN: "false"

View File

@@ -14,9 +14,9 @@ services:
ports: ports:
- "8080:8080" - "8080:8080"
volumes: volumes:
- ./stirling/latest/data:/usr/share/tessdata:rw - /stirling/latest/data:/usr/share/tessdata:rw
- ./stirling/latest/config:/configs:rw - /stirling/latest/config:/configs:rw
- ./stirling/latest/logs:/logs:rw - /stirling/latest/logs:/logs:rw
environment: environment:
DOCKER_ENABLE_SECURITY: "true" DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true" SECURITY_ENABLELOGIN: "true"

View File

@@ -1,34 +0,0 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Security-Fat
image: stirlingtools/stirling-pdf:latest-fat
deploy:
resources:
limits:
memory: 4G
healthcheck:
test: ["CMD-SHELL", "curl -f -H 'X-API-KEY: 123456789' http://localhost:8080/api/v1/info/status | grep -q 'UP'"]
interval: 5s
timeout: 10s
retries: 16
ports:
- 8080:8080
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true"
PUID: 1002
PGID: 1002
UMASK: "022"
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest-fat with Security
UI_APPNAMENAVBAR: Stirling-PDF Latest-fat
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
SECURITY_CUSTOMGLOBALAPIKEY: "123456789"
restart: on-failure:5

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -42,19 +42,14 @@ ignore = [
'addPageNumbers.selectText.3', 'addPageNumbers.selectText.3',
'alphabet', 'alphabet',
'certSign.name', 'certSign.name',
'fileChooser.dragAndDrop',
'home.pipeline.title', 'home.pipeline.title',
'language.direction', 'language.direction',
'legal.impressum',
'licenses.version', 'licenses.version',
'pipeline.title', 'pipeline.title',
'pipelineOptions.pipelineHeader', 'pipelineOptions.pipelineHeader',
'pro', 'pro',
'sponsor', 'sponsor',
'text', 'text',
'validateSignature.cert.bits',
'validateSignature.cert.version',
'validateSignature.status',
'watermark.type.1', 'watermark.type.1',
] ]
@@ -66,6 +61,7 @@ ignore = [
[es_ES] [es_ES]
ignore = [ ignore = [
'adminUserSettings.roles', 'adminUserSettings.roles',
'color',
'error', 'error',
'language.direction', 'language.direction',
'no', 'no',
@@ -77,11 +73,6 @@ ignore = [
'language.direction', 'language.direction',
] ]
[fa_IR]
ignore = [
'language.direction',
]
[fr_FR] [fr_FR]
ignore = [ ignore = [
'AddStampRequest.alphabet', 'AddStampRequest.alphabet',
@@ -247,11 +238,6 @@ ignore = [
'showJS.tags', 'showJS.tags',
] ]
[zh_BO]
ignore = [
'language.direction',
]
[zh_CN] [zh_CN]
ignore = [ ignore = [
'language.direction', 'language.direction',

View File

@@ -16,10 +16,10 @@ fi
# Check if TESSERACT_LANGS environment variable is set and is not empty # Check if TESSERACT_LANGS environment variable is set and is not empty
if [[ -n "$TESSERACT_LANGS" ]]; then if [[ -n "$TESSERACT_LANGS" ]]; then
# Convert comma-separated values to a space-separated list # Convert comma-separated values to a space-separated list
SPACE_SEPARATED_LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ') LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ')
pattern='^[a-zA-Z]{2,4}(_[a-zA-Z]{2,4})?$' pattern='^[a-zA-Z]{2,4}(_[a-zA-Z]{2,4})?$'
# Install each language pack # Install each language pack
for LANG in $SPACE_SEPARATED_LANGS; do for LANG in $LANGS; do
if [[ $LANG =~ $pattern ]]; then if [[ $LANG =~ $pattern ]]; then
apk add --no-cache "tesseract-ocr-data-$LANG" apk add --no-cache "tesseract-ocr-data-$LANG"
else else

View File

@@ -97,14 +97,14 @@ public abstract class CreateSignatureBase implements SignatureInterface {
this.privateKey = privateKey; this.privateKey = privateKey;
} }
public Certificate[] getCertificateChain() {
return certificateChain;
}
public final void setCertificateChain(final Certificate[] certificateChain) { public final void setCertificateChain(final Certificate[] certificateChain) {
this.certificateChain = certificateChain; this.certificateChain = certificateChain;
} }
public Certificate[] getCertificateChain() {
return certificateChain;
}
public void setTsaUrl(String tsaUrl) { public void setTsaUrl(String tsaUrl) {
this.tsaUrl = tsaUrl; this.tsaUrl = tsaUrl;
} }
@@ -152,10 +152,6 @@ public abstract class CreateSignatureBase implements SignatureInterface {
} }
} }
public boolean isExternalSigning() {
return externalSigning;
}
/** /**
* Set if external signing scenario should be used. If {@code false}, SignatureInterface would * Set if external signing scenario should be used. If {@code false}, SignatureInterface would
* be used for signing. * be used for signing.
@@ -167,4 +163,8 @@ public abstract class CreateSignatureBase implements SignatureInterface {
public void setExternalSigning(boolean externalSigning) { public void setExternalSigning(boolean externalSigning) {
this.externalSigning = externalSigning; this.externalSigning = externalSigning;
} }
public boolean isExternalSigning() {
return externalSigning;
}
} }

View File

@@ -51,13 +51,15 @@ public class TSAClient {
private static final DigestAlgorithmIdentifierFinder ALGORITHM_OID_FINDER = private static final DigestAlgorithmIdentifierFinder ALGORITHM_OID_FINDER =
new DefaultDigestAlgorithmIdentifierFinder(); new DefaultDigestAlgorithmIdentifierFinder();
// SecureRandom.getInstanceStrong() would be better, but sometimes blocks on Linux
private static final Random RANDOM = new SecureRandom();
private final URL url; private final URL url;
private final String username; private final String username;
private final String password; private final String password;
private final MessageDigest digest; private final MessageDigest digest;
// SecureRandom.getInstanceStrong() would be better, but sometimes blocks on Linux
private static final Random RANDOM = new SecureRandom();
/** /**
* @param url the URL of the TSA service * @param url the URL of the TSA service
* @param username user name of TSA * @param username user name of TSA

View File

@@ -1,5 +1,6 @@
package stirling.software.SPDF.EE; package stirling.software.SPDF.EE;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
@@ -13,23 +14,11 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Slf4j @Slf4j
public class EEAppConfig { public class EEAppConfig {
private final ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
@Autowired private LicenseKeyChecker licenseKeyChecker;
private final LicenseKeyChecker licenseKeyChecker;
public EEAppConfig(
ApplicationProperties applicationProperties, LicenseKeyChecker licenseKeyChecker) {
this.applicationProperties = applicationProperties;
this.licenseKeyChecker = licenseKeyChecker;
}
@Bean(name = "runningEE") @Bean(name = "runningEE")
public boolean runningEnterpriseEdition() { public boolean runningEnterpriseEdition() {
return licenseKeyChecker.getEnterpriseEnabledResult(); return licenseKeyChecker.getEnterpriseEnabledResult();
} }
@Bean(name = "SSOAutoLogin")
public boolean ssoAutoLogin() {
return applicationProperties.getEnterpriseEdition().isSsoAutoLogin();
}
} }

View File

@@ -94,7 +94,7 @@ public class KeygenLicenseVerifier {
.build(); .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
log.debug(" validateLicenseResponse body: " + response.body()); log.info(" validateLicenseResponse body: " + response.body());
JsonNode jsonResponse = objectMapper.readTree(response.body()); JsonNode jsonResponse = objectMapper.readTree(response.body());
if (response.statusCode() == 200) { if (response.statusCode() == 200) {

View File

@@ -18,7 +18,7 @@ public class LicenseKeyChecker {
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private boolean enterpriseEnabledResult = false; private boolean enterpriseEnbaledResult = false;
@Autowired @Autowired
public LicenseKeyChecker( public LicenseKeyChecker(
@@ -28,19 +28,19 @@ public class LicenseKeyChecker {
this.checkLicense(); this.checkLicense();
} }
@Scheduled(initialDelay = 604800000, fixedRate = 604800000) // 7 days in milliseconds @Scheduled(fixedRate = 604800000) // 7 days in milliseconds
public void checkLicensePeriodically() { public void checkLicensePeriodically() {
checkLicense(); checkLicense();
} }
private void checkLicense() { private void checkLicense() {
if (!applicationProperties.getEnterpriseEdition().isEnabled()) { if (!applicationProperties.getEnterpriseEdition().isEnabled()) {
enterpriseEnabledResult = false; enterpriseEnbaledResult = false;
} else { } else {
enterpriseEnabledResult = enterpriseEnbaledResult =
licenseService.verifyLicense( licenseService.verifyLicense(
applicationProperties.getEnterpriseEdition().getKey()); applicationProperties.getEnterpriseEdition().getKey());
if (enterpriseEnabledResult) { if (enterpriseEnbaledResult) {
log.info("License key is valid."); log.info("License key is valid.");
} else { } else {
log.info("License key is invalid."); log.info("License key is invalid.");
@@ -55,6 +55,6 @@ public class LicenseKeyChecker {
} }
public boolean getEnterpriseEnabledResult() { public boolean getEnterpriseEnabledResult() {
return enterpriseEnabledResult; return enterpriseEnbaledResult;
} }
} }

View File

@@ -6,6 +6,9 @@ import java.net.Socket;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.pixee.security.SystemCommand; import io.github.pixee.security.SystemCommand;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -13,20 +16,23 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class LibreOfficeListener { public class LibreOfficeListener {
private static final Logger logger = LoggerFactory.getLogger(LibreOfficeListener.class);
private static final long ACTIVITY_TIMEOUT = 20L * 60 * 1000; // 20 minutes private static final long ACTIVITY_TIMEOUT = 20L * 60 * 1000; // 20 minutes
private static final LibreOfficeListener INSTANCE = new LibreOfficeListener(); private static final LibreOfficeListener INSTANCE = new LibreOfficeListener();
private static final int LISTENER_PORT = 2002; private static final int LISTENER_PORT = 2002;
private ExecutorService executorService;
private long lastActivityTime;
private Process process;
private LibreOfficeListener() {}
public static LibreOfficeListener getInstance() { public static LibreOfficeListener getInstance() {
return INSTANCE; return INSTANCE;
} }
private ExecutorService executorService;
private long lastActivityTime;
private Process process;
private LibreOfficeListener() {}
private boolean isListenerRunning() { private boolean isListenerRunning() {
log.info("waiting for listener to start"); log.info("waiting for listener to start");
try (Socket socket = new Socket()) { try (Socket socket = new Socket()) {
@@ -81,7 +87,7 @@ public class LibreOfficeListener {
Thread.sleep(1000); Thread.sleep(1000);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
log.error("exception", e); logger.error("exception", e);
} // Check every 1 second } // Check every 1 second
} }
} }

View File

@@ -1,230 +0,0 @@
package stirling.software.SPDF;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.EnableScheduling;
import io.github.pixee.security.SystemCommand;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.UI.WebBrowser;
import stirling.software.SPDF.config.ConfigInitializer;
import stirling.software.SPDF.config.InstallationPathConfig;
import stirling.software.SPDF.model.ApplicationProperties;
@Slf4j
@EnableScheduling
@SpringBootApplication
public class SPDFApplication {
private static String serverPortStatic;
private static String baseUrlStatic;
private final Environment env;
private final ApplicationProperties applicationProperties;
private final WebBrowser webBrowser;
@Value("${baseUrl:http://localhost}")
private String baseUrl;
public SPDFApplication(
Environment env,
ApplicationProperties applicationProperties,
@Autowired(required = false) WebBrowser webBrowser) {
this.env = env;
this.applicationProperties = applicationProperties;
this.webBrowser = webBrowser;
}
public static void main(String[] args) throws IOException, InterruptedException {
SpringApplication app = new SpringApplication(SPDFApplication.class);
Properties props = new Properties();
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
System.setProperty("java.awt.headless", "false");
app.setHeadless(false);
props.put("java.awt.headless", "false");
props.put("spring.main.web-application-type", "servlet");
}
app.setAdditionalProfiles(getActiveProfile(args));
ConfigInitializer initializer = new ConfigInitializer();
try {
initializer.ensureConfigExists();
} catch (IOException | URISyntaxException e) {
log.error("Error initialising configuration", e);
}
Map<String, String> propertyFiles = new HashMap<>();
// External config files
log.info("Settings file: {}", InstallationPathConfig.getSettingsPath());
if (Files.exists(Paths.get(InstallationPathConfig.getSettingsPath()))) {
propertyFiles.put(
"spring.config.additional-location",
"file:" + InstallationPathConfig.getSettingsPath());
} else {
log.warn(
"External configuration file '{}' does not exist.",
InstallationPathConfig.getSettingsPath());
}
if (Files.exists(Paths.get(InstallationPathConfig.getCustomSettingsPath()))) {
String existingLocation =
propertyFiles.getOrDefault("spring.config.additional-location", "");
if (!existingLocation.isEmpty()) {
existingLocation += ",";
}
propertyFiles.put(
"spring.config.additional-location",
existingLocation + "file:" + InstallationPathConfig.getCustomSettingsPath());
} else {
log.warn(
"Custom configuration file '{}' does not exist.",
InstallationPathConfig.getCustomSettingsPath());
}
Properties finalProps = new Properties();
if (!propertyFiles.isEmpty()) {
finalProps.putAll(
Collections.singletonMap(
"spring.config.additional-location",
propertyFiles.get("spring.config.additional-location")));
}
if (!props.isEmpty()) {
finalProps.putAll(props);
}
app.setDefaultProperties(finalProps);
app.run(args);
// Ensure directories are created
try {
Files.createDirectories(Path.of(InstallationPathConfig.getTemplatesPath()));
Files.createDirectories(Path.of(InstallationPathConfig.getStaticPath()));
} catch (Exception e) {
log.error("Error creating directories: {}", e.getMessage());
}
printStartupLogs();
}
@PostConstruct
public void init() {
baseUrlStatic = this.baseUrl;
String url = baseUrl + ":" + getStaticPort();
if (webBrowser != null
&& Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
webBrowser.initWebUI(url);
} else {
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
if (browserOpen) {
try {
String os = System.getProperty("os.name").toLowerCase();
Runtime rt = Runtime.getRuntime();
if (os.contains("win")) {
// For Windows
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
} else if (os.contains("mac")) {
SystemCommand.runCommand(rt, "open " + url);
} else if (os.contains("nix") || os.contains("nux")) {
SystemCommand.runCommand(rt, "xdg-open " + url);
}
} catch (Exception e) {
log.error("Error opening browser: {}", e.getMessage());
}
}
}
log.info("Running configs {}", applicationProperties.toString());
}
@Value("${server.port:8080}")
public void setServerPortStatic(String port) {
if ("auto".equalsIgnoreCase(port)) {
// Use Spring Boot's automatic port assignment (server.port=0)
SPDFApplication.serverPortStatic =
"0"; // This will let Spring Boot assign an available port
} else {
SPDFApplication.serverPortStatic = port;
}
}
@PreDestroy
public void cleanup() {
if (webBrowser != null) {
webBrowser.cleanup();
}
}
private static void printStartupLogs() {
log.info("Stirling-PDF Started.");
String url = baseUrlStatic + ":" + getStaticPort();
log.info("Navigate to {}", url);
}
private static String[] getActiveProfile(String[] args) {
if (args == null) {
return new String[] {"default"};
}
for (String arg : args) {
if (arg.contains("spring.profiles.active")) {
return arg.substring(args[0].indexOf('=') + 1).split(", ");
}
}
return new String[] {"default"};
}
private static boolean isPortAvailable(int port) {
try (ServerSocket socket = new ServerSocket(port)) {
return true;
} catch (IOException e) {
return false;
}
}
// Optionally keep this method if you want to provide a manual port-incrementation fallback.
private static String findAvailablePort(int startPort) {
int port = startPort;
while (!isPortAvailable(port)) {
port++;
}
return String.valueOf(port);
}
public static String getStaticBaseUrl() {
return baseUrlStatic;
}
public String getNonStaticBaseUrl() {
return baseUrlStatic;
}
public static String getStaticPort() {
return serverPortStatic;
}
public String getNonStaticPort() {
return serverPortStatic;
}
}

View File

@@ -0,0 +1,165 @@
package stirling.software.SPDF;
import java.io.IOException;
import java.net.ServerSocket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.EnableScheduling;
import io.github.pixee.security.SystemCommand;
import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.config.ConfigInitializer;
import stirling.software.SPDF.model.ApplicationProperties;
@SpringBootApplication
@EnableScheduling
public class SPdfApplication {
private static final Logger logger = LoggerFactory.getLogger(SPdfApplication.class);
@Autowired private Environment env;
@Autowired ApplicationProperties applicationProperties;
private static String baseUrlStatic;
private static String serverPortStatic;
@Value("${baseUrl:http://localhost}")
private String baseUrl;
@Value("${server.port:8080}")
public void setServerPortStatic(String port) {
if ("auto".equalsIgnoreCase(port)) {
// Use Spring Boot's automatic port assignment (server.port=0)
SPdfApplication.serverPortStatic =
"0"; // This will let Spring Boot assign an available port
} else {
SPdfApplication.serverPortStatic = port;
}
}
// Optionally keep this method if you want to provide a manual port-incrementation fallback.
private static String findAvailablePort(int startPort) {
int port = startPort;
while (!isPortAvailable(port)) {
port++;
}
return String.valueOf(port);
}
private static boolean isPortAvailable(int port) {
try (ServerSocket socket = new ServerSocket(port)) {
return true;
} catch (IOException e) {
return false;
}
}
@PostConstruct
public void init() {
baseUrlStatic = this.baseUrl;
// Check if the BROWSER_OPEN environment variable is set to true
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
if (browserOpen) {
try {
String url = baseUrl + ":" + getStaticPort();
String os = System.getProperty("os.name").toLowerCase();
Runtime rt = Runtime.getRuntime();
if (os.contains("win")) {
// For Windows
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
} else if (os.contains("mac")) {
SystemCommand.runCommand(rt, "open " + url);
} else if (os.contains("nix") || os.contains("nux")) {
SystemCommand.runCommand(rt, "xdg-open " + url);
}
} catch (Exception e) {
logger.error("Error opening browser: {}", e.getMessage());
}
}
logger.info("Running configs {}", applicationProperties.toString());
}
public static void main(String[] args) throws IOException, InterruptedException {
SpringApplication app = new SpringApplication(SPdfApplication.class);
app.setAdditionalProfiles("default");
app.addInitializers(new ConfigInitializer());
Map<String, String> propertyFiles = new HashMap<>();
// External config files
if (Files.exists(Paths.get("configs/settings.yml"))) {
propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml");
} else {
logger.warn("External configuration file 'configs/settings.yml' does not exist.");
}
if (Files.exists(Paths.get("configs/custom_settings.yml"))) {
String existingLocation =
propertyFiles.getOrDefault("spring.config.additional-location", "");
if (!existingLocation.isEmpty()) {
existingLocation += ",";
}
propertyFiles.put(
"spring.config.additional-location",
existingLocation + "file:configs/custom_settings.yml");
} else {
logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
}
if (!propertyFiles.isEmpty()) {
app.setDefaultProperties(
Collections.singletonMap(
"spring.config.additional-location",
propertyFiles.get("spring.config.additional-location")));
}
app.run(args);
// Ensure directories are created
try {
Files.createDirectories(Path.of("customFiles/static/"));
Files.createDirectories(Path.of("customFiles/templates/"));
} catch (Exception e) {
logger.error("Error creating directories: {}", e.getMessage());
}
printStartupLogs();
}
private static void printStartupLogs() {
logger.info("Stirling-PDF Started.");
String url = baseUrlStatic + ":" + getStaticPort();
logger.info("Navigate to {}", url);
}
public static String getStaticBaseUrl() {
return baseUrlStatic;
}
public String getNonStaticBaseUrl() {
return baseUrlStatic;
}
public static String getStaticPort() {
return serverPortStatic;
}
public String getNonStaticPort() {
return serverPortStatic;
}
}

View File

@@ -1,7 +0,0 @@
package stirling.software.SPDF.UI;
public interface WebBrowser {
void initWebUI(String url);
void cleanup();
}

View File

@@ -1,412 +0,0 @@
package stirling.software.SPDF.UI.impl;
import java.awt.AWTException;
import java.awt.BorderLayout;
import java.awt.Frame;
import java.awt.Image;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.awt.event.WindowEvent;
import java.awt.event.WindowStateListener;
import java.io.File;
import java.io.InputStream;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import org.cef.CefApp;
import org.cef.CefClient;
import org.cef.CefSettings;
import org.cef.browser.CefBrowser;
import org.cef.callback.CefBeforeDownloadCallback;
import org.cef.callback.CefDownloadItem;
import org.cef.callback.CefDownloadItemCallback;
import org.cef.handler.CefDownloadHandlerAdapter;
import org.cef.handler.CefLoadHandlerAdapter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import me.friwi.jcefmaven.CefAppBuilder;
import me.friwi.jcefmaven.EnumProgress;
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler;
import stirling.software.SPDF.UI.WebBrowser;
import stirling.software.SPDF.config.InstallationPathConfig;
@Component
@Slf4j
@ConditionalOnProperty(
name = "STIRLING_PDF_DESKTOP_UI",
havingValue = "true",
matchIfMissing = false)
public class DesktopBrowser implements WebBrowser {
private static CefApp cefApp;
private static CefClient client;
private static CefBrowser browser;
private static JFrame frame;
private static LoadingWindow loadingWindow;
private static volatile boolean browserInitialized = false;
private static TrayIcon trayIcon;
private static SystemTray systemTray;
public DesktopBrowser() {
SwingUtilities.invokeLater(
() -> {
loadingWindow = new LoadingWindow(null, "Initializing...");
loadingWindow.setVisible(true);
});
}
public void initWebUI(String url) {
CompletableFuture.runAsync(
() -> {
try {
CefAppBuilder builder = new CefAppBuilder();
configureCefSettings(builder);
builder.setProgressHandler(createProgressHandler());
builder.setInstallDir(
new File(InstallationPathConfig.getClientWebUIPath()));
// Build and initialize CEF
cefApp = builder.build();
client = cefApp.createClient();
// Set up download handler
setupDownloadHandler();
// Create browser and frame on EDT
SwingUtilities.invokeAndWait(
() -> {
browser = client.createBrowser(url, false, false);
setupMainFrame();
setupLoadHandler();
// Show the frame immediately but transparent
frame.setVisible(true);
});
} catch (Exception e) {
log.error("Error initializing JCEF browser: ", e);
cleanup();
}
});
}
private void configureCefSettings(CefAppBuilder builder) {
CefSettings settings = builder.getCefSettings();
String basePath = InstallationPathConfig.getClientWebUIPath();
log.info("basePath " + basePath);
settings.cache_path = new File(basePath + "cache").getAbsolutePath();
settings.root_cache_path = new File(basePath + "root_cache").getAbsolutePath();
// settings.browser_subprocess_path = new File(basePath +
// "subprocess").getAbsolutePath();
// settings.resources_dir_path = new File(basePath + "resources").getAbsolutePath();
// settings.locales_dir_path = new File(basePath + "locales").getAbsolutePath();
settings.log_file = new File(basePath, "debug.log").getAbsolutePath();
settings.persist_session_cookies = true;
settings.windowless_rendering_enabled = false;
settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_INFO;
builder.setAppHandler(
new MavenCefAppHandlerAdapter() {
@Override
public void stateHasChanged(org.cef.CefApp.CefAppState state) {
log.info("CEF state changed: " + state);
if (state == CefApp.CefAppState.TERMINATED) {
System.exit(0);
}
}
});
}
private void setupDownloadHandler() {
client.addDownloadHandler(
new CefDownloadHandlerAdapter() {
@Override
public boolean onBeforeDownload(
CefBrowser browser,
CefDownloadItem downloadItem,
String suggestedName,
CefBeforeDownloadCallback callback) {
callback.Continue("", true);
return true;
}
@Override
public void onDownloadUpdated(
CefBrowser browser,
CefDownloadItem downloadItem,
CefDownloadItemCallback callback) {
if (downloadItem.isComplete()) {
log.info("Download completed: " + downloadItem.getFullPath());
} else if (downloadItem.isCanceled()) {
log.info("Download canceled: " + downloadItem.getFullPath());
}
}
});
}
private ConsoleProgressHandler createProgressHandler() {
return new ConsoleProgressHandler() {
@Override
public void handleProgress(EnumProgress state, float percent) {
Objects.requireNonNull(state, "state cannot be null");
SwingUtilities.invokeLater(
() -> {
if (loadingWindow != null) {
switch (state) {
case LOCATING:
loadingWindow.setStatus("Locating Files...");
loadingWindow.setProgress(0);
break;
case DOWNLOADING:
if (percent >= 0) {
loadingWindow.setStatus(
String.format(
"Downloading additional files: %.0f%%",
percent));
loadingWindow.setProgress((int) percent);
}
break;
case EXTRACTING:
loadingWindow.setStatus("Extracting files...");
loadingWindow.setProgress(60);
break;
case INITIALIZING:
loadingWindow.setStatus("Initializing UI...");
loadingWindow.setProgress(80);
break;
case INITIALIZED:
loadingWindow.setStatus("Finalising startup...");
loadingWindow.setProgress(90);
break;
}
}
});
}
};
}
private void setupMainFrame() {
frame = new JFrame("Stirling-PDF");
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
frame.setUndecorated(true);
frame.setOpacity(0.0f);
JPanel contentPane = new JPanel(new BorderLayout());
contentPane.setDoubleBuffered(true);
contentPane.add(browser.getUIComponent(), BorderLayout.CENTER);
frame.setContentPane(contentPane);
frame.addWindowListener(
new java.awt.event.WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent windowEvent) {
cleanup();
System.exit(0);
}
});
frame.setSize(1280, 768);
frame.setLocationRelativeTo(null);
loadIcon();
}
private void setupLoadHandler() {
final long initStartTime = System.currentTimeMillis();
log.info("Setting up load handler at: {}", initStartTime);
client.addLoadHandler(
new CefLoadHandlerAdapter() {
@Override
public void onLoadingStateChange(
CefBrowser browser,
boolean isLoading,
boolean canGoBack,
boolean canGoForward) {
log.debug(
"Loading state change - isLoading: {}, canGoBack: {}, canGoForward: {}, "
+ "browserInitialized: {}, Time elapsed: {}ms",
isLoading,
canGoBack,
canGoForward,
browserInitialized,
System.currentTimeMillis() - initStartTime);
if (!isLoading && !browserInitialized) {
log.info(
"Browser finished loading, preparing to initialize UI components");
browserInitialized = true;
SwingUtilities.invokeLater(
() -> {
try {
if (loadingWindow != null) {
log.info("Starting UI initialization sequence");
// Close loading window first
loadingWindow.setVisible(false);
loadingWindow.dispose();
loadingWindow = null;
log.info("Loading window disposed");
// Then setup the main frame
frame.setVisible(false);
frame.dispose();
frame.setOpacity(1.0f);
frame.setUndecorated(false);
frame.pack();
frame.setSize(1280, 800);
frame.setLocationRelativeTo(null);
log.debug("Frame reconfigured");
// Show the main frame
frame.setVisible(true);
frame.requestFocus();
frame.toFront();
log.info("Main frame displayed and focused");
// Focus the browser component
Timer focusTimer =
new Timer(
100,
e -> {
try {
browser.getUIComponent()
.requestFocus();
log.info(
"Browser component focused");
} catch (Exception ex) {
log.error(
"Error focusing browser",
ex);
}
});
focusTimer.setRepeats(false);
focusTimer.start();
}
} catch (Exception e) {
log.error("Error during UI initialization", e);
// Attempt cleanup on error
if (loadingWindow != null) {
loadingWindow.dispose();
loadingWindow = null;
}
if (frame != null) {
frame.setVisible(true);
frame.requestFocus();
}
}
});
}
}
});
}
private void setupTrayIcon(Image icon) {
if (!SystemTray.isSupported()) {
log.warn("System tray is not supported");
return;
}
try {
systemTray = SystemTray.getSystemTray();
// Create popup menu
PopupMenu popup = new PopupMenu();
// Create menu items
MenuItem showItem = new MenuItem("Show");
showItem.addActionListener(
e -> {
frame.setVisible(true);
frame.setState(Frame.NORMAL);
});
MenuItem exitItem = new MenuItem("Exit");
exitItem.addActionListener(
e -> {
cleanup();
System.exit(0);
});
// Add menu items to popup menu
popup.add(showItem);
popup.addSeparator();
popup.add(exitItem);
// Create tray icon
trayIcon = new TrayIcon(icon, "Stirling-PDF", popup);
trayIcon.setImageAutoSize(true);
// Add double-click behavior
trayIcon.addActionListener(
e -> {
frame.setVisible(true);
frame.setState(Frame.NORMAL);
});
// Add tray icon to system tray
systemTray.add(trayIcon);
// Modify frame behavior to minimize to tray
frame.addWindowStateListener(
new WindowStateListener() {
public void windowStateChanged(WindowEvent e) {
if (e.getNewState() == Frame.ICONIFIED) {
frame.setVisible(false);
}
}
});
} catch (AWTException e) {
log.error("Error setting up system tray icon", e);
}
}
private void loadIcon() {
try {
Image icon = null;
String[] iconPaths = {"/static/favicon.ico"};
for (String path : iconPaths) {
if (icon != null) break;
try {
try (InputStream is = getClass().getResourceAsStream(path)) {
if (is != null) {
icon = ImageIO.read(is);
break;
}
}
} catch (Exception e) {
log.debug("Could not load icon from " + path, e);
}
}
if (icon != null) {
frame.setIconImage(icon);
setupTrayIcon(icon);
} else {
log.warn("Could not load icon from any source");
}
} catch (Exception e) {
log.error("Error loading icon", e);
}
}
@PreDestroy
public void cleanup() {
if (browser != null) browser.close(true);
if (client != null) client.dispose();
if (cefApp != null) cefApp.dispose();
if (loadingWindow != null) loadingWindow.dispose();
}
}

View File

@@ -1,165 +0,0 @@
package stirling.software.SPDF.UI.impl;
import java.awt.*;
import java.io.InputStream;
import javax.imageio.ImageIO;
import javax.swing.*;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LoadingWindow extends JDialog {
private final JProgressBar progressBar;
private final JLabel statusLabel;
private final JPanel mainPanel;
private final JLabel brandLabel;
private long startTime;
public LoadingWindow(Frame parent, String initialUrl) {
super(parent, "Initializing Stirling-PDF", true);
startTime = System.currentTimeMillis();
log.info("Creating LoadingWindow - initialization started at: {}", startTime);
// Initialize components
mainPanel = new JPanel();
mainPanel.setBackground(Color.WHITE);
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 30, 20, 30));
mainPanel.setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
// Configure GridBagConstraints
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.insets = new Insets(5, 5, 5, 5);
gbc.weightx = 1.0;
gbc.weighty = 0.0;
// Add icon
try {
try (InputStream is = getClass().getResourceAsStream("/static/favicon.ico")) {
if (is != null) {
Image img = ImageIO.read(is);
if (img != null) {
Image scaledImg = img.getScaledInstance(48, 48, Image.SCALE_SMOOTH);
JLabel iconLabel = new JLabel(new ImageIcon(scaledImg));
iconLabel.setHorizontalAlignment(SwingConstants.CENTER);
gbc.gridy = 0;
mainPanel.add(iconLabel, gbc);
log.debug("Icon loaded and scaled successfully");
}
}
}
} catch (Exception e) {
log.error("Failed to load icon", e);
}
// URL Label with explicit size
brandLabel = new JLabel(initialUrl);
brandLabel.setHorizontalAlignment(SwingConstants.CENTER);
brandLabel.setPreferredSize(new Dimension(300, 25));
brandLabel.setText("Stirling-PDF");
gbc.gridy = 1;
mainPanel.add(brandLabel, gbc);
// Status label with explicit size
statusLabel = new JLabel("Initializing...");
statusLabel.setHorizontalAlignment(SwingConstants.CENTER);
statusLabel.setPreferredSize(new Dimension(300, 25));
gbc.gridy = 2;
mainPanel.add(statusLabel, gbc);
// Progress bar with explicit size
progressBar = new JProgressBar(0, 100);
progressBar.setStringPainted(true);
progressBar.setPreferredSize(new Dimension(300, 25));
gbc.gridy = 3;
mainPanel.add(progressBar, gbc);
// Set dialog properties
setContentPane(mainPanel);
setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
setResizable(false);
setUndecorated(false);
// Set size and position
setSize(400, 200);
setLocationRelativeTo(parent);
setAlwaysOnTop(true);
setProgress(0);
setStatus("Starting...");
log.info(
"LoadingWindow initialization completed in {}ms",
System.currentTimeMillis() - startTime);
}
public void setProgress(final int progress) {
SwingUtilities.invokeLater(
() -> {
try {
int validProgress = Math.min(Math.max(progress, 0), 100);
log.info(
"Setting progress to {}% at {}ms since start",
validProgress, System.currentTimeMillis() - startTime);
// Log additional details when near 90%
if (validProgress >= 85 && validProgress <= 95) {
log.info(
"Near 90% progress - Current status: {}, Window visible: {}, "
+ "Progress bar responding: {}, Memory usage: {}MB",
statusLabel.getText(),
isVisible(),
progressBar.isEnabled(),
Runtime.getRuntime().totalMemory() / (1024 * 1024));
// Add thread state logging
Thread currentThread = Thread.currentThread();
log.debug(
"Current thread state - Name: {}, State: {}, Priority: {}",
currentThread.getName(),
currentThread.getState(),
currentThread.getPriority());
}
progressBar.setValue(validProgress);
progressBar.setString(validProgress + "%");
mainPanel.revalidate();
mainPanel.repaint();
} catch (Exception e) {
log.error("Error updating progress to " + progress, e);
}
});
}
public void setStatus(final String status) {
log.info(
"Status update at {}ms - Setting status to: {}",
System.currentTimeMillis() - startTime,
status);
SwingUtilities.invokeLater(
() -> {
try {
String validStatus = status != null ? status : "";
statusLabel.setText(validStatus);
// Log UI state when status changes
log.debug(
"UI State - Window visible: {}, Progress: {}%, Status: {}",
isVisible(), progressBar.getValue(), validStatus);
mainPanel.revalidate();
mainPanel.repaint();
} catch (Exception e) {
log.error("Error updating status to: " + status, e);
}
});
}
@Override
public void dispose() {
log.info("LoadingWindow disposing after {}ms", System.currentTimeMillis() - startTime);
super.dispose();
}
}

View File

@@ -7,6 +7,9 @@ import java.nio.file.Paths;
import java.util.Properties; import java.util.Properties;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -18,19 +21,15 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.thymeleaf.spring6.SpringTemplateEngine; import org.thymeleaf.spring6.SpringTemplateEngine;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
@Lazy @Lazy
@Slf4j
public class AppConfig { public class AppConfig {
private final ApplicationProperties applicationProperties; private static final Logger logger = LoggerFactory.getLogger(AppConfig.class);
public AppConfig(ApplicationProperties applicationProperties) { @Autowired ApplicationProperties applicationProperties;
this.applicationProperties = applicationProperties;
}
@Bean @Bean
@ConditionalOnProperty( @ConditionalOnProperty(
@@ -62,7 +61,7 @@ public class AppConfig {
props.load(resource.getInputStream()); props.load(resource.getInputStream());
return props.getProperty("version"); return props.getProperty("version");
} catch (IOException e) { } catch (IOException e) {
log.error("exception", e); logger.error("exception", e);
} }
return "0.0.0"; return "0.0.0";
} }
@@ -102,25 +101,6 @@ public class AppConfig {
return Files.exists(Paths.get("/.dockerenv")); return Files.exists(Paths.get("/.dockerenv"));
} }
@Bean(name = "configDirMounted")
public boolean isRunningInDockerWithConfig() {
Path dockerEnv = Paths.get("/.dockerenv");
// default to true if not docker
if (!Files.exists(dockerEnv)) {
return true;
}
Path mountInfo = Paths.get("/proc/1/mountinfo");
// this should always exist, if not some unknown usecase
if (!Files.exists(mountInfo)) {
return true;
}
try {
return Files.lines(mountInfo).anyMatch(line -> line.contains(" /configs "));
} catch (IOException e) {
return false;
}
}
@Bean(name = "bookAndHtmlFormatsInstalled") @Bean(name = "bookAndHtmlFormatsInstalled")
public boolean bookAndHtmlFormatsInstalled() { public boolean bookAndHtmlFormatsInstalled() {
String installOps = System.getProperty("INSTALL_BOOK_AND_ADVANCED_HTML_OPS"); String installOps = System.getProperty("INSTALL_BOOK_AND_ADVANCED_HTML_OPS");
@@ -136,6 +116,16 @@ public class AppConfig {
return false; return false;
} }
@Bean(name = "watchedFoldersDir")
public String watchedFoldersDir() {
return "./pipeline/watchedFolders/";
}
@Bean(name = "finishedFoldersDir")
public String finishedFoldersDir() {
return "./pipeline/finishedFolders/";
}
@Bean(name = "directoryFilter") @Bean(name = "directoryFilter")
public Predicate<Path> processOnlyFiles() { public Predicate<Path> processOnlyFiles() {
return path -> { return path -> {

View File

@@ -11,16 +11,10 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Service @Service
class AppUpdateService { class AppUpdateService {
private final ApplicationProperties applicationProperties; @Autowired private ApplicationProperties applicationProperties;
private final ShowAdminInterface showAdmin; @Autowired(required = false)
ShowAdminInterface showAdmin;
public AppUpdateService(
ApplicationProperties applicationProperties,
@Autowired(required = false) ShowAdminInterface showAdmin) {
this.applicationProperties = applicationProperties;
this.showAdmin = showAdmin;
}
@Bean(name = "shouldShow") @Bean(name = "shouldShow")
@Scope("request") @Scope("request")

View File

@@ -16,15 +16,28 @@ import org.simpleyaml.configuration.comments.CommentType;
import org.simpleyaml.configuration.file.YamlFile; import org.simpleyaml.configuration.file.YamlFile;
import org.simpleyaml.configuration.implementation.SimpleYamlImplementation; import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions; import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import lombok.extern.slf4j.Slf4j; public class ConfigInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Slf4j private static final Logger logger = LoggerFactory.getLogger(ConfigInitializer.class);
public class ConfigInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
try {
ensureConfigExists();
} catch (Exception e) {
throw new RuntimeException("Failed to initialize application configuration", e);
}
}
public void ensureConfigExists() throws IOException, URISyntaxException { public void ensureConfigExists() throws IOException, URISyntaxException {
// Define the path to the external config directory // Define the path to the external config directory
Path destPath = Paths.get(InstallationPathConfig.getSettingsPath()); Path destPath = Paths.get("configs", "settings.yml");
// Check if the file already exists // Check if the file already exists
if (Files.notExists(destPath)) { if (Files.notExists(destPath)) {
@@ -41,11 +54,10 @@ public class ConfigInitializer {
"Resource file not found: settings.yml.template"); "Resource file not found: settings.yml.template");
} }
} }
log.info("Created settings file from template");
} else { } else {
// Define the path to the config settings file // Define the path to the config settings file
Path settingsPath = Paths.get(InstallationPathConfig.getSettingsPath()); Path settingsPath = Paths.get("configs", "settings.yml");
// Load the template resource // Load the template resource
URL settingsTemplateResource = URL settingsTemplateResource =
getClass().getClassLoader().getResource("settings.yml.template"); getClass().getClassLoader().getResource("settings.yml.template");
@@ -109,7 +121,7 @@ public class ConfigInitializer {
} }
// Create custom settings file if it doesn't exist // Create custom settings file if it doesn't exist
Path customSettingsPath = Paths.get(InstallationPathConfig.getCustomSettingsPath()); Path customSettingsPath = Paths.get("configs", "custom_settings.yml");
if (!Files.exists(customSettingsPath)) { if (!Files.exists(customSettingsPath)) {
Files.createFile(customSettingsPath); Files.createFile(customSettingsPath);
} }
@@ -137,7 +149,7 @@ public class ConfigInitializer {
.commentSide(settingsTemplateFile.getComment(path, CommentType.SIDE)); .commentSide(settingsTemplateFile.getComment(path, CommentType.SIDE));
} else { } else {
// Log if the key is not found in both YAML files // Log if the key is not found in both YAML files
log.info("Key not found in both YAML files: " + path); logger.info("Key not found in both YAML files: " + path);
} }
} }
} }

View File

@@ -1,6 +1,5 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -8,23 +7,24 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Service @Service
@Slf4j
@DependsOn({"bookAndHtmlFormatsInstalled"}) @DependsOn({"bookAndHtmlFormatsInstalled"})
public class EndpointConfiguration { public class EndpointConfiguration {
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
private static final String REMOVE_BLANKS = "remove-blanks";
private final ApplicationProperties applicationProperties;
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>(); private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>(); private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
private final ApplicationProperties applicationProperties;
private boolean bookAndHtmlFormatsInstalled; private boolean bookAndHtmlFormatsInstalled;
@Autowired @Autowired
@@ -43,7 +43,7 @@ public class EndpointConfiguration {
public void disableEndpoint(String endpoint) { public void disableEndpoint(String endpoint) {
if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) { if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
log.debug("Disabling {}", endpoint); logger.debug("Disabling {}", endpoint);
endpointStatuses.put(endpoint, false); endpointStatuses.put(endpoint, false);
} }
} }
@@ -87,7 +87,7 @@ public class EndpointConfiguration {
.collect(Collectors.toList()); .collect(Collectors.toList());
if (!disabledList.isEmpty()) { if (!disabledList.isEmpty()) {
log.info( logger.info(
"Total disabled endpoints: {}. Disabled endpoints: {}", "Total disabled endpoints: {}. Disabled endpoints: {}",
disabledList.size(), disabledList.size(),
String.join(", ", disabledList)); String.join(", ", disabledList));
@@ -136,7 +136,6 @@ public class EndpointConfiguration {
addEndpointToGroup("Security", "remove-cert-sign"); addEndpointToGroup("Security", "remove-cert-sign");
addEndpointToGroup("Security", "sanitize-pdf"); addEndpointToGroup("Security", "sanitize-pdf");
addEndpointToGroup("Security", "auto-redact"); addEndpointToGroup("Security", "auto-redact");
addEndpointToGroup("Security", "redact");
// Adding endpoints to "Other" group // Adding endpoints to "Other" group
addEndpointToGroup("Other", "ocr-pdf"); addEndpointToGroup("Other", "ocr-pdf");
@@ -236,7 +235,6 @@ public class EndpointConfiguration {
addEndpointToGroup("Java", "markdown-to-pdf"); addEndpointToGroup("Java", "markdown-to-pdf");
addEndpointToGroup("Java", "show-javascript"); addEndpointToGroup("Java", "show-javascript");
addEndpointToGroup("Java", "auto-redact"); addEndpointToGroup("Java", "auto-redact");
addEndpointToGroup("Java", "redact");
addEndpointToGroup("Java", "pdf-to-csv"); addEndpointToGroup("Java", "pdf-to-csv");
addEndpointToGroup("Java", "split-by-size-or-count"); addEndpointToGroup("Java", "split-by-size-or-count");
addEndpointToGroup("Java", "overlay-pdf"); addEndpointToGroup("Java", "overlay-pdf");
@@ -262,32 +260,23 @@ public class EndpointConfiguration {
// Pdftohtml dependent endpoints // Pdftohtml dependent endpoints
addEndpointToGroup("Pdftohtml", "pdf-to-html"); addEndpointToGroup("Pdftohtml", "pdf-to-html");
// disabled for now while we resolve issues
disableEndpoint("pdf-to-pdfa");
} }
private void processEnvironmentConfigs() { private void processEnvironmentConfigs() {
if (applicationProperties != null && applicationProperties.getEndpoints() != null) { List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove(); List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove(); if (!bookAndHtmlFormatsInstalled) {
groupsToRemove.add("Calibre");
if (!bookAndHtmlFormatsInstalled) { }
if (groupsToRemove == null) { if (endpointsToRemove != null) {
groupsToRemove = new ArrayList<>(); for (String endpoint : endpointsToRemove) {
} disableEndpoint(endpoint.trim());
groupsToRemove.add("Calibre");
}
if (endpointsToRemove != null) {
for (String endpoint : endpointsToRemove) {
disableEndpoint(endpoint.trim());
}
} }
}
if (groupsToRemove != null) { if (groupsToRemove != null) {
for (String group : groupsToRemove) { for (String group : groupsToRemove) {
disableGroup(group.trim()); disableGroup(group.trim());
}
} }
} }
} }
@@ -295,4 +284,6 @@ public class EndpointConfiguration {
public Set<String> getEndpointsForGroup(String group) { public Set<String> getEndpointsForGroup(String group) {
return endpointGroups.getOrDefault(group, new HashSet<>()); return endpointGroups.getOrDefault(group, new HashSet<>());
} }
private static final String REMOVE_BLANKS = "remove-blanks";
} }

View File

@@ -1,5 +1,6 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
@@ -9,11 +10,7 @@ import jakarta.servlet.http.HttpServletResponse;
@Component @Component
public class EndpointInterceptor implements HandlerInterceptor { public class EndpointInterceptor implements HandlerInterceptor {
private final EndpointConfiguration endpointConfiguration; @Autowired private EndpointConfiguration endpointConfiguration;
public EndpointInterceptor(EndpointConfiguration endpointConfiguration) {
this.endpointConfiguration = endpointConfiguration;
}
@Override @Override
public boolean preHandle( public boolean preHandle(

View File

@@ -6,6 +6,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
@@ -14,24 +15,7 @@ import lombok.extern.slf4j.Slf4j;
@Configuration @Configuration
@Slf4j @Slf4j
public class ExternalAppDepConfig { public class ExternalAppDepConfig {
@Autowired private EndpointConfiguration endpointConfiguration;
private final EndpointConfiguration endpointConfiguration;
private final Map<String, List<String>> commandToGroupMapping =
new HashMap<>() {
{
put("soffice", List.of("LibreOffice"));
put("weasyprint", List.of("Weasyprint"));
put("pdftohtml", List.of("Pdftohtml"));
put("unoconv", List.of("Unoconv"));
put("qpdf", List.of("qpdf"));
put("tesseract", List.of("tesseract"));
}
};
public ExternalAppDepConfig(EndpointConfiguration endpointConfiguration) {
this.endpointConfiguration = endpointConfiguration;
}
private boolean isCommandAvailable(String command) { private boolean isCommandAvailable(String command) {
try { try {
@@ -50,6 +34,18 @@ public class ExternalAppDepConfig {
} }
} }
private final Map<String, List<String>> commandToGroupMapping =
new HashMap<>() {
{
put("soffice", List.of("LibreOffice"));
put("weasyprint", List.of("Weasyprint"));
put("pdftohtml", List.of("Pdftohtml"));
put("unoconv", List.of("Unoconv"));
put("qpdf", List.of("qpdf"));
put("tesseract", List.of("tesseract"));
}
};
private List<String> getAffectedFeatures(String group) { private List<String> getAffectedFeatures(String group) {
return endpointConfiguration.getEndpointsForGroup(group).stream() return endpointConfiguration.getEndpointsForGroup(group).stream()
.map(endpoint -> formatEndpointAsFeature(endpoint)) .map(endpoint -> formatEndpointAsFeature(endpoint))
@@ -59,6 +55,7 @@ public class ExternalAppDepConfig {
private String formatEndpointAsFeature(String endpoint) { private String formatEndpointAsFeature(String endpoint) {
// First replace common terms // First replace common terms
String feature = endpoint.replace("-", " ").replace("pdf", "PDF").replace("img", "image"); String feature = endpoint.replace("-", " ").replace("pdf", "PDF").replace("img", "image");
// Split into words and capitalize each word // Split into words and capitalize each word
return Arrays.stream(feature.split("\\s+")) return Arrays.stream(feature.split("\\s+"))
.map(word -> capitalizeWord(word)) .map(word -> capitalizeWord(word))
@@ -79,6 +76,7 @@ public class ExternalAppDepConfig {
boolean isAvailable = isCommandAvailable(command); boolean isAvailable = isCommandAvailable(command);
if (!isAvailable) { if (!isAvailable) {
List<String> affectedGroups = commandToGroupMapping.get(command); List<String> affectedGroups = commandToGroupMapping.get(command);
if (affectedGroups != null) { if (affectedGroups != null) {
for (String group : affectedGroups) { for (String group : affectedGroups) {
List<String> affectedFeatures = getAffectedFeatures(group); List<String> affectedFeatures = getAffectedFeatures(group);
@@ -97,6 +95,7 @@ public class ExternalAppDepConfig {
@PostConstruct @PostConstruct
public void checkDependencies() { public void checkDependencies() {
// Check core dependencies // Check core dependencies
checkDependencyAndDisableGroup("tesseract"); checkDependencyAndDisableGroup("tesseract");
checkDependencyAndDisableGroup("soffice"); checkDependencyAndDisableGroup("soffice");
@@ -104,11 +103,13 @@ public class ExternalAppDepConfig {
checkDependencyAndDisableGroup("weasyprint"); checkDependencyAndDisableGroup("weasyprint");
checkDependencyAndDisableGroup("pdftohtml"); checkDependencyAndDisableGroup("pdftohtml");
checkDependencyAndDisableGroup("unoconv"); checkDependencyAndDisableGroup("unoconv");
// Special handling for Python/OpenCV dependencies // Special handling for Python/OpenCV dependencies
boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python"); boolean pythonAvailable = isCommandAvailable("python3") || isCommandAvailable("python");
if (!pythonAvailable) { if (!pythonAvailable) {
List<String> pythonFeatures = getAffectedFeatures("Python"); List<String> pythonFeatures = getAffectedFeatures("Python");
List<String> openCVFeatures = getAffectedFeatures("OpenCV"); List<String> openCVFeatures = getAffectedFeatures("OpenCV");
endpointConfiguration.disableGroup("Python"); endpointConfiguration.disableGroup("Python");
endpointConfiguration.disableGroup("OpenCV"); endpointConfiguration.disableGroup("OpenCV");
log.warn( log.warn(

View File

@@ -33,8 +33,7 @@ public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateRe
String characterEncoding, String characterEncoding,
Map<String, Object> templateResolutionAttributes) { Map<String, Object> templateResolutionAttributes) {
Resource resource = Resource resource =
resourceLoader.getResource( resourceLoader.getResource("file:./customFiles/templates/" + resourceName);
"file:" + InstallationPathConfig.getTemplatesPath() + resourceName);
try { try {
if (resource.exists() && resource.isReadable()) { if (resource.exists() && resource.isReadable()) {
return new FileTemplateResource(resource.getFile().getPath(), characterEncoding); return new FileTemplateResource(resource.getFile().getPath(), characterEncoding);

View File

@@ -1,13 +1,11 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.io.IOException; import java.io.IOException;
import java.util.Properties;
import java.util.UUID; import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import io.micrometer.common.util.StringUtils; import io.micrometer.common.util.StringUtils;
@@ -22,80 +20,44 @@ import stirling.software.SPDF.utils.GeneralUtils;
@Order(Ordered.HIGHEST_PRECEDENCE + 1) @Order(Ordered.HIGHEST_PRECEDENCE + 1)
public class InitialSetup { public class InitialSetup {
private final ApplicationProperties applicationProperties; @Autowired private ApplicationProperties applicationProperties;
public InitialSetup(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@PostConstruct @PostConstruct
public void init() throws IOException {
initUUIDKey();
initSecretKey();
initEnableCSRFSecurity();
initLegalUrls();
initSetAppVersion();
}
public void initUUIDKey() throws IOException { public void initUUIDKey() throws IOException {
String uuid = applicationProperties.getAutomaticallyGenerated().getUUID(); String uuid = applicationProperties.getAutomaticallyGenerated().getUUID();
if (!GeneralUtils.isValidUUID(uuid)) { if (!GeneralUtils.isValidUUID(uuid)) {
// Generating a random UUID as the secret key uuid = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
uuid = UUID.randomUUID().toString();
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.UUID", uuid); GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.UUID", uuid);
applicationProperties.getAutomaticallyGenerated().setUUID(uuid); applicationProperties.getAutomaticallyGenerated().setUUID(uuid);
} }
} }
@PostConstruct
public void initSecretKey() throws IOException { public void initSecretKey() throws IOException {
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey(); String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
if (!GeneralUtils.isValidUUID(secretKey)) { if (!GeneralUtils.isValidUUID(secretKey)) {
// Generating a random UUID as the secret key secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
secretKey = UUID.randomUUID().toString();
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.key", secretKey); GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.key", secretKey);
applicationProperties.getAutomaticallyGenerated().setKey(secretKey); applicationProperties.getAutomaticallyGenerated().setKey(secretKey);
} }
} }
public void initEnableCSRFSecurity() throws IOException { @PostConstruct
if (GeneralUtils.isVersionHigher(
"0.36.0", applicationProperties.getAutomaticallyGenerated().getAppVersion())) {
Boolean csrf = applicationProperties.getSecurity().getCsrfDisabled();
if (!csrf) {
GeneralUtils.saveKeyToConfig("security.csrfDisabled", false, false);
GeneralUtils.saveKeyToConfig("system.enableAnalytics", "true", false);
applicationProperties.getSecurity().setCsrfDisabled(false);
}
}
}
public void initLegalUrls() throws IOException { public void initLegalUrls() throws IOException {
// Initialize Terms and Conditions // Initialize Terms and Conditions
String termsUrl = applicationProperties.getLegal().getTermsAndConditions(); String termsUrl = applicationProperties.getLegal().getTermsAndConditions();
if (StringUtils.isEmpty(termsUrl)) { if (StringUtils.isEmpty(termsUrl)) {
String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions"; String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions";
GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl, false); GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl);
applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl); applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl);
} }
// Initialize Privacy Policy // Initialize Privacy Policy
String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy(); String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy();
if (StringUtils.isEmpty(privacyUrl)) { if (StringUtils.isEmpty(privacyUrl)) {
String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy"; String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy";
GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl, false); GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl);
applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl); applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl);
} }
} }
public void initSetAppVersion() throws IOException {
String appVersion = "0.0.0";
Resource resource = new ClassPathResource("version.properties");
Properties props = new Properties();
try {
props.load(resource.getInputStream());
appVersion = props.getProperty("version");
} catch (Exception e) {
}
applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion);
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.appVersion", appVersion, false);
}
} }

View File

@@ -1,132 +0,0 @@
package stirling.software.SPDF.config;
import java.io.File;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class InstallationPathConfig {
private static final String BASE_PATH;
// Root paths
private static final String LOG_PATH;
private static final String CONFIG_PATH;
private static final String PIPELINE_PATH;
private static final String CUSTOM_FILES_PATH;
private static final String CLIENT_WEBUI_PATH;
// Config paths
private static final String SETTINGS_PATH;
private static final String CUSTOM_SETTINGS_PATH;
// Pipeline paths
private static final String PIPELINE_WATCHED_FOLDERS_PATH;
private static final String PIPELINE_FINISHED_FOLDERS_PATH;
// Custom file paths
private static final String STATIC_PATH;
private static final String TEMPLATES_PATH;
private static final String SIGNATURES_PATH;
static {
BASE_PATH = initializeBasePath();
// Initialize root paths
LOG_PATH = BASE_PATH + "logs" + File.separator;
CONFIG_PATH = BASE_PATH + "configs" + File.separator;
PIPELINE_PATH = BASE_PATH + "pipeline" + File.separator;
CUSTOM_FILES_PATH = BASE_PATH + "customFiles" + File.separator;
CLIENT_WEBUI_PATH = BASE_PATH + "clientWebUI" + File.separator;
// Initialize config paths
SETTINGS_PATH = CONFIG_PATH + "settings.yml";
CUSTOM_SETTINGS_PATH = CONFIG_PATH + "custom_settings.yml";
// Initialize pipeline paths
PIPELINE_WATCHED_FOLDERS_PATH = PIPELINE_PATH + "watchedFolders" + File.separator;
PIPELINE_FINISHED_FOLDERS_PATH = PIPELINE_PATH + "finishedFolders" + File.separator;
// Initialize custom file paths
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
TEMPLATES_PATH = CUSTOM_FILES_PATH + "templates" + File.separator;
SIGNATURES_PATH = CUSTOM_FILES_PATH + "signatures" + File.separator;
}
private static String initializeBasePath() {
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("win")) {
return System.getenv("APPDATA") + File.separator + "Stirling-PDF" + File.separator;
} else if (os.contains("mac")) {
return System.getProperty("user.home")
+ File.separator
+ "Library"
+ File.separator
+ "Application Support"
+ File.separator
+ "Stirling-PDF"
+ File.separator;
} else {
return System.getProperty("user.home")
+ File.separator
+ ".config"
+ File.separator
+ "Stirling-PDF"
+ File.separator;
}
}
return "./";
}
public static String getPath() {
return BASE_PATH;
}
public static String getLogPath() {
return LOG_PATH;
}
public static String getConfigPath() {
return CONFIG_PATH;
}
public static String getPipelinePath() {
return PIPELINE_PATH;
}
public static String getCustomFilesPath() {
return CUSTOM_FILES_PATH;
}
public static String getClientWebUIPath() {
return CLIENT_WEBUI_PATH;
}
public static String getSettingsPath() {
return SETTINGS_PATH;
}
public static String getCustomSettingsPath() {
return CUSTOM_SETTINGS_PATH;
}
public static String getPipelineWatchedFoldersDir() {
return PIPELINE_WATCHED_FOLDERS_PATH;
}
public static String getPipelineFinishedFoldersDir() {
return PIPELINE_FINISHED_FOLDERS_PATH;
}
public static String getStaticPath() {
return STATIC_PATH;
}
public static String getTemplatesPath() {
return TEMPLATES_PATH;
}
public static String getSignaturesPath() {
return SIGNATURES_PATH;
}
}

View File

@@ -2,6 +2,7 @@ package stirling.software.SPDF.config;
import java.util.Locale; import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.LocaleResolver;
@@ -15,11 +16,7 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
public class LocaleConfiguration implements WebMvcConfigurer { public class LocaleConfiguration implements WebMvcConfigurer {
private final ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
public LocaleConfiguration(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
@@ -37,17 +34,21 @@ public class LocaleConfiguration implements WebMvcConfigurer {
@Bean @Bean
public LocaleResolver localeResolver() { public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver(); SessionLocaleResolver slr = new SessionLocaleResolver();
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale(); String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
Locale defaultLocale = // Fallback to UK locale if environment variable is not set Locale defaultLocale =
Locale.UK; Locale.UK; // Fallback to UK locale if environment variable is not set
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) { if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv); Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
String tempLanguageTag = tempLocale.toLanguageTag(); String tempLanguageTag = tempLocale.toLanguageTag();
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
defaultLocale = tempLocale; defaultLocale = tempLocale;
} else { } else {
tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_", "-")); tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_", "-"));
tempLanguageTag = tempLocale.toLanguageTag(); tempLanguageTag = tempLocale.toLanguageTag();
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
defaultLocale = tempLocale; defaultLocale = tempLocale;
} else { } else {
@@ -56,6 +57,7 @@ public class LocaleConfiguration implements WebMvcConfigurer {
} }
} }
} }
slr.setDefaultLocale(defaultLocale); slr.setDefaultLocale(defaultLocale);
return slr; return slr;
} }

View File

@@ -1,10 +0,0 @@
package stirling.software.SPDF.config;
import ch.qos.logback.core.PropertyDefinerBase;
public class LogbackPropertyLoader extends PropertyDefinerBase {
@Override
public String getPropertyValue() {
return InstallationPathConfig.getLogPath();
}
}

View File

@@ -1,5 +1,6 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -14,19 +15,15 @@ import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
public class OpenApiConfig { public class OpenApiConfig {
private final ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
public OpenApiConfig(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@Bean @Bean
public OpenAPI customOpenAPI() { public OpenAPI customOpenAPI() {
String version = getClass().getPackage().getImplementationVersion(); String version = getClass().getPackage().getImplementationVersion();
if (version == null) { if (version == null) {
// default version if all else fails version = "1.0.0"; // default version if all else fails
version = "1.0.0";
} }
SecurityScheme apiKeyScheme = SecurityScheme apiKeyScheme =
new SecurityScheme() new SecurityScheme()
.type(SecurityScheme.Type.APIKEY) .type(SecurityScheme.Type.APIKEY)

View File

@@ -1,5 +1,6 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
@@ -8,11 +9,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration @Configuration
public class WebMvcConfig implements WebMvcConfigurer { public class WebMvcConfig implements WebMvcConfigurer {
private final EndpointInterceptor endpointInterceptor; @Autowired private EndpointInterceptor endpointInterceptor;
public WebMvcConfig(EndpointInterceptor endpointInterceptor) {
this.endpointInterceptor = endpointInterceptor;
}
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
@@ -23,8 +20,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
public void addResourceHandlers(ResourceHandlerRegistry registry) { public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Handler for external static resources // Handler for external static resources
registry.addResourceHandler("/**") registry.addResourceHandler("/**")
.addResourceLocations( .addResourceLocations("file:customFiles/static/", "classpath:/static/");
"file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/");
// .setCachePeriod(0); // Optional: disable caching // .setCachePeriod(0); // Optional: disable caching
} }
} }

View File

@@ -16,6 +16,7 @@ public class YamlPropertySourceFactory implements PropertySourceFactory {
throws IOException { throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(encodedResource.getResource()); factory.setResources(encodedResource.getResource());
Properties properties = factory.getObject(); Properties properties = factory.getObject();
return new PropertiesPropertySource( return new PropertiesPropertySource(

View File

@@ -0,0 +1,16 @@
package stirling.software.SPDF.config.interfaces;
import java.io.IOException;
import java.util.List;
import stirling.software.SPDF.utils.FileInfo;
public interface DatabaseBackupInterface {
void exportDatabase() throws IOException;
boolean importDatabase();
boolean hasBackup();
List<FileInfo> getBackupList();
}

View File

@@ -1,17 +0,0 @@
package stirling.software.SPDF.config.interfaces;
import java.sql.SQLException;
import java.util.List;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.FileInfo;
public interface DatabaseInterface {
void exportDatabase() throws SQLException, UnsupportedProviderException;
void importDatabase();
boolean hasBackup();
List<FileInfo> getBackupList();
}

View File

@@ -2,6 +2,7 @@ package stirling.software.SPDF.config.security;
import java.util.Optional; import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -14,15 +15,8 @@ import stirling.software.SPDF.repository.UserRepository;
@Service @Service
class AppUpdateAuthService implements ShowAdminInterface { class AppUpdateAuthService implements ShowAdminInterface {
private final UserRepository userRepository; @Autowired private UserRepository userRepository;
@Autowired private ApplicationProperties applicationProperties;
private final ApplicationProperties applicationProperties;
public AppUpdateAuthService(
UserRepository userRepository, ApplicationProperties applicationProperties) {
this.userRepository = userRepository;
this.applicationProperties = applicationProperties;
}
@Override @Override
public boolean getShowUpdateOnlyAdmins() { public boolean getShowUpdateOnlyAdmins() {
@@ -30,18 +24,24 @@ class AppUpdateAuthService implements ShowAdminInterface {
if (!showUpdate) { if (!showUpdate) {
return showUpdate; return showUpdate;
} }
boolean showUpdateOnlyAdmin = applicationProperties.getSystem().getShowUpdateOnlyAdmin(); boolean showUpdateOnlyAdmin = applicationProperties.getSystem().getShowUpdateOnlyAdmin();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) { if (authentication == null || !authentication.isAuthenticated()) {
return !showUpdateOnlyAdmin; return !showUpdateOnlyAdmin;
} }
if (authentication.getName().equalsIgnoreCase("anonymousUser")) { if (authentication.getName().equalsIgnoreCase("anonymousUser")) {
return !showUpdateOnlyAdmin; return !showUpdateOnlyAdmin;
} }
Optional<User> user = userRepository.findByUsername(authentication.getName()); Optional<User> user = userRepository.findByUsername(authentication.getName());
if (user.isPresent() && showUpdateOnlyAdmin) { if (user.isPresent() && showUpdateOnlyAdmin) {
return "ROLE_ADMIN".equals(user.get().getRolesAsString()); return "ROLE_ADMIN".equals(user.get().getRolesAsString());
} }
return showUpdate; return showUpdate;
} }
} }

View File

@@ -20,7 +20,7 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.SPDFApplication; import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.config.security.saml2.CertificateUtils; import stirling.software.SPDF.config.security.saml2.CertificateUtils;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@@ -110,7 +110,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
// Construct URLs required for SAML configuration // Construct URLs required for SAML configuration
String serverUrl = String serverUrl =
SPDFApplication.getStaticBaseUrl() + ":" + SPDFApplication.getStaticPort(); SPdfApplication.getStaticBaseUrl() + ":" + SPdfApplication.getStaticPort();
String relyingPartyIdentifier = String relyingPartyIdentifier =
serverUrl + "/saml2/service-provider-metadata/" + registrationId; serverUrl + "/saml2/service-provider-metadata/" + registrationId;
@@ -219,9 +219,9 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue=" // "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
// + response.encodeRedirectURL(redirect_url); // + response.encodeRedirectURL(redirect_url);
log.info("Google does not have a specific logout URL"); log.info("Google does not have a specific logout URL");
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl); // log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
// response.sendRedirect(googleLogoutUrl); // response.sendRedirect(googleLogoutUrl);
// break; // break;
default: default:
String defaultRedirectUrl = request.getContextPath() + "/login?" + param; String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
log.info("Redirecting to default logout URL: " + defaultRedirectUrl); log.info("Redirecting to default logout URL: " + defaultRedirectUrl);

View File

@@ -4,6 +4,7 @@ import java.util.Collection;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
@@ -19,15 +20,9 @@ import stirling.software.SPDF.repository.UserRepository;
@Service @Service
public class CustomUserDetailsService implements UserDetailsService { public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository; @Autowired private UserRepository userRepository;
private final LoginAttemptService loginAttemptService; @Autowired private LoginAttemptService loginAttemptService;
public CustomUserDetailsService(
UserRepository userRepository, LoginAttemptService loginAttemptService) {
this.userRepository = userRepository;
this.loginAttemptService = loginAttemptService;
}
@Override @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
@@ -38,13 +33,16 @@ public class CustomUserDetailsService implements UserDetailsService {
() -> () ->
new UsernameNotFoundException( new UsernameNotFoundException(
"No user found with username: " + username)); "No user found with username: " + username));
if (loginAttemptService.isBlocked(username)) { if (loginAttemptService.isBlocked(username)) {
throw new LockedException( throw new LockedException(
"Your account has been locked due to too many failed login attempts."); "Your account has been locked due to too many failed login attempts.");
} }
if (!user.hasPassword()) { if (!user.hasPassword()) {
throw new IllegalArgumentException("Password must not be null"); throw new IllegalArgumentException("Password must not be null");
} }
return new org.springframework.security.core.userdetails.User( return new org.springframework.security.core.userdetails.User(
user.getUsername(), user.getUsername(),
user.getPassword(), user.getPassword(),

View File

@@ -5,6 +5,7 @@ import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.Optional; import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
@@ -24,11 +25,7 @@ import stirling.software.SPDF.utils.RequestUriUtils;
@Component @Component
public class FirstLoginFilter extends OncePerRequestFilter { public class FirstLoginFilter extends OncePerRequestFilter {
@Lazy private final UserService userService; @Autowired @Lazy private UserService userService;
public FirstLoginFilter(@Lazy UserService userService) {
this.userService = userService;
}
@Override @Override
protected void doFilterInternal( protected void doFilterInternal(
@@ -37,13 +34,16 @@ public class FirstLoginFilter extends OncePerRequestFilter {
String method = request.getMethod(); String method = request.getMethod();
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
String contextPath = request.getContextPath(); String contextPath = request.getContextPath();
// Check if the request is for static resources // Check if the request is for static resources
boolean isStaticResource = RequestUriUtils.isStaticResource(contextPath, requestURI); boolean isStaticResource = RequestUriUtils.isStaticResource(contextPath, requestURI);
// If it's a static resource, just continue the filter chain and skip the logic below // If it's a static resource, just continue the filter chain and skip the logic below
if (isStaticResource) { if (isStaticResource) {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
Optional<User> user = userService.findByUsernameIgnoreCase(authentication.getName()); Optional<User> user = userService.findByUsernameIgnoreCase(authentication.getName());
@@ -55,10 +55,12 @@ public class FirstLoginFilter extends OncePerRequestFilter {
return; return;
} }
} }
if (log.isDebugEnabled()) { if (log.isDebugEnabled()) {
HttpSession session = request.getSession(true); HttpSession session = request.getSession(true);
SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss"); SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss");
String creationTime = timeFormat.format(new Date(session.getCreationTime())); String creationTime = timeFormat.format(new Date(session.getCreationTime()));
log.debug( log.debug(
"Request Info - New: {}, creationTimeSession {}, ID: {}, IP: {}, User-Agent: {}, Referer: {}, Request URL: {}", "Request Info - New: {}, creationTimeSession {}, ID: {}, IP: {}, User-Agent: {}, Referer: {}, Request URL: {}",
session.isNew(), session.isNew(),

View File

@@ -4,7 +4,11 @@ import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import jakarta.servlet.*; import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import stirling.software.SPDF.utils.RequestUriUtils; import stirling.software.SPDF.utils.RequestUriUtils;

View File

@@ -1,56 +1,41 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.sql.SQLException; import java.io.IOException;
import java.util.UUID; import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.DatabaseInterface; import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
@Slf4j
@Component @Component
@Slf4j
public class InitialSecuritySetup { public class InitialSecuritySetup {
private final UserService userService; @Autowired private UserService userService;
private final ApplicationProperties applicationProperties; @Autowired private ApplicationProperties applicationProperties;
private final DatabaseInterface databaseService; @Autowired private DatabaseBackupInterface databaseBackupHelper;
public InitialSecuritySetup(
UserService userService,
ApplicationProperties applicationProperties,
DatabaseInterface databaseService) {
this.userService = userService;
this.applicationProperties = applicationProperties;
this.databaseService = databaseService;
}
@PostConstruct @PostConstruct
public void init() { public void init() throws IllegalArgumentException, IOException {
try { if (databaseBackupHelper.hasBackup() && !userService.hasUsers()) {
if (databaseService.hasBackup()) { databaseBackupHelper.importDatabase();
databaseService.importDatabase(); } else if (!userService.hasUsers()) {
} initializeAdminUser();
} else {
if (!userService.hasUsers()) { databaseBackupHelper.exportDatabase();
initializeAdminUser();
}
userService.migrateOauth2ToSSO(); userService.migrateOauth2ToSSO();
initializeInternalApiUser();
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
log.error("Failed to initialize security setup.", e);
System.exit(1);
} }
initializeInternalApiUser();
} }
private void initializeAdminUser() throws SQLException, UnsupportedProviderException { private void initializeAdminUser() throws IOException {
String initialUsername = String initialUsername =
applicationProperties.getSecurity().getInitialLogin().getUsername(); applicationProperties.getSecurity().getInitialLogin().getUsername();
String initialPassword = String initialPassword =
@@ -59,35 +44,36 @@ public class InitialSecuritySetup {
&& !initialUsername.isEmpty() && !initialUsername.isEmpty()
&& initialPassword != null && initialPassword != null
&& !initialPassword.isEmpty() && !initialPassword.isEmpty()
&& userService.findByUsernameIgnoreCase(initialUsername).isEmpty()) { && !userService.findByUsernameIgnoreCase(initialUsername).isPresent()) {
try {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
log.info("Admin user created: {}", initialUsername); log.info("Admin user created: " + initialUsername);
} catch (IllegalArgumentException e) {
log.error("Failed to initialize security setup", e);
System.exit(1);
}
} else { } else {
createDefaultAdminUser(); createDefaultAdminUser();
} }
} }
private void createDefaultAdminUser() throws SQLException, UnsupportedProviderException { private void createDefaultAdminUser() throws IllegalArgumentException, IOException {
String defaultUsername = "admin"; String defaultUsername = "admin";
String defaultPassword = "stirling"; String defaultPassword = "stirling";
if (!userService.findByUsernameIgnoreCase(defaultUsername).isPresent()) {
if (userService.findByUsernameIgnoreCase(defaultUsername).isEmpty()) {
userService.saveUser(defaultUsername, defaultPassword, Role.ADMIN.getRoleId(), true); userService.saveUser(defaultUsername, defaultPassword, Role.ADMIN.getRoleId(), true);
log.info("Default admin user created: {}", defaultUsername); log.info("Default admin user created: " + defaultUsername);
} }
} }
private void initializeInternalApiUser() private void initializeInternalApiUser() throws IllegalArgumentException, IOException {
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!userService.usernameExistsIgnoreCase(Role.INTERNAL_API_USER.getRoleId())) { if (!userService.usernameExistsIgnoreCase(Role.INTERNAL_API_USER.getRoleId())) {
userService.saveUser( userService.saveUser(
Role.INTERNAL_API_USER.getRoleId(), Role.INTERNAL_API_USER.getRoleId(),
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
Role.INTERNAL_API_USER.getRoleId()); Role.INTERNAL_API_USER.getRoleId());
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId()); userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
log.info("Internal API user created: {}", Role.INTERNAL_API_USER.getRoleId()); log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
} }
userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey());
} }
} }

View File

@@ -3,6 +3,7 @@ package stirling.software.SPDF.config.security;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
@@ -14,20 +15,13 @@ import stirling.software.SPDF.model.AttemptCounter;
@Slf4j @Slf4j
public class LoginAttemptService { public class LoginAttemptService {
private final ApplicationProperties applicationProperties; @Autowired private ApplicationProperties applicationProperties;
private int MAX_ATTEMPT; private int MAX_ATTEMPT;
private long ATTEMPT_INCREMENT_TIME; private long ATTEMPT_INCREMENT_TIME;
private ConcurrentHashMap<String, AttemptCounter> attemptsCache; private ConcurrentHashMap<String, AttemptCounter> attemptsCache;
private boolean isBlockedEnabled = true; private boolean isBlockedEnabled = true;
public LoginAttemptService(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@PostConstruct @PostConstruct
public void init() { public void init() {
MAX_ATTEMPT = applicationProperties.getSecurity().getLoginAttemptCount(); MAX_ATTEMPT = applicationProperties.getSecurity().getLoginAttemptCount();
@@ -52,6 +46,7 @@ public class LoginAttemptService {
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) { if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
return; return;
} }
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase()); AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
if (attemptCounter == null) { if (attemptCounter == null) {
attemptCounter = new AttemptCounter(); attemptCounter = new AttemptCounter();
@@ -72,18 +67,20 @@ public class LoginAttemptService {
if (attemptCounter == null) { if (attemptCounter == null) {
return false; return false;
} }
return attemptCounter.getAttemptCount() >= MAX_ATTEMPT; return attemptCounter.getAttemptCount() >= MAX_ATTEMPT;
} }
public int getRemainingAttempts(String key) { public int getRemainingAttempts(String key) {
if (!isBlockedEnabled || key == null || key.trim().isEmpty()) { if (!isBlockedEnabled || key == null || key.trim().isEmpty()) {
// Arbitrarily high number if tracking is disabled return Integer.MAX_VALUE; // Arbitrarily high number if tracking is disabled
return Integer.MAX_VALUE;
} }
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase()); AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
if (attemptCounter == null) { if (attemptCounter == null) {
return MAX_ATTEMPT; return MAX_ATTEMPT;
} }
return MAX_ATTEMPT - attemptCounter.getAttemptCount(); return MAX_ATTEMPT - attemptCounter.getAttemptCount();
} }
} }

View File

@@ -1,45 +1,84 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.*; import java.util.*;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.Resource;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver; import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.savedrequest.NullRequestCache; import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
import stirling.software.SPDF.config.security.saml2.CertificateUtils;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationFailureHandler; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationSuccessHandler; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationSuccessHandler;
import stirling.software.SPDF.config.security.saml2.CustomSaml2ResponseAuthenticationConverter; import stirling.software.SPDF.config.security.saml2.CustomSaml2ResponseAuthenticationConverter;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.repository.JPATokenRepositoryImpl; import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
import stirling.software.SPDF.repository.PersistentLoginRepository;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@@ -48,66 +87,38 @@ import stirling.software.SPDF.repository.PersistentLoginRepository;
@DependsOn("runningEE") @DependsOn("runningEE")
public class SecurityConfiguration { public class SecurityConfiguration {
private final CustomUserDetailsService userDetailsService; @Autowired private CustomUserDetailsService userDetailsService;
private final UserService userService;
@Qualifier("loginEnabled")
private final boolean loginEnabledValue;
@Qualifier("runningEE")
private final boolean runningEE;
private final ApplicationProperties applicationProperties;
private final UserAuthenticationFilter userAuthenticationFilter;
private final LoginAttemptService loginAttemptService;
private final FirstLoginFilter firstLoginFilter;
private final SessionPersistentRegistry sessionRegistry;
private final PersistentLoginRepository persistentLoginRepository;
private final GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper;
private final RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations;
private final OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver;
public SecurityConfiguration(
PersistentLoginRepository persistentLoginRepository,
CustomUserDetailsService userDetailsService,
@Lazy UserService userService,
@Qualifier("loginEnabled") boolean loginEnabledValue,
@Qualifier("runningEE") boolean runningEE,
ApplicationProperties applicationProperties,
UserAuthenticationFilter userAuthenticationFilter,
LoginAttemptService loginAttemptService,
FirstLoginFilter firstLoginFilter,
SessionPersistentRegistry sessionRegistry,
@Autowired(required = false) GrantedAuthoritiesMapper oAuth2userAuthoritiesMapper,
@Autowired(required = false)
RelyingPartyRegistrationRepository saml2RelyingPartyRegistrations,
@Autowired(required = false)
OpenSaml4AuthenticationRequestResolver saml2AuthenticationRequestResolver) {
this.userDetailsService = userDetailsService;
this.userService = userService;
this.loginEnabledValue = loginEnabledValue;
this.runningEE = runningEE;
this.applicationProperties = applicationProperties;
this.userAuthenticationFilter = userAuthenticationFilter;
this.loginAttemptService = loginAttemptService;
this.firstLoginFilter = firstLoginFilter;
this.sessionRegistry = sessionRegistry;
this.persistentLoginRepository = persistentLoginRepository;
this.oAuth2userAuthoritiesMapper = oAuth2userAuthoritiesMapper;
this.saml2RelyingPartyRegistrations = saml2RelyingPartyRegistrations;
this.saml2AuthenticationRequestResolver = saml2AuthenticationRequestResolver;
}
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();
} }
@Autowired @Lazy private UserService userService;
@Autowired
@Qualifier("loginEnabled")
public boolean loginEnabledValue;
@Autowired
@Qualifier("runningEE")
public boolean runningEE;
@Autowired ApplicationProperties applicationProperties;
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
@Autowired private LoginAttemptService loginAttemptService;
@Autowired private FirstLoginFilter firstLoginFilter;
@Autowired private SessionPersistentRegistry sessionRegistry;
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) { if (applicationProperties.getSecurity().getCsrfDisabled()) {
http.csrf(csrf -> csrf.disable()); http.csrf(csrf -> csrf.disable());
} }
if (loginEnabledValue) { if (loginEnabledValue) {
http.addFilterBefore( http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
@@ -121,12 +132,14 @@ public class SecurityConfiguration {
csrf -> csrf ->
csrf.ignoringRequestMatchers( csrf.ignoringRequestMatchers(
request -> { request -> {
String apiKey = request.getHeader("X-API-KEY"); String apiKey = request.getHeader("X-API-Key");
// If there's no API key, don't ignore CSRF // If there's no API key, don't ignore CSRF
// (return false) // (return false)
if (apiKey == null || apiKey.trim().isEmpty()) { if (apiKey == null || apiKey.trim().isEmpty()) {
return false; return false;
} }
// Validate API key using existing UserService // Validate API key using existing UserService
try { try {
Optional<User> user = Optional<User> user =
@@ -150,11 +163,12 @@ public class SecurityConfiguration {
http.sessionManagement( http.sessionManagement(
sessionManagement -> sessionManagement ->
sessionManagement sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(10) .maximumSessions(10)
.maxSessionsPreventsLogin(false) .maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry) .sessionRegistry(sessionRegistry)
.expiredUrl("/login?logout=true")); .expiredUrl("/login?logout=true"));
http.authenticationProvider(daoAuthenticationProvider()); http.authenticationProvider(daoAuthenticationProvider());
http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache())); http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
http.logout( http.logout(
@@ -163,23 +177,18 @@ public class SecurityConfiguration {
.logoutSuccessHandler( .logoutSuccessHandler(
new CustomLogoutSuccessHandler(applicationProperties)) new CustomLogoutSuccessHandler(applicationProperties))
.clearAuthentication(true) .clearAuthentication(true)
.invalidateHttpSession( // Invalidate session .invalidateHttpSession(true) // Invalidate session
true)
.deleteCookies("JSESSIONID", "remember-me")); .deleteCookies("JSESSIONID", "remember-me"));
http.rememberMe( http.rememberMe(
rememberMeConfigurer -> // Use the configurator directly rememberMeConfigurer ->
rememberMeConfigurer rememberMeConfigurer // Use the configurator directly
.tokenRepository(persistentTokenRepository()) .tokenRepository(persistentTokenRepository())
.tokenValiditySeconds( // 14 days .tokenValiditySeconds(14 * 24 * 60 * 60) // 14 days
14 * 24 * 60 * 60) .userDetailsService(
.userDetailsService( // Your existing UserDetailsService userDetailsService) // Your existing UserDetailsService
userDetailsService) .useSecureCookie(true) // Enable secure cookie
.useSecureCookie( // Enable secure cookie .rememberMeParameter("remember-me") // Form parameter name
true) .rememberMeCookieName("remember-me") // Cookie name
.rememberMeParameter( // Form parameter name
"remember-me")
.rememberMeCookieName( // Cookie name
"remember-me")
.alwaysRemember(false)); .alwaysRemember(false));
http.authorizeHttpRequests( http.authorizeHttpRequests(
authz -> authz ->
@@ -187,12 +196,14 @@ public class SecurityConfiguration {
req -> { req -> {
String uri = req.getRequestURI(); String uri = req.getRequestURI();
String contextPath = req.getContextPath(); String contextPath = req.getContextPath();
// Remove the context path from the URI // Remove the context path from the URI
String trimmedUri = String trimmedUri =
uri.startsWith(contextPath) uri.startsWith(contextPath)
? uri.substring( ? uri.substring(
contextPath.length()) contextPath.length())
: uri; : uri;
return trimmedUri.startsWith("/login") return trimmedUri.startsWith("/login")
|| trimmedUri.startsWith("/oauth") || trimmedUri.startsWith("/oauth")
|| trimmedUri.startsWith("/saml2") || trimmedUri.startsWith("/saml2")
@@ -210,6 +221,7 @@ public class SecurityConfiguration {
.permitAll() .permitAll()
.anyRequest() .anyRequest()
.authenticated()); .authenticated());
// Handle User/Password Logins // Handle User/Password Logins
if (applicationProperties.getSecurity().isUserPass()) { if (applicationProperties.getSecurity().isUserPass()) {
http.formLogin( http.formLogin(
@@ -225,26 +237,27 @@ public class SecurityConfiguration {
.defaultSuccessUrl("/") .defaultSuccessUrl("/")
.permitAll()); .permitAll());
} }
// Handle OAUTH2 Logins // Handle OAUTH2 Logins
if (applicationProperties.getSecurity().isOauth2Activ()) { if (applicationProperties.getSecurity().isOauth2Activ()) {
http.oauth2Login( http.oauth2Login(
oauth2 -> oauth2 ->
oauth2.loginPage("/oauth2") oauth2.loginPage("/oauth2")
.
/* /*
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database. This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser' If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
is set as true, else login fails with an error message advising the same. is set as true, else login fails with an error message advising the same.
*/ */
successHandler( .successHandler(
new CustomOAuth2AuthenticationSuccessHandler( new CustomOAuth2AuthenticationSuccessHandler(
loginAttemptService, loginAttemptService,
applicationProperties, applicationProperties,
userService)) userService))
.failureHandler( .failureHandler(
new CustomOAuth2AuthenticationFailureHandler()) new CustomOAuth2AuthenticationFailureHandler())
. // Add existing Authorities from the database // Add existing Authorities from the database
userInfoEndpoint( .userInfoEndpoint(
userInfoEndpoint -> userInfoEndpoint ->
userInfoEndpoint userInfoEndpoint
.oidcUserService( .oidcUserService(
@@ -253,26 +266,28 @@ public class SecurityConfiguration {
userService, userService,
loginAttemptService)) loginAttemptService))
.userAuthoritiesMapper( .userAuthoritiesMapper(
oAuth2userAuthoritiesMapper)) userAuthoritiesMapper()))
.permitAll()); .permitAll());
} }
// Handle SAML // Handle SAML
if (applicationProperties.getSecurity().isSaml2Activ()) { if (applicationProperties.getSecurity().isSaml2Activ()) { // && runningEE
// && runningEE
// Configure the authentication provider // Configure the authentication provider
OpenSaml4AuthenticationProvider authenticationProvider = OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider(); new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter( authenticationProvider.setResponseAuthenticationConverter(
new CustomSaml2ResponseAuthenticationConverter(userService)); new CustomSaml2ResponseAuthenticationConverter(userService));
http.authenticationProvider(authenticationProvider) http.authenticationProvider(authenticationProvider)
.saml2Login( .saml2Login(
saml2 -> { saml2 -> {
try { try {
saml2.loginPage("/saml2") saml2.loginPage("/saml2")
.relyingPartyRegistrationRepository( .relyingPartyRegistrationRepository(
saml2RelyingPartyRegistrations) relyingPartyRegistrations())
.authenticationManager( .authenticationManager(
new ProviderManager(authenticationProvider)) new ProviderManager(authenticationProvider))
.successHandler( .successHandler(
new CustomSaml2AuthenticationSuccessHandler( new CustomSaml2AuthenticationSuccessHandler(
loginAttemptService, loginAttemptService,
@@ -281,19 +296,274 @@ public class SecurityConfiguration {
.failureHandler( .failureHandler(
new CustomSaml2AuthenticationFailureHandler()) new CustomSaml2AuthenticationFailureHandler())
.authenticationRequestResolver( .authenticationRequestResolver(
saml2AuthenticationRequestResolver); authenticationRequestResolver(
relyingPartyRegistrations()));
} catch (Exception e) { } catch (Exception e) {
log.error("Error configuring SAML2 login", e); log.error("Error configuring SAML2 login", e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}); });
} }
} else { } else {
if (!applicationProperties.getSecurity().getCsrfDisabled()) {
CookieCsrfTokenRepository cookieRepo =
CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler =
new CsrfTokenRequestAttributeHandler();
requestHandler.setCsrfRequestAttributeName(null);
http.csrf(
csrf ->
csrf.csrfTokenRepository(cookieRepo)
.csrfTokenRequestHandler(requestHandler));
}
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
} }
return http.build(); return http.build();
} }
@Bean
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
public ClientRegistrationRepository clientRegistrationRepository() {
List<ClientRegistration> registrations = new ArrayList<>();
githubClientRegistration().ifPresent(registrations::add);
oidcClientRegistration().ifPresent(registrations::add);
googleClientRegistration().ifPresent(registrations::add);
keycloakClientRegistration().ifPresent(registrations::add);
if (registrations.isEmpty()) {
log.error("At least one OAuth2 provider must be configured");
System.exit(1);
}
return new InMemoryClientRegistrationRepository(registrations);
}
private Optional<ClientRegistration> googleClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
GoogleProvider google = client.getGoogle();
return google != null && google.isSettingsValid()
? Optional.of(
ClientRegistration.withRegistrationId(google.getName())
.clientId(google.getClientId())
.clientSecret(google.getClientSecret())
.scope(google.getScopes())
.authorizationUri(google.getAuthorizationuri())
.tokenUri(google.getTokenuri())
.userInfoUri(google.getUserinfouri())
.userNameAttributeName(google.getUseAsUsername())
.clientName(google.getClientName())
.redirectUri("{baseUrl}/login/oauth2/code/" + google.getName())
.authorizationGrantType(
org.springframework.security.oauth2.core
.AuthorizationGrantType.AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> keycloakClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
KeycloakProvider keycloak = client.getKeycloak();
return keycloak != null && keycloak.isSettingsValid()
? Optional.of(
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
.registrationId(keycloak.getName())
.clientId(keycloak.getClientId())
.clientSecret(keycloak.getClientSecret())
.scope(keycloak.getScopes())
.userNameAttributeName(keycloak.getUseAsUsername())
.clientName(keycloak.getClientName())
.build())
: Optional.empty();
}
private Optional<ClientRegistration> githubClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
GithubProvider github = client.getGithub();
return github != null && github.isSettingsValid()
? Optional.of(
ClientRegistration.withRegistrationId(github.getName())
.clientId(github.getClientId())
.clientSecret(github.getClientSecret())
.scope(github.getScopes())
.authorizationUri(github.getAuthorizationuri())
.tokenUri(github.getTokenuri())
.userInfoUri(github.getUserinfouri())
.userNameAttributeName(github.getUseAsUsername())
.clientName(github.getClientName())
.redirectUri("{baseUrl}/login/oauth2/code/" + github.getName())
.authorizationGrantType(
org.springframework.security.oauth2.core
.AuthorizationGrantType.AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> oidcClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null
|| oauth.getIssuer() == null
|| oauth.getIssuer().isEmpty()
|| oauth.getClientId() == null
|| oauth.getClientId().isEmpty()
|| oauth.getClientSecret() == null
|| oauth.getClientSecret().isEmpty()
|| oauth.getScopes() == null
|| oauth.getScopes().isEmpty()
|| oauth.getUseAsUsername() == null
|| oauth.getUseAsUsername().isEmpty()) {
return Optional.empty();
}
return Optional.of(
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
.registrationId("oidc")
.clientId(oauth.getClientId())
.clientSecret(oauth.getClientSecret())
.scope(oauth.getScopes())
.userNameAttributeName(oauth.getUseAsUsername())
.clientName("OIDC")
.build());
}
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
Resource privateKeyResource = samlConf.getPrivateKey();
Resource certificateResource = samlConf.getSpCert();
Saml2X509Credential signingCredential =
new Saml2X509Credential(
CertificateUtils.readPrivateKey(privateKeyResource),
CertificateUtils.readCertificate(certificateResource),
Saml2X509CredentialType.SIGNING);
RelyingPartyRegistration rp =
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
.signingX509Credentials(c -> c.add(signingCredential))
.assertingPartyMetadata(
metadata ->
metadata.entityId(samlConf.getIdpIssuer())
.singleSignOnServiceLocation(
samlConf.getIdpSingleLoginUrl())
.verificationX509Credentials(
c -> c.add(verificationCredential))
.singleSignOnServiceBinding(
Saml2MessageBinding.POST)
.wantAuthnRequestsSigned(true))
.build();
return new InMemoryRelyingPartyRegistrationRepository(rp);
}
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
OpenSaml4AuthenticationRequestResolver resolver =
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
resolver.setAuthnRequestCustomizer(
customizer -> {
log.debug("Customizing SAML Authentication request");
AuthnRequest authnRequest = customizer.getAuthnRequest();
log.debug("AuthnRequest ID: {}", authnRequest.getID());
if (authnRequest.getID() == null) {
authnRequest.setID("ARQ" + UUID.randomUUID().toString());
}
log.debug("AuthnRequest new ID after set: {}", authnRequest.getID());
log.debug("AuthnRequest IssueInstant: {}", authnRequest.getIssueInstant());
log.debug(
"AuthnRequest Issuer: {}",
authnRequest.getIssuer() != null
? authnRequest.getIssuer().getValue()
: "null");
HttpServletRequest request = customizer.getRequest();
// Log HTTP request details
log.debug("HTTP Request Method: {}", request.getMethod());
log.debug("Request URI: {}", request.getRequestURI());
log.debug("Request URL: {}", request.getRequestURL().toString());
log.debug("Query String: {}", request.getQueryString());
log.debug("Remote Address: {}", request.getRemoteAddr());
// Log headers
Collections.list(request.getHeaderNames())
.forEach(
headerName -> {
log.debug(
"Header - {}: {}",
headerName,
request.getHeader(headerName));
});
// Log SAML specific parameters
log.debug("SAML Request Parameters:");
log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest"));
log.debug("RelayState: {}", request.getParameter("RelayState"));
// Log session debugrmation if exists
if (request.getSession(false) != null) {
log.debug("Session ID: {}", request.getSession().getId());
}
// Log any assertions consumer service details if present
if (authnRequest.getAssertionConsumerServiceURL() != null) {
log.debug(
"AssertionConsumerServiceURL: {}",
authnRequest.getAssertionConsumerServiceURL());
}
// Log NameID policy if present
if (authnRequest.getNameIDPolicy() != null) {
log.debug(
"NameIDPolicy Format: {}",
authnRequest.getNameIDPolicy().getFormat());
}
});
return resolver;
}
public DaoAuthenticationProvider daoAuthenticationProvider() { public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService); provider.setUserDetailsService(userDetailsService);
@@ -301,20 +571,71 @@ public class SecurityConfiguration {
return provider; return provider;
} }
/*
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
This is required for the internal; 'hasRole()' function to give out the correct role.
*/
@Bean
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(
authority -> {
// Add existing OAUTH2 Authorities
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
// Add Authorities from database for existing user, if user is present.
if (authority instanceof OAuth2UserAuthority oauth2Auth) {
String useAsUsername =
applicationProperties
.getSecurity()
.getOauth2()
.getUseAsUsername();
Optional<User> userOpt =
userService.findByUsernameIgnoreCase(
(String) oauth2Auth.getAttributes().get(useAsUsername));
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user != null) {
mappedAuthorities.add(
new SimpleGrantedAuthority(
userService.findRole(user).getAuthority()));
}
}
}
});
return mappedAuthorities;
};
}
@Bean @Bean
public IPRateLimitingFilter rateLimitingFilter() { public IPRateLimitingFilter rateLimitingFilter() {
// Example limit TODO add config level int maxRequestsPerIp = 1000000; // Example limit TODO add config level
int maxRequestsPerIp = 1000000;
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp); return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
} }
@Bean @Bean
public PersistentTokenRepository persistentTokenRepository() { public PersistentTokenRepository persistentTokenRepository() {
return new JPATokenRepositoryImpl(persistentLoginRepository); return new JPATokenRepositoryImpl();
} }
@Bean @Bean
public boolean activSecurity() { public boolean activSecurity() {
return true; return true;
} }
// // Only Dev test
// @Bean
// public WebSecurityCustomizer webSecurityCustomizer() {
// return (web) ->
// web.ignoring()
// .requestMatchers(
// "/css/**", "/images/**", "/js/**", "/**.svg",
// "/pdfjs-legacy/**");
// }
} }

View File

@@ -22,31 +22,22 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApiKeyAuthenticationToken; import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
@Slf4j
@Component @Component
public class UserAuthenticationFilter extends OncePerRequestFilter { public class UserAuthenticationFilter extends OncePerRequestFilter {
private final ApplicationProperties applicationProperties;
private final UserService userService; private final UserService userService;
private final SessionPersistentRegistry sessionPersistentRegistry; private final SessionPersistentRegistry sessionPersistentRegistry;
private final boolean loginEnabledValue; private final boolean loginEnabledValue;
public UserAuthenticationFilter( public UserAuthenticationFilter(
@Lazy ApplicationProperties applicationProperties,
@Lazy UserService userService, @Lazy UserService userService,
SessionPersistentRegistry sessionPersistentRegistry, SessionPersistentRegistry sessionPersistentRegistry,
@Qualifier("loginEnabled") boolean loginEnabledValue) { @Qualifier("loginEnabled") boolean loginEnabledValue) {
this.applicationProperties = applicationProperties;
this.userService = userService; this.userService = userService;
this.sessionPersistentRegistry = sessionPersistentRegistry; this.sessionPersistentRegistry = sessionPersistentRegistry;
this.loginEnabledValue = loginEnabledValue; this.loginEnabledValue = loginEnabledValue;
@@ -80,7 +71,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
// Check for API key in the request headers if no authentication exists // Check for API key in the request headers if no authentication exists
if (authentication == null || !authentication.isAuthenticated()) { if (authentication == null || !authentication.isAuthenticated()) {
String apiKey = request.getHeader("X-API-KEY"); String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && !apiKey.trim().isEmpty()) { if (apiKey != null && !apiKey.trim().isEmpty()) {
try { try {
// Use API key to authenticate. This requires you to have an authentication // Use API key to authenticate. This requires you to have an authentication
@@ -130,67 +121,33 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
// Check if the authenticated user is disabled and invalidate their session if so // Check if the authenticated user is disabled and invalidate their session if so
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
Security securityProp = applicationProperties.getSecurity();
LoginMethod loginMethod = LoginMethod.UNKNOWN;
boolean blockRegistration = false;
// Extract username and determine the login method
Object principal = authentication.getPrincipal(); Object principal = authentication.getPrincipal();
String username = null; String username = null;
if (principal instanceof UserDetails) { if (principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername(); username = ((UserDetails) principal).getUsername();
loginMethod = LoginMethod.USERDETAILS;
} else if (principal instanceof OAuth2User) { } else if (principal instanceof OAuth2User) {
username = ((OAuth2User) principal).getName(); username = ((OAuth2User) principal).getName();
loginMethod = LoginMethod.OAUTH2USER;
OAUTH2 oAuth = securityProp.getOauth2();
blockRegistration = oAuth != null && oAuth.getBlockRegistration();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) { } else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
username = ((CustomSaml2AuthenticatedPrincipal) principal).getName(); username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
loginMethod = LoginMethod.SAML2USER;
SAML2 saml2 = securityProp.getSaml2();
blockRegistration = saml2 != null && saml2.getBlockRegistration();
} else if (principal instanceof String) { } else if (principal instanceof String) {
username = (String) principal; username = (String) principal;
loginMethod = LoginMethod.STRINGUSER;
} }
// Retrieve all active sessions for the user
List<SessionInformation> sessionsInformations = List<SessionInformation> sessionsInformations =
sessionPersistentRegistry.getAllSessions(principal, false); sessionPersistentRegistry.getAllSessions(principal, false);
// Check if the user exists, is disabled, or needs session invalidation
if (username != null) { if (username != null) {
log.debug("Validating user: {}", username);
boolean isUserExists = userService.usernameExistsIgnoreCase(username); boolean isUserExists = userService.usernameExistsIgnoreCase(username);
boolean isUserDisabled = userService.isUserDisabled(username); boolean isUserDisabled = userService.isUserDisabled(username);
boolean notSsoLogin =
!loginMethod.equals(LoginMethod.OAUTH2USER)
&& !loginMethod.equals(LoginMethod.SAML2USER);
// Block user registration if not allowed by configuration
if (blockRegistration && !isUserExists) {
log.warn("Blocked registration for OAuth2/SAML user: {}", username);
response.sendRedirect(
request.getContextPath() + "/logout?oauth2_admin_blocked_user=true");
return;
}
// Expire sessions and logout if the user does not exist or is disabled
if (!isUserExists || isUserDisabled) { if (!isUserExists || isUserDisabled) {
log.info(
"Invalidating session for disabled or non-existent user: {}", username);
for (SessionInformation sessionsInformation : sessionsInformations) { for (SessionInformation sessionsInformation : sessionsInformations) {
sessionsInformation.expireNow(); sessionsInformation.expireNow();
sessionPersistentRegistry.expireSession(sessionsInformation.getSessionId()); sessionPersistentRegistry.expireSession(sessionsInformation.getSessionId());
} }
} }
// Redirect to logout if credentials are invalid if (!isUserExists) {
if (!isUserExists && notSsoLogin) {
response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true"); response.sendRedirect(request.getContextPath() + "/logout?badcredentials=true");
return; return;
} }
@@ -204,25 +161,6 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }
private enum LoginMethod {
USERDETAILS("UserDetails"),
OAUTH2USER("OAuth2User"),
STRINGUSER("StringUser"),
UNKNOWN("Unknown"),
SAML2USER("Saml2User");
private String method;
LoginMethod(String method) {
this.method = method;
}
@Override
public String toString() {
return method;
}
}
@Override @Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String uri = request.getRequestURI(); String uri = request.getRequestURI();

View File

@@ -5,12 +5,14 @@ import java.time.Duration;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
@@ -29,15 +31,13 @@ import stirling.software.SPDF.model.Role;
public class UserBasedRateLimitingFilter extends OncePerRequestFilter { public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>(); private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>(); private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
@Qualifier("rateLimit") @Autowired private UserDetailsService userDetailsService;
private final boolean rateLimit;
public UserBasedRateLimitingFilter(@Qualifier("rateLimit") boolean rateLimit) { @Autowired
this.rateLimit = rateLimit; @Qualifier("rateLimit")
} public boolean rateLimit;
@Override @Override
protected void doFilterInternal( protected void doFilterInternal(
@@ -48,18 +48,21 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
String method = request.getMethod(); String method = request.getMethod();
if (!"POST".equalsIgnoreCase(method)) { if (!"POST".equalsIgnoreCase(method)) {
// If the request is not a POST, just pass it through without rate limiting // If the request is not a POST, just pass it through without rate limiting
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
String identifier = null; String identifier = null;
// Check for API key in the request headers // Check for API key in the request headers
String apiKey = request.getHeader("X-API-KEY"); String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && !apiKey.trim().isEmpty()) { if (apiKey != null && !apiKey.trim().isEmpty()) {
identifier = // Prefix to distinguish between API keys and usernames identifier =
"API_KEY_" + apiKey; "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
} else { } else {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
@@ -67,13 +70,16 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
identifier = userDetails.getUsername(); identifier = userDetails.getUsername();
} }
} }
// If neither API key nor an authenticated user is present, use IP address // If neither API key nor an authenticated user is present, use IP address
if (identifier == null) { if (identifier == null) {
identifier = request.getRemoteAddr(); identifier = request.getRemoteAddr();
} }
Role userRole = Role userRole =
getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication()); getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
if (request.getHeader("X-API-KEY") != null) {
if (request.getHeader("X-API-Key") != null) {
// It's an API call // It's an API call
processRequest( processRequest(
userRole.getApiCallsPerDay(), userRole.getApiCallsPerDay(),
@@ -117,6 +123,7 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
throws IOException, ServletException { throws IOException, ServletException {
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay)); Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay));
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1); ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
if (probe.isConsumed()) { if (probe.isConsumed()) {
response.setHeader( response.setHeader(
"X-Rate-Limit-Remaining", "X-Rate-Limit-Remaining",

View File

@@ -1,10 +1,10 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import java.sql.SQLException;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -21,12 +21,15 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.DatabaseInterface; import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface; import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.*; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.provider.UnsupportedProviderException; import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.AuthorityRepository; import stirling.software.SPDF.repository.AuthorityRepository;
import stirling.software.SPDF.repository.UserRepository; import stirling.software.SPDF.repository.UserRepository;
@@ -34,36 +37,19 @@ import stirling.software.SPDF.repository.UserRepository;
@Slf4j @Slf4j
public class UserService implements UserServiceInterface { public class UserService implements UserServiceInterface {
private final UserRepository userRepository; @Autowired private UserRepository userRepository;
private final AuthorityRepository authorityRepository; @Autowired private AuthorityRepository authorityRepository;
private final PasswordEncoder passwordEncoder; @Autowired private PasswordEncoder passwordEncoder;
private final MessageSource messageSource; @Autowired private MessageSource messageSource;
private final SessionPersistentRegistry sessionRegistry; @Autowired private SessionPersistentRegistry sessionRegistry;
private final DatabaseInterface databaseService; @Autowired DatabaseBackupInterface databaseBackupHelper;
private final ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
public UserService(
UserRepository userRepository,
AuthorityRepository authorityRepository,
PasswordEncoder passwordEncoder,
MessageSource messageSource,
SessionPersistentRegistry sessionRegistry,
DatabaseInterface databaseService,
ApplicationProperties applicationProperties) {
this.userRepository = userRepository;
this.authorityRepository = authorityRepository;
this.passwordEncoder = passwordEncoder;
this.messageSource = messageSource;
this.sessionRegistry = sessionRegistry;
this.databaseService = databaseService;
this.applicationProperties = applicationProperties;
}
@Transactional @Transactional
public void migrateOauth2ToSSO() { public void migrateOauth2ToSSO() {
@@ -78,7 +64,7 @@ public class UserService implements UserServiceInterface {
// Handle OAUTH2 login and user auto creation. // Handle OAUTH2 login and user auto creation.
public boolean processSSOPostLogin(String username, boolean autoCreateUser) public boolean processSSOPostLogin(String username, boolean autoCreateUser)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, IOException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
return false; return false;
} }
@@ -98,11 +84,13 @@ public class UserService implements UserServiceInterface {
if (!user.isPresent()) { if (!user.isPresent()) {
throw new UsernameNotFoundException("API key is not valid"); throw new UsernameNotFoundException("API key is not valid");
} }
// Convert the user into an Authentication object // Convert the user into an Authentication object
return new UsernamePasswordAuthenticationToken( // principal (typically the user) return new UsernamePasswordAuthenticationToken(
user, // credentials (we don't expose the password or API key here) user, // principal (typically the user)
null, // user's authorities (roles/permissions) null, // credentials (we don't expose the password or API key here)
getAuthorities(user.get())); getAuthorities(user.get()) // user's authorities (roles/permissions)
);
} }
private Collection<? extends GrantedAuthority> getAuthorities(User user) { private Collection<? extends GrantedAuthority> getAuthorities(User user) {
@@ -116,8 +104,7 @@ public class UserService implements UserServiceInterface {
String apiKey; String apiKey;
do { do {
apiKey = UUID.randomUUID().toString(); apiKey = UUID.randomUUID().toString();
} while ( // Ensure uniqueness } while (userRepository.findByApiKey(apiKey).isPresent()); // Ensure uniqueness
userRepository.findByApiKey(apiKey).isPresent());
return apiKey; return apiKey;
} }
@@ -131,8 +118,7 @@ public class UserService implements UserServiceInterface {
} }
public User refreshApiKeyForUser(String username) { public User refreshApiKeyForUser(String username) {
// reuse the add API key method for refreshing return addApiKeyToUser(username); // reuse the add API key method for refreshing
return addApiKeyToUser(username);
} }
public String getApiKeyForUser(String username) { public String getApiKeyForUser(String username) {
@@ -152,11 +138,11 @@ public class UserService implements UserServiceInterface {
public Optional<User> loadUserByApiKey(String apiKey) { public Optional<User> loadUserByApiKey(String apiKey) {
Optional<User> user = userRepository.findByApiKey(apiKey); Optional<User> user = userRepository.findByApiKey(apiKey);
if (user.isPresent()) { if (user.isPresent()) {
return user; return user;
} }
// or throw an exception return null; // or throw an exception
return null;
} }
public boolean validateApiKeyForUser(String username, String apiKey) { public boolean validateApiKeyForUser(String username, String apiKey) {
@@ -165,12 +151,12 @@ public class UserService implements UserServiceInterface {
} }
public void saveUser(String username, AuthenticationType authenticationType) public void saveUser(String username, AuthenticationType authenticationType)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, IOException {
saveUser(username, authenticationType, Role.USER.getRoleId()); saveUser(username, authenticationType, Role.USER.getRoleId());
} }
public void saveUser(String username, AuthenticationType authenticationType, String role) public void saveUser(String username, AuthenticationType authenticationType, String role)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, IOException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage()); throw new IllegalArgumentException(getInvalidUsernameMessage());
} }
@@ -181,11 +167,11 @@ public class UserService implements UserServiceInterface {
user.addAuthority(new Authority(role, user)); user.addAuthority(new Authority(role, user));
user.setAuthenticationType(authenticationType); user.setAuthenticationType(authenticationType);
userRepository.save(user); userRepository.save(user);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
public void saveUser(String username, String password) public void saveUser(String username, String password)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, IOException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage()); throw new IllegalArgumentException(getInvalidUsernameMessage());
} }
@@ -195,11 +181,11 @@ public class UserService implements UserServiceInterface {
user.setEnabled(true); user.setEnabled(true);
user.setAuthenticationType(AuthenticationType.WEB); user.setAuthenticationType(AuthenticationType.WEB);
userRepository.save(user); userRepository.save(user);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
public void saveUser(String username, String password, String role, boolean firstLogin) public void saveUser(String username, String password, String role, boolean firstLogin)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, IOException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage()); throw new IllegalArgumentException(getInvalidUsernameMessage());
} }
@@ -211,11 +197,11 @@ public class UserService implements UserServiceInterface {
user.setAuthenticationType(AuthenticationType.WEB); user.setAuthenticationType(AuthenticationType.WEB);
user.setFirstLogin(firstLogin); user.setFirstLogin(firstLogin);
userRepository.save(user); userRepository.save(user);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
public void saveUser(String username, String password, String role) public void saveUser(String username, String password, String role)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, IOException {
saveUser(username, password, role, false); saveUser(username, password, role, false);
} }
@@ -249,19 +235,21 @@ public class UserService implements UserServiceInterface {
} }
public void updateUserSettings(String username, Map<String, String> updates) public void updateUserSettings(String username, Map<String, String> updates)
throws SQLException, UnsupportedProviderException { throws IOException {
Optional<User> userOpt = findByUsernameIgnoreCaseWithSettings(username); Optional<User> userOpt = findByUsernameIgnoreCaseWithSettings(username);
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
User user = userOpt.get(); User user = userOpt.get();
Map<String, String> settingsMap = user.getSettings(); Map<String, String> settingsMap = user.getSettings();
if (settingsMap == null) { if (settingsMap == null) {
settingsMap = new HashMap<>(); settingsMap = new HashMap<>();
} }
settingsMap.clear(); settingsMap.clear();
settingsMap.putAll(updates); settingsMap.putAll(updates);
user.setSettings(settingsMap); user.setSettings(settingsMap);
userRepository.save(user); userRepository.save(user);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
} }
@@ -282,45 +270,38 @@ public class UserService implements UserServiceInterface {
} }
public void changeUsername(User user, String newUsername) public void changeUsername(User user, String newUsername)
throws IllegalArgumentException, throws IllegalArgumentException, IOException {
IOException,
SQLException,
UnsupportedProviderException {
if (!isUsernameValid(newUsername)) { if (!isUsernameValid(newUsername)) {
throw new IllegalArgumentException(getInvalidUsernameMessage()); throw new IllegalArgumentException(getInvalidUsernameMessage());
} }
user.setUsername(newUsername); user.setUsername(newUsername);
userRepository.save(user); userRepository.save(user);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
public void changePassword(User user, String newPassword) public void changePassword(User user, String newPassword) throws IOException {
throws SQLException, UnsupportedProviderException {
user.setPassword(passwordEncoder.encode(newPassword)); user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user); userRepository.save(user);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
public void changeFirstUse(User user, boolean firstUse) public void changeFirstUse(User user, boolean firstUse) throws IOException {
throws SQLException, UnsupportedProviderException {
user.setFirstLogin(firstUse); user.setFirstLogin(firstUse);
userRepository.save(user); userRepository.save(user);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
public void changeRole(User user, String newRole) public void changeRole(User user, String newRole) throws IOException {
throws SQLException, UnsupportedProviderException {
Authority userAuthority = this.findRole(user); Authority userAuthority = this.findRole(user);
userAuthority.setAuthority(newRole); userAuthority.setAuthority(newRole);
authorityRepository.save(userAuthority); authorityRepository.save(userAuthority);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
public void changeUserEnabled(User user, Boolean enbeled) public void changeUserEnabled(User user, Boolean enbeled) throws IOException {
throws SQLException, UnsupportedProviderException {
user.setEnabled(enbeled); user.setEnabled(enbeled);
userRepository.save(user); userRepository.save(user);
databaseService.exportDatabase(); databaseBackupHelper.exportDatabase();
} }
public boolean isPasswordCorrect(User user, String currentPassword) { public boolean isPasswordCorrect(User user, String currentPassword) {
@@ -335,9 +316,12 @@ public class UserService implements UserServiceInterface {
boolean isValidEmail = boolean isValidEmail =
username.matches( username.matches(
"^(?=.{1,64}@)[A-Za-z0-9]+(\\.[A-Za-z0-9_+.-]+)*@[^-][A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z]{2,})$"); "^(?=.{1,64}@)[A-Za-z0-9]+(\\.[A-Za-z0-9_+.-]+)*@[^-][A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z]{2,})$");
List<String> notAllowedUserList = new ArrayList<>(); List<String> notAllowedUserList = new ArrayList<>();
notAllowedUserList.add("ALL_USERS".toLowerCase()); notAllowedUserList.add("ALL_USERS".toLowerCase());
boolean notAllowedUser = notAllowedUserList.contains(username.toLowerCase()); boolean notAllowedUser = notAllowedUserList.contains(username.toLowerCase());
return (isValidSimpleUsername || isValidEmail) && !notAllowedUser; return (isValidSimpleUsername || isValidEmail) && !notAllowedUser;
} }
@@ -390,6 +374,7 @@ public class UserService implements UserServiceInterface {
public String getCurrentUsername() { public String getCurrentUsername() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) { if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername(); return ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) { } else if (principal instanceof OAuth2User) {
@@ -405,37 +390,6 @@ public class UserService implements UserServiceInterface {
} }
} }
@Transactional
public void syncCustomApiUser(String customApiKey)
throws SQLException, UnsupportedProviderException {
if (customApiKey == null || customApiKey.trim().length() == 0) {
return;
}
String username = "CUSTOM_API_USER";
Optional<User> existingUser = findByUsernameIgnoreCase(username);
if (!existingUser.isPresent()) {
// Create new user with API role
User user = new User();
user.setUsername(username);
user.setPassword(UUID.randomUUID().toString());
user.setEnabled(true);
user.setFirstLogin(false);
user.setAuthenticationType(AuthenticationType.WEB);
user.setApiKey(customApiKey);
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
userRepository.save(user);
databaseService.exportDatabase();
} else {
// Update API key if it has changed
User user = existingUser.get();
if (!customApiKey.equals(user.getApiKey())) {
user.setApiKey(customApiKey);
userRepository.save(user);
databaseService.exportDatabase();
}
}
}
@Override @Override
public long getTotalUsersCount() { public long getTotalUsersCount() {
return userRepository.count(); return userRepository.count();

View File

@@ -0,0 +1,237 @@
package stirling.software.SPDF.config.security.database;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.utils.FileInfo;
@Slf4j
@Configuration
public class DatabaseBackupHelper implements DatabaseBackupInterface {
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String databaseUsername;
@Value("${spring.datasource.password}")
private String databasePassword;
private Path backupPath = Paths.get("configs/db/backup/");
@Override
public boolean hasBackup() {
// Check if there is at least one backup
return !getBackupList().isEmpty();
}
@Override
public List<FileInfo> getBackupList() {
// Check if the backup directory exists, and create it if it does not
ensureBackupDirectoryExists();
List<FileInfo> backupFiles = new ArrayList<>();
// Read the backup directory and filter for files with the prefix "backup_" and suffix
// ".sql"
try (DirectoryStream<Path> stream =
Files.newDirectoryStream(
backupPath,
path ->
path.getFileName().toString().startsWith("backup_")
&& path.getFileName().toString().endsWith(".sql"))) {
for (Path entry : stream) {
BasicFileAttributes attrs = Files.readAttributes(entry, BasicFileAttributes.class);
LocalDateTime modificationDate =
LocalDateTime.ofInstant(
attrs.lastModifiedTime().toInstant(), ZoneId.systemDefault());
LocalDateTime creationDate =
LocalDateTime.ofInstant(
attrs.creationTime().toInstant(), ZoneId.systemDefault());
long fileSize = attrs.size();
backupFiles.add(
new FileInfo(
entry.getFileName().toString(),
entry.toString(),
modificationDate,
fileSize,
creationDate));
}
} catch (IOException e) {
log.error("Error reading backup directory: {}", e.getMessage(), e);
}
return backupFiles;
}
// Imports a database backup from the specified file.
public boolean importDatabaseFromUI(String fileName) throws IOException {
return this.importDatabaseFromUI(getBackupFilePath(fileName));
}
// Imports a database backup from the specified path.
public boolean importDatabaseFromUI(Path tempTemplatePath) throws IOException {
boolean success = executeDatabaseScript(tempTemplatePath);
if (success) {
LocalDateTime dateNow = LocalDateTime.now();
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
Path insertOutputFilePath =
this.getBackupFilePath("backup_user_" + dateNow.format(myFormatObj) + ".sql");
Files.copy(tempTemplatePath, insertOutputFilePath);
Files.deleteIfExists(tempTemplatePath);
}
return success;
}
@Override
public boolean importDatabase() {
if (!this.hasBackup()) return false;
List<FileInfo> backupList = this.getBackupList();
backupList.sort(Comparator.comparing(FileInfo::getModificationDate).reversed());
return executeDatabaseScript(Paths.get(backupList.get(0).getFilePath()));
}
@Override
public void exportDatabase() throws IOException {
// Check if the backup directory exists, and create it if it does not
ensureBackupDirectoryExists();
// Filter and delete old backups if there are more than 5
List<FileInfo> filteredBackupList =
this.getBackupList().stream()
.filter(backup -> !backup.getFileName().startsWith("backup_user_"))
.collect(Collectors.toList());
if (filteredBackupList.size() > 5) {
filteredBackupList.sort(
Comparator.comparing(
p -> p.getFileName().substring(7, p.getFileName().length() - 4)));
Files.deleteIfExists(Paths.get(filteredBackupList.get(0).getFilePath()));
log.info("Deleted oldest backup: {}", filteredBackupList.get(0).getFileName());
}
LocalDateTime dateNow = LocalDateTime.now();
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
Path insertOutputFilePath =
this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql");
String query = "SCRIPT SIMPLE COLUMNS DROP to ?;";
try (Connection conn =
DriverManager.getConnection(url, databaseUsername, databasePassword);
PreparedStatement stmt = conn.prepareStatement(query)) {
stmt.setString(1, insertOutputFilePath.toString());
stmt.execute();
log.info("Database export completed: {}", insertOutputFilePath);
} catch (SQLException e) {
log.error("Error during database export: {}", e.getMessage(), e);
}
}
// Retrieves the H2 database version.
public String getH2Version() {
String version = "Unknown";
try (Connection conn =
DriverManager.getConnection(url, databaseUsername, databasePassword)) {
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) {
if (rs.next()) {
version = rs.getString("version");
log.info("H2 Database Version: {}", version);
}
}
} catch (SQLException e) {
log.error("Error retrieving H2 version: {}", e.getMessage(), e);
}
return version;
}
// Deletes a backup file.
public boolean deleteBackupFile(String fileName) throws IOException {
if (!isValidFileName(fileName)) {
log.error("Invalid file name: {}", fileName);
return false;
}
Path filePath = this.getBackupFilePath(fileName);
if (Files.deleteIfExists(filePath)) {
log.info("Deleted backup file: {}", fileName);
return true;
} else {
log.error("File not found or could not be deleted: {}", fileName);
return false;
}
}
// Gets the Path object for a given backup file name.
public Path getBackupFilePath(String fileName) {
Path filePath = Paths.get(backupPath.toString(), fileName).normalize();
if (!filePath.startsWith(backupPath)) {
throw new SecurityException("Path traversal detected");
}
return filePath;
}
private boolean executeDatabaseScript(Path scriptPath) {
String query = "RUNSCRIPT from ?;";
try (Connection conn =
DriverManager.getConnection(url, databaseUsername, databasePassword);
PreparedStatement stmt = conn.prepareStatement(query)) {
stmt.setString(1, scriptPath.toString());
stmt.execute();
log.info("Database import completed: {}", scriptPath);
return true;
} catch (SQLException e) {
log.error("Error during database import: {}", e.getMessage(), e);
return false;
}
}
private void ensureBackupDirectoryExists() {
if (Files.notExists(backupPath)) {
try {
Files.createDirectories(backupPath);
} catch (IOException e) {
log.error("Error creating directories: {}", e.getMessage());
}
}
}
private boolean isValidFileName(String fileName) {
// Check for invalid characters or sequences
return fileName != null
&& !fileName.contains("..")
&& !fileName.contains("/")
&& !fileName.contains("\\")
&& !fileName.contains(":")
&& !fileName.contains("*")
&& !fileName.contains("?")
&& !fileName.contains("\"")
&& !fileName.contains("<")
&& !fileName.contains(">")
&& !fileName.contains("|");
}
}

View File

@@ -1,139 +0,0 @@
package stirling.software.SPDF.config.security.database;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
@Slf4j
@Getter
@Configuration
public class DatabaseConfig {
public static final String DATASOURCE_DEFAULT_URL =
"jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE";
public static final String DATASOURCE_URL_TEMPLATE = "jdbc:%s://%s:%4d/%s";
public static final String DEFAULT_DRIVER = "org.h2.Driver";
public static final String DEFAULT_USERNAME = "sa";
public static final String POSTGRES_DRIVER = "org.postgresql.Driver";
private final ApplicationProperties applicationProperties;
private final boolean runningEE;
public DatabaseConfig(
ApplicationProperties applicationProperties,
@Qualifier("runningEE") boolean runningEE) {
this.applicationProperties = applicationProperties;
this.runningEE = runningEE;
}
/**
* Creates the <code>DataSource</code> for the connection to the DB. If <code>useDefault</code>
* is set to <code>true</code>, it will use the default H2 DB. If it is set to <code>false
* </code>, it will use the user's custom configuration set in the settings.yml.
*
* @return a <code>DataSource</code> using the configuration settings in the settings.yml
* @throws UnsupportedProviderException if the type of database selected is not supported
*/
@Bean
@Qualifier("dataSource")
public DataSource dataSource() throws UnsupportedProviderException {
DataSourceBuilder<?> dataSourceBuilder = DataSourceBuilder.create();
if (!runningEE) {
return useDefaultDataSource(dataSourceBuilder);
}
ApplicationProperties.System system = applicationProperties.getSystem();
ApplicationProperties.Datasource datasource = system.getDatasource();
if (!datasource.isEnableCustomDatabase()) {
return useDefaultDataSource(dataSourceBuilder);
}
log.info("Using custom database configuration");
if (!datasource.getCustomDatabaseUrl().isBlank()) {
if (datasource.getCustomDatabaseUrl().contains("postgresql")) {
dataSourceBuilder.driverClassName(POSTGRES_DRIVER);
}
dataSourceBuilder.url(datasource.getCustomDatabaseUrl());
} else {
dataSourceBuilder.driverClassName(getDriverClassName(datasource.getType()));
dataSourceBuilder.url(
generateCustomDataSourceUrl(
datasource.getType(),
datasource.getHostName(),
datasource.getPort(),
datasource.getName()));
}
dataSourceBuilder.username(datasource.getUsername());
dataSourceBuilder.password(datasource.getPassword());
return dataSourceBuilder.build();
}
private DataSource useDefaultDataSource(DataSourceBuilder<?> dataSourceBuilder) {
log.info("Using default H2 database");
dataSourceBuilder.url(DATASOURCE_DEFAULT_URL);
dataSourceBuilder.username(DEFAULT_USERNAME);
return dataSourceBuilder.build();
}
/**
* Generate the URL the <code>DataSource</code> will use to connect to the database
*
* @param dataSourceType the type of the database
* @param hostname the host name
* @param port the port number to use for the database
* @param dataSourceName the name the database to connect to
* @return the <code>DataSource</code> URL
*/
private String generateCustomDataSourceUrl(
String dataSourceType, String hostname, Integer port, String dataSourceName) {
return DATASOURCE_URL_TEMPLATE.formatted(dataSourceType, hostname, port, dataSourceName);
}
/**
* Selects the database driver based on the type of database chosen.
*
* @param driverName the type of the driver (e.g. 'h2', 'postgresql')
* @return the fully qualified driver for the database chosen
* @throws UnsupportedProviderException when an unsupported database is selected
*/
private String getDriverClassName(String driverName) throws UnsupportedProviderException {
try {
ApplicationProperties.Driver driver =
ApplicationProperties.Driver.valueOf(driverName.toUpperCase());
switch (driver) {
case H2 -> {
log.debug("H2 driver selected");
return DEFAULT_DRIVER;
}
case POSTGRESQL -> {
log.debug("Postgres driver selected");
return POSTGRES_DRIVER;
}
default -> {
log.warn("{} driver selected", driverName);
throw new UnsupportedProviderException(
driverName + " is not currently supported");
}
}
} catch (IllegalArgumentException e) {
log.warn("Unknown driver: {}", driverName);
throw new UnsupportedProviderException(driverName + " is not currently supported");
}
}
}

View File

@@ -1,301 +0,0 @@
package stirling.software.SPDF.config.security.database;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.init.CannotReadScriptException;
import org.springframework.jdbc.datasource.init.ScriptException;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.exception.BackupNotFoundException;
import stirling.software.SPDF.utils.FileInfo;
@Slf4j
@Service
public class DatabaseService implements DatabaseInterface {
public static final String BACKUP_PREFIX = "backup_";
public static final String SQL_SUFFIX = ".sql";
private static final String BACKUP_DIR = "configs/db/backup/";
private final ApplicationProperties applicationProperties;
private final DataSource dataSource;
public DatabaseService(ApplicationProperties applicationProperties, DataSource dataSource) {
this.applicationProperties = applicationProperties;
this.dataSource = dataSource;
}
/**
* Checks if there is at least one backup. First checks if the directory exists, then checks if
* there are backup scripts within the directory
*
* @return true if there are backup scripts, false if there are not
*/
@Override
public boolean hasBackup() {
Path filePath = Paths.get(BACKUP_DIR);
if (Files.exists(filePath)) {
return !getBackupList().isEmpty();
}
return false;
}
/**
* Read the backup directory and filter for files with the prefix "backup_" and suffix ".sql"
*
* @return a <code>List</code> of backup files
*/
@Override
public List<FileInfo> getBackupList() {
List<FileInfo> backupFiles = new ArrayList<>();
if (isH2Database()) {
Path backupPath = Paths.get(BACKUP_DIR);
try (DirectoryStream<Path> stream =
Files.newDirectoryStream(
backupPath,
path ->
path.getFileName().toString().startsWith(BACKUP_PREFIX)
&& path.getFileName()
.toString()
.endsWith(SQL_SUFFIX))) {
for (Path entry : stream) {
BasicFileAttributes attrs =
Files.readAttributes(entry, BasicFileAttributes.class);
LocalDateTime modificationDate =
LocalDateTime.ofInstant(
attrs.lastModifiedTime().toInstant(), ZoneId.systemDefault());
LocalDateTime creationDate =
LocalDateTime.ofInstant(
attrs.creationTime().toInstant(), ZoneId.systemDefault());
long fileSize = attrs.size();
backupFiles.add(
new FileInfo(
entry.getFileName().toString(),
entry.toString(),
modificationDate,
fileSize,
creationDate));
}
} catch (IOException e) {
log.error("Error reading backup directory: {}", e.getMessage(), e);
}
}
return backupFiles;
}
@Override
public void importDatabase() {
if (!hasBackup()) throw new BackupNotFoundException("No backup scripts were found.");
List<FileInfo> backupList = this.getBackupList();
backupList.sort(Comparator.comparing(FileInfo::getModificationDate).reversed());
Path latestExport = Paths.get(backupList.get(0).getFilePath());
executeDatabaseScript(latestExport);
}
/** Imports a database backup from the specified file. */
public boolean importDatabaseFromUI(String fileName) {
try {
importDatabaseFromUI(getBackupFilePath(fileName));
return true;
} catch (IOException e) {
log.error(
"Error importing database from file: {}, message: {}",
fileName,
e.getMessage(),
e.getCause());
return false;
}
}
/** Imports a database backup from the specified path. */
public boolean importDatabaseFromUI(Path tempTemplatePath) throws IOException {
executeDatabaseScript(tempTemplatePath);
LocalDateTime dateNow = LocalDateTime.now();
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
Path insertOutputFilePath =
this.getBackupFilePath(
BACKUP_PREFIX + "user_" + dateNow.format(myFormatObj) + SQL_SUFFIX);
Files.copy(tempTemplatePath, insertOutputFilePath);
Files.deleteIfExists(tempTemplatePath);
return true;
}
@Override
public void exportDatabase() {
List<FileInfo> filteredBackupList =
this.getBackupList().stream()
.filter(backup -> !backup.getFileName().startsWith(BACKUP_PREFIX + "user_"))
.collect(Collectors.toList());
if (filteredBackupList.size() > 5) {
deleteOldestBackup(filteredBackupList);
}
LocalDateTime dateNow = LocalDateTime.now();
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
Path insertOutputFilePath =
this.getBackupFilePath(BACKUP_PREFIX + dateNow.format(myFormatObj) + SQL_SUFFIX);
if (isH2Database()) {
String query = "SCRIPT SIMPLE COLUMNS DROP to ?;";
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(query)) {
stmt.setString(1, insertOutputFilePath.toString());
stmt.execute();
} catch (SQLException e) {
log.error("Error during database export: {}", e.getMessage(), e);
} catch (CannotReadScriptException e) {
log.error("Error during database export: File {} not found", insertOutputFilePath);
}
log.info("Database export completed: {}", insertOutputFilePath);
}
}
private static void deleteOldestBackup(List<FileInfo> filteredBackupList) {
try {
filteredBackupList.sort(
Comparator.comparing(
p -> p.getFileName().substring(7, p.getFileName().length() - 4)));
FileInfo oldestFile = filteredBackupList.get(0);
Files.deleteIfExists(Paths.get(oldestFile.getFilePath()));
log.info("Deleted oldest backup: {}", oldestFile.getFileName());
} catch (IOException e) {
log.error("Unable to delete oldest backup, message: {}", e.getMessage(), e);
}
}
/**
* Retrieves the H2 database version.
*
* @return <code>String</code> of the H2 version
*/
public String getH2Version() {
String version = "Unknown";
if (isH2Database()) {
try (Connection conn = dataSource.getConnection()) {
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) {
if (rs.next()) {
version = rs.getString("version");
log.info("H2 Database Version: {}", version);
}
}
} catch (SQLException e) {
log.error("Error retrieving H2 version: {}", e.getMessage(), e);
}
}
return version;
}
private boolean isH2Database() {
ApplicationProperties.Datasource datasource =
applicationProperties.getSystem().getDatasource();
return !datasource.isEnableCustomDatabase()
|| datasource.getType().equalsIgnoreCase(ApplicationProperties.Driver.H2.name());
}
/**
* Deletes a backup file.
*
* @return true if successful, false if not
*/
public boolean deleteBackupFile(String fileName) throws IOException {
if (!isValidFileName(fileName)) {
log.error("Invalid file name: {}", fileName);
return false;
}
Path filePath = this.getBackupFilePath(fileName);
if (Files.deleteIfExists(filePath)) {
log.info("Deleted backup file: {}", fileName);
return true;
} else {
log.error("File not found or could not be deleted: {}", fileName);
return false;
}
}
/**
* Gets the Path for a given backup file name.
*
* @return the <code>Path</code> object for the given file name
*/
public Path getBackupFilePath(String fileName) {
Path filePath = Paths.get(BACKUP_DIR, fileName).normalize();
if (!filePath.startsWith(BACKUP_DIR)) {
throw new SecurityException("Path traversal detected");
}
return filePath;
}
private void executeDatabaseScript(Path scriptPath) {
if (isH2Database()) {
String query = "RUNSCRIPT from ?;";
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(query)) {
stmt.setString(1, scriptPath.toString());
stmt.execute();
} catch (SQLException e) {
log.error("Error during database import: {}", e.getMessage(), e);
} catch (ScriptException e) {
log.error("Error: File {} not found", scriptPath.toString(), e);
}
}
log.info("Database import completed: {}", scriptPath);
}
/**
* Checks for invalid characters or sequences
*
* @return true if it contains no invalid characters, false if it does
*/
private boolean isValidFileName(String fileName) {
return fileName != null
&& !fileName.contains("..")
&& !fileName.contains("/")
&& !fileName.contains("\\")
&& !fileName.contains(":")
&& !fileName.contains("*")
&& !fileName.contains("?")
&& !fileName.contains("\"")
&& !fileName.contains("<")
&& !fileName.contains(">")
&& !fileName.contains("|");
}
}

View File

@@ -1,27 +1,18 @@
package stirling.software.SPDF.config.security.database; package stirling.software.SPDF.config.security.database;
import java.sql.SQLException; import java.io.IOException;
import org.springframework.context.annotation.Conditional; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
import stirling.software.SPDF.controller.api.H2SQLCondition;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
@Component @Component
@Conditional(H2SQLCondition.class)
public class ScheduledTasks { public class ScheduledTasks {
private final DatabaseInterface databaseService; @Autowired private DatabaseBackupHelper databaseBackupService;
public ScheduledTasks(DatabaseInterface databaseService) {
this.databaseService = databaseService;
}
@Scheduled(cron = "0 0 0 * * ?") @Scheduled(cron = "0 0 0 * * ?")
public void performBackup() throws SQLException, UnsupportedProviderException { public void performBackup() throws IOException {
databaseService.exportDatabase(); databaseBackupService.exportDatabase();
} }
} }

View File

@@ -1,7 +1,6 @@
package stirling.software.SPDF.config.security.oauth2; package stirling.software.SPDF.config.security.oauth2;
import java.io.IOException; import java.io.IOException;
import java.sql.SQLException;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@@ -19,7 +18,6 @@ import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.AuthenticationType; import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.RequestUriUtils; import stirling.software.SPDF.utils.RequestUriUtils;
public class CustomOAuth2AuthenticationSuccessHandler public class CustomOAuth2AuthenticationSuccessHandler
@@ -99,8 +97,10 @@ public class CustomOAuth2AuthenticationSuccessHandler
userService.processSSOPostLogin(username, oAuth.getAutoCreateUser()); userService.processSSOPostLogin(username, oAuth.getAutoCreateUser());
} }
response.sendRedirect(contextPath + "/"); response.sendRedirect(contextPath + "/");
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) { return;
} catch (IllegalArgumentException e) {
response.sendRedirect(contextPath + "/logout?invalidUsername=true"); response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return;
} }
} }
} }

View File

@@ -2,6 +2,8 @@ package stirling.software.SPDF.config.security.oauth2;
import java.util.Optional; import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
@@ -11,7 +13,6 @@ import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.LoginAttemptService; import stirling.software.SPDF.config.security.LoginAttemptService;
import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@@ -19,7 +20,6 @@ import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client; import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
@Slf4j
public class CustomOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> { public class CustomOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
private final OidcUserService delegate = new OidcUserService(); private final OidcUserService delegate = new OidcUserService();
@@ -30,6 +30,8 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
private ApplicationProperties applicationProperties; private ApplicationProperties applicationProperties;
private static final Logger logger = LoggerFactory.getLogger(CustomOAuth2UserService.class);
public CustomOAuth2UserService( public CustomOAuth2UserService(
ApplicationProperties applicationProperties, ApplicationProperties applicationProperties,
UserService userService, UserService userService,
@@ -80,10 +82,10 @@ public class CustomOAuth2UserService implements OAuth2UserService<OidcUserReques
user.getUserInfo(), user.getUserInfo(),
usernameAttribute); usernameAttribute);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
log.error("Error loading OIDC user: {}", e.getMessage()); logger.error("Error loading OIDC user: {}", e.getMessage());
throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e); throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e);
} catch (Exception e) { } catch (Exception e) {
log.error("Unexpected error loading OIDC user", e); logger.error("Unexpected error loading OIDC user", e);
throw new OAuth2AuthenticationException("Unexpected error during authentication"); throw new OAuth2AuthenticationException("Unexpected error during authentication");
} }
} }

View File

@@ -1,213 +0,0 @@
package stirling.software.SPDF.config.security.oauth2;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider;
@Configuration
@Slf4j
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
public class OAuth2Configuration {
private final ApplicationProperties applicationProperties;
@Lazy private final UserService userService;
public OAuth2Configuration(
ApplicationProperties applicationProperties, @Lazy UserService userService) {
this.userService = userService;
this.applicationProperties = applicationProperties;
}
@Bean
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
public ClientRegistrationRepository clientRegistrationRepository() {
List<ClientRegistration> registrations = new ArrayList<>();
githubClientRegistration().ifPresent(registrations::add);
oidcClientRegistration().ifPresent(registrations::add);
googleClientRegistration().ifPresent(registrations::add);
keycloakClientRegistration().ifPresent(registrations::add);
if (registrations.isEmpty()) {
log.error("At least one OAuth2 provider must be configured");
System.exit(1);
}
return new InMemoryClientRegistrationRepository(registrations);
}
private Optional<ClientRegistration> googleClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
GoogleProvider google = client.getGoogle();
return google != null && google.isSettingsValid()
? Optional.of(
ClientRegistration.withRegistrationId(google.getName())
.clientId(google.getClientId())
.clientSecret(google.getClientSecret())
.scope(google.getScopes())
.authorizationUri(google.getAuthorizationuri())
.tokenUri(google.getTokenuri())
.userInfoUri(google.getUserinfouri())
.userNameAttributeName(google.getUseAsUsername())
.clientName(google.getClientName())
.redirectUri("{baseUrl}/login/oauth2/code/" + google.getName())
.authorizationGrantType(
org.springframework.security.oauth2.core
.AuthorizationGrantType.AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> keycloakClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
KeycloakProvider keycloak = client.getKeycloak();
return keycloak != null && keycloak.isSettingsValid()
? Optional.of(
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
.registrationId(keycloak.getName())
.clientId(keycloak.getClientId())
.clientSecret(keycloak.getClientSecret())
.scope(keycloak.getScopes())
.userNameAttributeName(keycloak.getUseAsUsername())
.clientName(keycloak.getClientName())
.build())
: Optional.empty();
}
private Optional<ClientRegistration> githubClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
GithubProvider github = client.getGithub();
return github != null && github.isSettingsValid()
? Optional.of(
ClientRegistration.withRegistrationId(github.getName())
.clientId(github.getClientId())
.clientSecret(github.getClientSecret())
.scope(github.getScopes())
.authorizationUri(github.getAuthorizationuri())
.tokenUri(github.getTokenuri())
.userInfoUri(github.getUserinfouri())
.userNameAttributeName(github.getUseAsUsername())
.clientName(github.getClientName())
.redirectUri("{baseUrl}/login/oauth2/code/" + github.getName())
.authorizationGrantType(
org.springframework.security.oauth2.core
.AuthorizationGrantType.AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> oidcClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null
|| oauth.getIssuer() == null
|| oauth.getIssuer().isEmpty()
|| oauth.getClientId() == null
|| oauth.getClientId().isEmpty()
|| oauth.getClientSecret() == null
|| oauth.getClientSecret().isEmpty()
|| oauth.getScopes() == null
|| oauth.getScopes().isEmpty()
|| oauth.getUseAsUsername() == null
|| oauth.getUseAsUsername().isEmpty()) {
return Optional.empty();
}
return Optional.of(
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
.registrationId("oidc")
.clientId(oauth.getClientId())
.clientSecret(oauth.getClientSecret())
.scope(oauth.getScopes())
.userNameAttributeName(oauth.getUseAsUsername())
.clientName("OIDC")
.build());
}
/*
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
This is required for the internal; 'hasRole()' function to give out the correct role.
*/
@Bean
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(
authority -> {
// Add existing OAUTH2 Authorities
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
// Add Authorities from database for existing user, if user is present.
if (authority instanceof OAuth2UserAuthority oauth2Auth) {
String useAsUsername =
applicationProperties
.getSecurity()
.getOauth2()
.getUseAsUsername();
Optional<User> userOpt =
userService.findByUsernameIgnoreCase(
(String) oauth2Auth.getAttributes().get(useAsUsername));
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user != null) {
mappedAuthorities.add(
new SimpleGrantedAuthority(
userService.findRole(user).getAuthority()));
}
}
}
});
return mappedAuthorities;
};
}
}

View File

@@ -1,7 +1,6 @@
package stirling.software.SPDF.config.security.saml2; package stirling.software.SPDF.config.security.saml2;
import java.io.IOException; import java.io.IOException;
import java.sql.SQLException;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@@ -19,7 +18,6 @@ import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2; import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.AuthenticationType; import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.RequestUriUtils; import stirling.software.SPDF.utils.RequestUriUtils;
@AllArgsConstructor @AllArgsConstructor
@@ -111,7 +109,7 @@ public class CustomSaml2AuthenticationSuccessHandler
log.debug("Successfully processed authentication for user: {}", username); log.debug("Successfully processed authentication for user: {}", username);
response.sendRedirect(contextPath + "/"); response.sendRedirect(contextPath + "/");
return; return;
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) { } catch (IllegalArgumentException e) {
log.debug( log.debug(
"Invalid username detected for user: {}, redirecting to logout", "Invalid username detected for user: {}, redirecting to logout",
username); username);

View File

@@ -11,11 +11,13 @@ import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
@Component
@Slf4j @Slf4j
public class CustomSaml2ResponseAuthenticationConverter public class CustomSaml2ResponseAuthenticationConverter
implements Converter<ResponseToken, Saml2Authentication> { implements Converter<ResponseToken, Saml2Authentication> {

View File

@@ -1,136 +0,0 @@
package stirling.software.SPDF.config.security.saml2;
import java.security.cert.X509Certificate;
import java.util.Collections;
import java.util.UUID;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
@Configuration
@Slf4j
@ConditionalOnProperty(
value = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public class SAML2Configuration {
private final ApplicationProperties applicationProperties;
public SAML2Configuration(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
Resource privateKeyResource = samlConf.getPrivateKey();
Resource certificateResource = samlConf.getSpCert();
Saml2X509Credential signingCredential =
new Saml2X509Credential(
CertificateUtils.readPrivateKey(privateKeyResource),
CertificateUtils.readCertificate(certificateResource),
Saml2X509CredentialType.SIGNING);
RelyingPartyRegistration rp =
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
.signingX509Credentials(c -> c.add(signingCredential))
.assertingPartyMetadata(
metadata ->
metadata.entityId(samlConf.getIdpIssuer())
.singleSignOnServiceLocation(
samlConf.getIdpSingleLoginUrl())
.verificationX509Credentials(
c -> c.add(verificationCredential))
.singleSignOnServiceBinding(
Saml2MessageBinding.POST)
.wantAuthnRequestsSigned(true))
.build();
return new InMemoryRelyingPartyRegistrationRepository(rp);
}
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
OpenSaml4AuthenticationRequestResolver resolver =
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
resolver.setAuthnRequestCustomizer(
customizer -> {
log.debug("Customizing SAML Authentication request");
AuthnRequest authnRequest = customizer.getAuthnRequest();
log.debug("AuthnRequest ID: {}", authnRequest.getID());
if (authnRequest.getID() == null) {
authnRequest.setID("ARQ" + UUID.randomUUID().toString());
}
log.debug("AuthnRequest new ID after set: {}", authnRequest.getID());
log.debug("AuthnRequest IssueInstant: {}", authnRequest.getIssueInstant());
log.debug(
"AuthnRequest Issuer: {}",
authnRequest.getIssuer() != null
? authnRequest.getIssuer().getValue()
: "null");
HttpServletRequest request = customizer.getRequest();
// Log HTTP request details
log.debug("HTTP Request Method: {}", request.getMethod());
log.debug("Request URI: {}", request.getRequestURI());
log.debug("Request URL: {}", request.getRequestURL().toString());
log.debug("Query String: {}", request.getQueryString());
log.debug("Remote Address: {}", request.getRemoteAddr());
// Log headers
Collections.list(request.getHeaderNames())
.forEach(
headerName -> {
log.debug(
"Header - {}: {}",
headerName,
request.getHeader(headerName));
});
// Log SAML specific parameters
log.debug("SAML Request Parameters:");
log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest"));
log.debug("RelayState: {}", request.getParameter("RelayState"));
// Log session debugrmation if exists
if (request.getSession(false) != null) {
log.debug("Session ID: {}", request.getSession().getId());
}
// Log any assertions consumer service details if present
if (authnRequest.getAssertionConsumerServiceURL() != null) {
log.debug(
"AssertionConsumerServiceURL: {}",
authnRequest.getAssertionConsumerServiceURL());
}
// Log NameID policy if present
if (authnRequest.getNameIDPolicy() != null) {
log.debug(
"NameIDPolicy Format: {}",
authnRequest.getNameIDPolicy().getFormat());
}
});
return resolver;
}
}

Some files were not shown because too many files have changed in this diff Show More