init docker

This commit is contained in:
Anthony Stirling 2024-12-09 18:18:16 +00:00
parent a772b4fa09
commit d59cb18666
18 changed files with 507 additions and 318 deletions

View File

@ -6,7 +6,6 @@ COPY scripts /scripts
COPY pipeline /pipeline
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
#COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
COPY build/libs/*.jar app.jar
ARG VERSION_TAG
@ -19,6 +18,10 @@ ENV DOCKER_ENABLE_SECURITY=false \
PGID=1000 \
UMASK=022
# Create non-root user first
RUN addgroup -S stirlingpdfgroup && \
adduser -S stirlingpdfuser -G stirlingpdfgroup
# JDK for app
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 && \
@ -31,8 +34,6 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
bash \
curl \
qpdf \
shadow \
su-exec \
openssl \
openssl-dev \
openjdk21-jre \
@ -50,15 +51,16 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
# uno unoconv and HTML
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \
mv /usr/share/tessdata /usr/share/tessdata-original && \
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
fc-cache -f -v && \
chmod +x /scripts/* && \
chmod +x /scripts/init.sh && \
# User permissions
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
tesseract --list-langs
mkdir -p ${HOME} /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /scripts /usr/share/fonts/custom && \
chown -R stirlingpdfuser:stirlingpdfgroup ${HOME} /configs /logs /customFiles /pipeline /scripts /usr/share/fonts/custom && \
chmod -R 755 ${HOME} /configs /customFiles /pipeline /scripts /usr/share/fonts/custom && \
tesseract --list-langs && \
chmod -R 777 /logs
COPY build/libs/*.jar app.jar
RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
EXPOSE 8080/tcp

View File

@ -1,4 +1,4 @@
# Build the application
# Build stage
FROM gradle:8.11-jdk17 AS build
# Set the working directory
@ -7,18 +7,20 @@ WORKDIR /app
# Copy the entire project to the working directory
COPY . .
# Build the application with DOCKER_ENABLE_SECURITY=false
# Build the application
RUN DOCKER_ENABLE_SECURITY=true \
./gradlew clean build
./gradlew clean build
# Main stage
FROM alpine:3.20.3
# Copy necessary files
# Create non-root user first
RUN addgroup -S stirlingpdfgroup && \
adduser -S stirlingpdfuser -G stirlingpdfgroup
COPY scripts /scripts
COPY pipeline /pipeline
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
COPY --from=build /app/build/libs/*.jar app.jar
ARG VERSION_TAG
@ -33,51 +35,45 @@ ENV DOCKER_ENABLE_SECURITY=false \
FAT_DOCKER=true \
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
# Create necessary directories with correct permissions
RUN mkdir -p ${HOME} /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /scripts /usr/share/fonts/custom && \
chown -R stirlingpdfuser:stirlingpdfgroup ${HOME} /configs /logs /customFiles /pipeline /scripts /usr/share/fonts/custom && \
chmod -R 755 ${HOME} /configs /logs /customFiles /pipeline /scripts /usr/share/fonts/custom
# JDK for app
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/testing" | tee -a /etc/apk/repositories && \
# JDK and other dependencies
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" >> /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \
apk upgrade --no-cache -a && \
apk add --no-cache \
ca-certificates \
tzdata \
tini \
bash \
curl \
shadow \
su-exec \
openssl \
openssl-dev \
openjdk21-jre \
# Doc conversion
libreoffice \
# pdftohtml
poppler-utils \
# OCR MY PDF (unpaper for descew and other advanced featues)
qpdf \
tesseract-ocr-data-eng \
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra \
# CV
py3-opencv \
# python3/pip
python3 \
ca-certificates \
tzdata \
tini \
bash \
curl \
openssl \
openssl-dev \
openjdk21-jre \
libreoffice \
poppler-utils \
qpdf \
tesseract-ocr-data-eng \
tesseract-ocr-data-fra \
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra \
py3-opencv \
python3 \
py3-pip && \
# uno unoconv and HTML
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint pdf2image pillow && \
mv /usr/share/tessdata /usr/share/tessdata-original && \
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
fc-cache -f -v && \
chmod +x /scripts/* && \
chmod +x /scripts/init.sh && \
# User permissions
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
tesseract --list-langs
mkdir -p /usr/share/tessdata && \
chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/tessdata /usr/share/fonts/opentype/noto && \
fc-cache -f -v
COPY build/libs/*.jar app.jar
RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
EXPOSE 8080/tcp
# Set user and run command
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]

View File

@ -17,8 +17,11 @@ COPY scripts/download-security-jar.sh /scripts/download-security-jar.sh
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
COPY scripts/installFonts.sh /scripts/installFonts.sh
COPY pipeline /pipeline
COPY build/libs/*.jar app.jar
# Create non-root user first
RUN addgroup -S stirlingpdfgroup && \
adduser -S stirlingpdfuser -G stirlingpdfgroup
# Set up necessary directories and permissions
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 && \
@ -30,18 +33,15 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
tini \
bash \
curl \
shadow \
su-exec \
openjdk21-jre && \
# User permissions
mkdir -p /configs /logs /customFiles /usr/share/fonts/opentype/noto && \
chmod +x /scripts/*.sh && \
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar
mkdir -p ${HOME} /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders /scripts /usr/share/fonts/custom && \
chown -R stirlingpdfuser:stirlingpdfgroup ${HOME} /configs /logs /customFiles /pipeline /scripts /usr/share/fonts/custom && \
chmod -R 755 ${HOME} /configs /customFiles /pipeline /scripts /usr/share/fonts/custom && \
chmod -R 777 /logs
# Set environment variables
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
COPY build/libs/*.jar app.jar
RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
EXPOSE 8080/tcp

View File

@ -14,14 +14,17 @@ services:
ports:
- 8080:8080
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
- ./stirling/latest/data:/usr/share/tessdata:rw
- ./stirling/latest/config:/configs:rw
- ./stirling/latest/logs:/logs:rw
user: "stirlingpdfuser"
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "false"
PUID: 1002
PGID: 1002
LANGS: "ALL"
TESSERACT_LANGS: "eng,fra,deu,spa,ita"
UMASK: "022"
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF

View File

@ -14,9 +14,9 @@ services:
ports:
- "8080:8080"
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
- ./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"

View File

@ -14,9 +14,9 @@ services:
ports:
- "8080:8080"
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
- ./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"

View File

@ -14,9 +14,9 @@ services:
ports:
- "8080:8080"
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
- ./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"

View File

@ -14,8 +14,8 @@ services:
ports:
- "8080:8080"
volumes:
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
- ./stirling/latest/config:/configs:rw
- ./stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "false"
SECURITY_ENABLELOGIN: "false"

View File

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

View File

@ -1,37 +1,11 @@
#!/bin/bash
# Update the user and group IDs as per environment variables
if [ ! -z "$PUID" ] && [ "$PUID" != "$(id -u stirlingpdfuser)" ]; then
usermod -o -u "$PUID" stirlingpdfuser || true
fi
if [ ! -z "$PGID" ] && [ "$PGID" != "$(getent group stirlingpdfgroup | cut -d: -f3)" ]; then
groupmod -o -g "$PGID" stirlingpdfgroup || true
fi
umask "$UMASK" || true
if [[ "$INSTALL_BOOK_AND_ADVANCED_HTML_OPS" == "true" && "$FAT_DOCKER" != "true" ]]; then
echo "issue with calibre in current version, feature currently disabled on Stirling-PDF"
#apk add --no-cache calibre@testing
fi
if [[ "$FAT_DOCKER" != "true" ]]; then
/scripts/download-security-jar.sh
/scripts/download-security-jar.sh
fi
if [[ -n "$LANGS" ]]; then
/scripts/installFonts.sh $LANGS
/scripts/installFonts.sh $LANGS
fi
echo "Setting permissions and ownership for necessary directories..."
# Attempt to change ownership of directories and files
if chown -R stirlingpdfuser:stirlingpdfgroup $HOME /logs /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /app.jar; then
chmod -R 755 /logs /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /app.jar || true
# If chown succeeds, execute the command as stirlingpdfuser
exec su-exec stirlingpdfuser "$@"
else
# If chown fails, execute the command without changing the user context
echo "[WARN] Chown failed, running as host user"
exec "$@"
fi
exec "$@"

View File

@ -1,31 +1,39 @@
#!/bin/bash
# Copy the original tesseract-ocr files to the volume directory without overwriting existing files
echo "Copying original files without overwriting existing files"
mkdir -p /usr/share/tessdata
cp -rn /usr/share/tessdata-original/* /usr/share/tessdata
cp -rn /usr/share/tessdata-original/* /usr/share/tessdata 2>/dev/null || true
# Copy additional tessdata if available
if [ -d /usr/share/tesseract-ocr/4.00/tessdata ]; then
cp -r /usr/share/tesseract-ocr/4.00/tessdata/* /usr/share/tessdata || true;
cp -rn /usr/share/tesseract-ocr/4.00/tessdata/* /usr/share/tessdata 2>/dev/null || true
fi
if [ -d /usr/share/tesseract-ocr/5/tessdata ]; then
cp -r /usr/share/tesseract-ocr/5/tessdata/* /usr/share/tessdata || true;
cp -rn /usr/share/tesseract-ocr/5/tessdata/* /usr/share/tessdata 2>/dev/null || true
fi
# Check if TESSERACT_LANGS environment variable is set and is not empty
if [[ -n "$TESSERACT_LANGS" ]]; then
# Convert comma-separated values to a space-separated list
LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ')
pattern='^[a-zA-Z]{2,4}(_[a-zA-Z]{2,4})?$'
# Install each language pack
for LANG in $LANGS; do
if [[ $LANG =~ $pattern ]]; then
apk add --no-cache "tesseract-ocr-data-$LANG"
else
echo "Skipping invalid language code"
fi
done
# Convert comma-separated values to a space-separated list
TES_LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ')
pattern='^[a-zA-Z]{2,4}(_[a-zA-Z]{2,4})?$'
# Log available languages
echo "Currently installed languages:"
tesseract --list-langs
echo "Requested additional languages: $TES_LANGS"
# Instead of apk add, download language files from a known source
for LANG in $TES_LANGS; do
if [[ $LANG =~ $pattern ]]; then
# Download to user-writable directory
wget -P /usr/share/tessdata/ "https://github.com/tesseract-ocr/tessdata/raw/main/${LANG}.traineddata" || \
echo "Failed to download language pack for ${LANG}"
else
echo "Skipping invalid language code"
fi
done
fi
/scripts/init-without-ocr.sh "$@"

View File

@ -1,67 +1,156 @@
#!/bin/bash
LANGS=$1
FONT_DIR="$HOME/.local/share/fonts"
TEMP_DIR=$(mktemp -d)
# Function to install a font package
install_font() {
echo "Installing font package: $1"
if ! apk add "$1" --no-cache; then
echo "Failed to install $1"
fi
# Create fonts directory if it doesn't exist
mkdir -p "$FONT_DIR"
# Function to get latest GitHub release
get_latest_release() {
local repo=$1
local api_url="https://api.github.com/repos/$repo/releases/latest"
curl --silent "$api_url" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/'
}
# Install common fonts used across many languages
#common_fonts=(
# font-terminus
# font-dejavu
# font-noto
# font-noto-cjk
# font-awesome
# font-noto-extra
#)
#
#for font in "${common_fonts[@]}"; do
# install_font $font
#done
# Function to download and install a font
install_font() {
local font_name=$1
echo "Installing font package: $font_name"
# Map font package names to actual font URLs and installation methods
case $font_name in
"font-dejavu")
local version=$(get_latest_release "dejavu-fonts/dejavu-fonts")
version=${version#version_} # Remove 'version_' prefix
local url="https://github.com/dejavu-fonts/dejavu-fonts/releases/download/version_${version}/dejavu-fonts-ttf-${version}.tar.bz2"
wget -q "$url" -P "$TEMP_DIR" && \
tar xjf "$TEMP_DIR/dejavu-fonts-ttf-${version}.tar.bz2" -C "$TEMP_DIR" && \
find "$TEMP_DIR" -name "*.ttf" -exec cp {} "$FONT_DIR/" \;
;;
"font-noto")
# Base Noto Sans and Serif
wget -q "https://noto-website-2.storage.googleapis.com/pkgs/NotoSans-hinted.zip" -P "$TEMP_DIR" && \
wget -q "https://noto-website-2.storage.googleapis.com/pkgs/NotoSerif-hinted.zip" -P "$TEMP_DIR" && \
unzip -q "$TEMP_DIR/NotoSans-hinted.zip" -d "$TEMP_DIR/noto-sans" && \
unzip -q "$TEMP_DIR/NotoSerif-hinted.zip" -d "$TEMP_DIR/noto-serif" && \
cp "$TEMP_DIR/noto-sans"/*.ttf "$FONT_DIR/" && \
cp "$TEMP_DIR/noto-serif"/*.ttf "$FONT_DIR/"
;;
"font-noto-cjk")
# Noto CJK
wget -q "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTF/Japanese/NotoSansCJKjp-Regular.otf" -P "$FONT_DIR"
wget -q "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTF/Korean/NotoSansCJKkr-Regular.otf" -P "$FONT_DIR"
wget -q "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf" -P "$FONT_DIR"
wget -q "https://github.com/notofonts/noto-cjk/raw/main/Sans/OTF/TraditionalChinese/NotoSansCJKtc-Regular.otf" -P "$FONT_DIR"
;;
"font-noto-arabic")
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoNaskhArabic/NotoNaskhArabic-Regular.ttf" -P "$FONT_DIR"
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoKufiArabic/NotoKufiArabic-Regular.ttf" -P "$FONT_DIR"
;;
"font-noto-devanagari")
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoSansDevanagari/NotoSansDevanagari-Regular.ttf" -P "$FONT_DIR"
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoSerifDevanagari/NotoSerifDevanagari-Regular.ttf" -P "$FONT_DIR"
;;
"font-noto-thai")
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoSansThai/NotoSansThai-Regular.ttf" -P "$FONT_DIR"
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoSerifThai/NotoSerifThai-Regular.ttf" -P "$FONT_DIR"
;;
"font-noto-hebrew")
wget -q "https://github.com/notofonts/noto-fonts/raw/main/hinted/ttf/NotoSansHebrew/NotoSansHebrew-Regular.ttf" -P "$FONT_DIR"
;;
"font-awesome")
local version=$(get_latest_release "FortAwesome/Font-Awesome")
wget -q "https://use.fontawesome.com/releases/v${version}/fontawesome-free-${version}-desktop.zip" -P "$TEMP_DIR" && \
unzip -q "$TEMP_DIR/fontawesome-free-${version}-desktop.zip" -d "$TEMP_DIR" && \
cp "$TEMP_DIR/fontawesome-free-${version}-desktop/otfs"/*.otf "$FONT_DIR/"
;;
"font-source-code-pro")
local version=$(get_latest_release "adobe-fonts/source-code-pro")
wget -q "https://github.com/adobe-fonts/source-code-pro/releases/download/${version}/TTF-source-code-pro-${version}.zip" -P "$TEMP_DIR" && \
unzip -q "$TEMP_DIR/TTF-source-code-pro-${version}.zip" -d "$TEMP_DIR/source-code-pro" && \
cp "$TEMP_DIR/source-code-pro"/*.ttf "$FONT_DIR/"
;;
"font-vollkorn")
wget -q "https://github.com/FAlthausen/Vollkorn-Typeface/raw/main/fonts/TTF/Vollkorn-Regular.ttf" -P "$FONT_DIR"
;;
"font-liberation")
wget -q "https://github.com/liberationfonts/liberation-fonts/files/7261482/liberation-fonts-ttf-2.1.5.tar.gz" -P "$TEMP_DIR" && \
tar xzf "$TEMP_DIR/liberation-fonts-ttf-2.1.5.tar.gz" -C "$TEMP_DIR" && \
cp "$TEMP_DIR/liberation-fonts-ttf-2.1.5"/*.ttf "$FONT_DIR/"
;;
esac
echo "Completed installation attempt for $font_name"
}
# Map languages to specific font packages
# Enhanced language-specific font mappings
declare -A language_fonts=(
["ar_AR"]="font-noto-arabic"
["zh_CN"]="font-isas-misc"
["zh_TW"]="font-isas-misc"
["ja_JP"]="font-noto font-noto-thai font-noto-tibetan font-ipa font-sony-misc font-jis-misc"
["ru_RU"]="font-vollkorn font-misc-cyrillic font-mutt-misc font-screen-cyrillic font-winitzki-cyrillic font-cronyx-cyrillic"
["sr_LATN_RS"]="font-vollkorn font-misc-cyrillic font-mutt-misc font-screen-cyrillic font-winitzki-cyrillic font-cronyx-cyrillic"
["uk_UA"]="font-vollkorn font-misc-cyrillic font-mutt-misc font-screen-cyrillic font-winitzki-cyrillic font-cronyx-cyrillic"
["ko_KR"]="font-noto font-noto-thai font-noto-tibetan"
["zh_CN"]="font-noto-cjk"
["zh_TW"]="font-noto-cjk"
["ja_JP"]="font-noto font-noto-cjk"
["ru_RU"]="font-noto font-liberation font-vollkorn"
["sr_LATN_RS"]="font-noto font-liberation"
["uk_UA"]="font-noto font-liberation"
["ko_KR"]="font-noto font-noto-cjk"
["el_GR"]="font-noto"
["hi_IN"]="font-noto-devanagari"
["bg_BG"]="font-vollkorn font-misc-cyrillic"
["GENERAL"]="font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra"
["bg_BG"]="font-noto font-liberation"
["th_TH"]="font-noto-thai"
["he_IL"]="font-noto-hebrew"
["GENERAL"]="font-noto font-dejavu font-liberation font-source-code-pro font-awesome"
)
# Install fonts for other languages which generally do not need special packages beyond 'font-noto'
other_langs=("en_GB" "en_US" "de_DE" "fr_FR" "es_ES" "ca_CA" "it_IT" "pt_BR" "nl_NL" "sv_SE" "pl_PL" "ro_RO" "hu_HU" "tr_TR" "id_ID" "eu_ES")
# Install fonts based on specified languages
if [[ $LANGS == "ALL" ]]; then
# Install all fonts from the language_fonts map
declare -A installed_fonts
for fonts in "${language_fonts[@]}"; do
for font in $fonts; do
install_font $font
if [[ -z "${installed_fonts[$font]}" ]]; then
install_font "$font"
installed_fonts[$font]=1
fi
done
done
else
# Split comma-separated languages and install necessary fonts
declare -A installed_fonts
IFS=',' read -ra LANG_CODES <<< "$LANGS"
for code in "${LANG_CODES[@]}"; do
if [[ " ${other_langs[@]} " =~ " ${code} " ]]; then
install_font font-noto
else
fonts_to_install=${language_fonts[$code]}
if [ ! -z "$fonts_to_install" ]; then
for font in $fonts_to_install; do
install_font $font
done
fi
fonts_to_install=${language_fonts[$code]}
if [ ! -z "$fonts_to_install" ]; then
for font in $fonts_to_install; do
if [[ -z "${installed_fonts[$font]}" ]]; then
install_font "$font"
installed_fonts[$font]=1
fi
done
fi
done
fi
# Cleanup
rm -rf "$TEMP_DIR"
# Update font cache
if command -v fc-cache >/dev/null; then
fc-cache -f "$FONT_DIR"
echo "Font cache updated"
else
echo "Warning: fc-cache not found. You may need to manually update your font cache"
fi
echo "Font installation completed. Fonts installed in: $FONT_DIR"

View File

@ -28,7 +28,7 @@ public class LicenseKeyChecker {
this.checkLicense();
}
@Scheduled(initialDelay = 604800000,fixedRate = 604800000) // 7 days in milliseconds
@Scheduled(initialDelay = 604800000, fixedRate = 604800000) // 7 days in milliseconds
public void checkLicensePeriodically() {
checkLicense();
}

View File

@ -595,7 +595,9 @@ public class GetInfoOnPDF {
permissionsNode.put("Document Assembly", getPermissionState(ap.canAssembleDocument()));
permissionsNode.put("Extracting Content", getPermissionState(ap.canExtractContent()));
permissionsNode.put("Extracting for accessibility", getPermissionState(ap.canExtractForAccessibility()));
permissionsNode.put(
"Extracting for accessibility",
getPermissionState(ap.canExtractForAccessibility()));
permissionsNode.put("Form Filling", getPermissionState(ap.canFillInForm()));
permissionsNode.put("Modifying", getPermissionState(ap.canModify()));
permissionsNode.put("Modifying annotations", getPermissionState(ap.canModifyAnnotations()));

View File

@ -3,11 +3,12 @@ package stirling.software.SPDF.controller.api.security;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
@ -41,7 +42,6 @@ import stirling.software.SPDF.service.CustomPDDocumentFactory;
@RequestMapping("/api/v1/security")
@Tag(name = "Security", description = "Security APIs")
public class ValidateSignatureController {
private final CustomPDDocumentFactory pdfDocumentFactory;
private final CertificateValidationService certValidationService;
@ -56,7 +56,7 @@ public class ValidateSignatureController {
@Operation(
summary = "Validate PDF Digital Signature",
description =
"Validates the digital signatures in a PDF file against default or custom certificates. Input:PDF Output:JSON Type:SISO")
"Validates digital signatures in a PDF file against default or custom certificates.")
@PostMapping(value = "/validate-signature")
public ResponseEntity<List<SignatureValidationResult>> validateSignature(
@ModelAttribute SignatureValidationRequest request) throws IOException {
@ -80,7 +80,6 @@ public class ValidateSignatureController {
for (PDSignature sig : signatures) {
SignatureValidationResult result = new SignatureValidationResult();
try {
byte[] signedContent = sig.getSignedContent(file.getInputStream());
byte[] signatureBytes = sig.getContents(file.getInputStream());
@ -92,77 +91,90 @@ public class ValidateSignatureController {
SignerInformationStore signerStore = signedData.getSignerInfos();
for (SignerInformation signer : signerStore.getSigners()) {
X509CertificateHolder certHolder = (X509CertificateHolder) certStore.getMatches(signer.getSID()).iterator().next();
X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certHolder);
X509CertificateHolder certHolder =
(X509CertificateHolder)
certStore.getMatches(signer.getSID()).iterator().next();
X509Certificate cert =
new JcaX509CertificateConverter().getCertificate(certHolder);
boolean isValid = signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(cert));
result.setValid(isValid);
// Basic signature validation
result.setValid(
signer.verify(
new JcaSimpleSignerInfoVerifierBuilder().build(cert)));
// Additional validations
result.setChainValid(customCert != null
? certValidationService.validateCertificateChainWithCustomCert(cert, customCert)
: certValidationService.validateCertificateChain(cert));
result.setTrustValid(customCert != null
? certValidationService.validateTrustWithCustomCert(cert, customCert)
: certValidationService.validateTrustStore(cert));
result.setNotRevoked(!certValidationService.isRevoked(cert));
result.setNotExpired(!cert.getNotAfter().before(new Date()));
// Set basic signature info
result.setSignerName(sig.getName());
result.setSignatureDate(sig.getSignDate().getTime().toString());
result.setReason(sig.getReason());
result.setLocation(sig.getLocation());
// Set new certificate details
result.setIssuerDN(cert.getIssuerX500Principal().getName());
result.setSubjectDN(cert.getSubjectX500Principal().getName());
result.setSerialNumber(cert.getSerialNumber().toString(16)); // Hex format
result.setValidFrom(cert.getNotBefore().toString());
result.setValidUntil(cert.getNotAfter().toString());
result.setSignatureAlgorithm(cert.getSigAlgName());
// Get key size (if possible)
try {
result.setKeySize(((RSAPublicKey) cert.getPublicKey()).getModulus().bitLength());
} catch (Exception e) {
// If not RSA or error, set to 0
result.setKeySize(0);
// Perform chain validation
CertificateValidationService.ValidationResult chainResult;
if (customCert != null) {
chainResult =
certValidationService.validateWithCustomCert(cert, customCert);
} else {
chainResult = certValidationService.validateCertificateChain(cert);
}
result.setVersion(String.valueOf(cert.getVersion()));
// Set key usage
List<String> keyUsages = new ArrayList<>();
boolean[] keyUsageFlags = cert.getKeyUsage();
if (keyUsageFlags != null) {
String[] keyUsageLabels = {
"Digital Signature", "Non-Repudiation", "Key Encipherment",
"Data Encipherment", "Key Agreement", "Certificate Signing",
"CRL Signing", "Encipher Only", "Decipher Only"
};
for (int i = 0; i < keyUsageFlags.length; i++) {
if (keyUsageFlags[i]) {
keyUsages.add(keyUsageLabels[i]);
}
result.setChainValid(chainResult.isValid());
result.setTrustValid(chainResult.isValid());
result.setNotExpired(!chainResult.isExpired());
// Check if signature was valid at the time of signing
if (sig.getSignDate() != null) {
try {
cert.checkValidity(sig.getSignDate().getTime());
result.setValidAtTimeOfSigning(true);
} catch (CertificateExpiredException
| CertificateNotYetValidException e) {
result.setValidAtTimeOfSigning(false);
}
}
result.setKeyUsages(keyUsages);
// Check if self-signed
result.setSelfSigned(cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal()));
// Set signature info
populateSignatureInfo(result, sig, cert);
}
} catch (Exception e) {
result.setValid(false);
result.setErrorMessage("Signature validation failed: " + e.getMessage());
}
results.add(result);
}
}
return ResponseEntity.ok(results);
}
private void populateSignatureInfo(
SignatureValidationResult result, PDSignature sig, X509Certificate cert) {
result.setSignerName(sig.getName());
result.setSignatureDate(sig.getSignDate().getTime().toString());
result.setReason(sig.getReason());
result.setLocation(sig.getLocation());
result.setIssuerDN(cert.getIssuerX500Principal().getName());
result.setSubjectDN(cert.getSubjectX500Principal().getName());
result.setSerialNumber(cert.getSerialNumber().toString(16));
result.setValidFrom(cert.getNotBefore().toString());
result.setValidUntil(cert.getNotAfter().toString());
result.setSignatureAlgorithm(cert.getSigAlgName());
try {
result.setKeySize(((RSAPublicKey) cert.getPublicKey()).getModulus().bitLength());
} catch (Exception e) {
result.setKeySize(0);
}
result.setVersion(String.valueOf(cert.getVersion()));
List<String> keyUsages = new ArrayList<>();
boolean[] keyUsageFlags = cert.getKeyUsage();
if (keyUsageFlags != null) {
String[] keyUsageLabels = {
"Digital Signature", "Non-Repudiation", "Key Encipherment",
"Data Encipherment", "Key Agreement", "Certificate Signing",
"CRL Signing", "Encipher Only", "Decipher Only"
};
for (int i = 0; i < keyUsageFlags.length; i++) {
if (keyUsageFlags[i]) {
keyUsages.add(keyUsageLabels[i]);
}
}
}
result.setKeyUsages(keyUsages);
result.setSelfSigned(cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal()));
}
}

View File

@ -16,16 +16,16 @@ public class SignatureValidationResult {
private boolean trustValid;
private boolean notExpired;
private boolean notRevoked;
private String issuerDN; // Certificate issuer's Distinguished Name
private String subjectDN; // Certificate subject's Distinguished Name
private String serialNumber; // Certificate serial number
private String validFrom; // Certificate validity start date
private String validUntil; // Certificate validity end date
private String signatureAlgorithm;// Algorithm used for signing
private int keySize; // Key size in bits
private String version; // Certificate version
private List<String> keyUsages; // List of key usage purposes
private boolean isSelfSigned; // Whether the certificate is self-signed
private boolean validAtTimeOfSigning;
private String issuerDN; // Certificate issuer's Distinguished Name
private String subjectDN; // Certificate subject's Distinguished Name
private String serialNumber; // Certificate serial number
private String validFrom; // Certificate validity start date
private String validUntil; // Certificate validity end date
private String signatureAlgorithm; // Algorithm used for signing
private int keySize; // Key size in bits
private String version; // Certificate version
private List<String> keyUsages; // List of key usage purposes
private boolean isSelfSigned; // Whether the certificate is self-signed
}

View File

@ -1,13 +1,11 @@
package stirling.software.SPDF.service;
import io.github.pixee.security.BoundedLineReader;
import java.io.BufferedReader;
import java.io.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.*;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
import java.security.cert.CertificateExpiredException;
@ -16,84 +14,126 @@ import java.security.cert.CertificateNotYetValidException;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.*;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@Service
@Slf4j
public class CertificateValidationService {
private KeyStore trustStore;
private static final String AATL_RESOURCE = "/tl12.acrobatsecuritysettings";
@PostConstruct
private void initializeTrustStore() throws Exception {
trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
loadMozillaCertificates();
loadAATLCertificatesFromPDF();
}
private void loadMozillaCertificates() throws Exception {
try (InputStream is = getClass().getResourceAsStream("/certdata.txt")) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line;
StringBuilder certData = new StringBuilder();
boolean inCert = false;
int certCount = 0;
private void loadAATLCertificatesFromPDF() throws Exception {
log.debug("Starting AATL certificate loading from PDF...");
while ((line = BoundedLineReader.readLine(reader, 5_000_000)) != null) {
if (line.startsWith("CKA_VALUE MULTILINE_OCTAL")) {
inCert = true;
certData = new StringBuilder();
continue;
}
if (inCert) {
if ("END".equals(line)) {
inCert = false;
byte[] certBytes = parseOctalData(certData.toString());
if (certBytes != null) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate cert =
(X509Certificate)
cf.generateCertificate(
new ByteArrayInputStream(certBytes));
trustStore.setCertificateEntry("mozilla-cert-" + certCount++, cert);
}
} else {
certData.append(line).append("\n");
}
try (InputStream pdfStream = new ClassPathResource(AATL_RESOURCE).getInputStream()) {
PDDocument document = Loader.loadPDF(pdfStream.readAllBytes());
PDEmbeddedFilesNameTreeNode embeddedFiles =
document.getDocumentCatalog().getNames().getEmbeddedFiles();
Map<String, PDComplexFileSpecification> files = embeddedFiles.getNames();
for (Map.Entry<String, PDComplexFileSpecification> entry : files.entrySet()) {
log.debug(entry.getKey());
if (entry.getKey().equals("SecuritySettings.xml")) {
byte[] xmlContent = entry.getValue().getEmbeddedFile().toByteArray();
processSecuritySettingsXML(xmlContent);
break;
}
}
}
}
private byte[] parseOctalData(String data) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
String[] tokens = data.split("\\\\");
for (String token : tokens) {
token = token.trim();
if (!token.isEmpty()) {
baos.write(Integer.parseInt(token, 8));
private void processSecuritySettingsXML(byte[] xmlContent) throws Exception {
// Simple XML parsing using String operations
String xmlString = new String(xmlContent, "UTF-8");
int certCount = 0;
int failedCerts = 0;
// Find all Certificate tags
String startTag = "<Certificate>";
String endTag = "</Certificate>";
int startIndex = 0;
while ((startIndex = xmlString.indexOf(startTag, startIndex)) != -1) {
int endIndex = xmlString.indexOf(endTag, startIndex);
if (endIndex == -1) break;
// Extract certificate data
String certData = xmlString.substring(startIndex + startTag.length(), endIndex).trim();
startIndex = endIndex + endTag.length();
try {
byte[] certBytes = Base64.getDecoder().decode(certData);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate cert =
(X509Certificate)
cf.generateCertificate(new ByteArrayInputStream(certBytes));
// Only store root certificates (self-signed)
if (cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal())) {
trustStore.setCertificateEntry("aatl-cert-" + certCount, cert);
log.trace(
"Successfully loaded AATL root certificate #"
+ certCount
+ "\n Subject: "
+ cert.getSubjectX500Principal().getName()
+ "\n Valid until: "
+ cert.getNotAfter());
certCount++;
}
} catch (Exception e) {
failedCerts++;
log.error("Failed to process AATL certificate: " + e.getMessage());
}
return baos.toByteArray();
} catch (Exception e) {
return null;
}
log.debug("AATL Certificate loading completed:");
log.debug(" Total root certificates successfully loaded: " + certCount);
log.debug(" Failed certificates: " + failedCerts);
}
public boolean validateCertificateChain(X509Certificate cert) {
@Data
public static class ValidationResult {
private boolean valid;
private boolean expired;
private boolean validAtSigningTime;
private String errorMessage;
}
public ValidationResult validateCertificateChain(X509Certificate signerCert) {
ValidationResult result = new ValidationResult();
try {
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
// Build the certificate chain
List<X509Certificate> certChain = buildCertificateChain(signerCert);
// Create certificate path
CertificateFactory cf = CertificateFactory.getInstance("X.509");
List<X509Certificate> certList = Arrays.asList(cert);
CertPath certPath = cf.generateCertPath(certList);
CertPath certPath = cf.generateCertPath(certChain);
// Set up trust anchors
Set<TrustAnchor> anchors = new HashSet<>();
Enumeration<String> aliases = trustStore.aliases();
while (aliases.hasMoreElements()) {
@ -103,31 +143,113 @@ public class CertificateValidationService {
}
}
// Set up validation parameters
PKIXParameters params = new PKIXParameters(anchors);
params.setRevocationEnabled(false);
// Validate the path
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
validator.validate(certPath, params);
return true;
result.setValid(true);
result.setExpired(isExpired(signerCert));
return result;
} catch (Exception e) {
return false;
result.setValid(false);
result.setErrorMessage(e.getMessage());
return result;
}
}
public boolean validateTrustStore(X509Certificate cert) {
public ValidationResult validateWithCustomCert(
X509Certificate signerCert, X509Certificate customCert) {
ValidationResult result = new ValidationResult();
try {
// Build the complete chain from signer cert
List<X509Certificate> certChain = buildCertificateChain(signerCert);
// Check if custom cert matches any cert in the chain
boolean matchFound = false;
for (X509Certificate chainCert : certChain) {
if (chainCert.equals(customCert)) {
matchFound = true;
break;
}
}
if (!matchFound) {
// Check if custom cert is a valid issuer for any cert in the chain
for (X509Certificate chainCert : certChain) {
try {
chainCert.verify(customCert.getPublicKey());
matchFound = true;
break;
} catch (Exception e) {
// Continue checking next cert
}
}
}
result.setValid(matchFound);
if (!matchFound) {
result.setErrorMessage(
"Custom certificate is not part of the chain and is not a valid issuer");
}
return result;
} catch (Exception e) {
result.setValid(false);
result.setErrorMessage(e.getMessage());
return result;
}
}
private List<X509Certificate> buildCertificateChain(X509Certificate signerCert)
throws CertificateException {
List<X509Certificate> chain = new ArrayList<>();
chain.add(signerCert);
X509Certificate current = signerCert;
while (!isSelfSigned(current)) {
X509Certificate issuer = findIssuer(current);
if (issuer == null) break;
chain.add(issuer);
current = issuer;
}
return chain;
}
private boolean isSelfSigned(X509Certificate cert) {
return cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal());
}
private X509Certificate findIssuer(X509Certificate cert) throws CertificateException {
try {
Enumeration<String> aliases = trustStore.aliases();
while (aliases.hasMoreElements()) {
Object trustCert = trustStore.getCertificate(aliases.nextElement());
if (trustCert instanceof X509Certificate && cert.equals(trustCert)) {
return true;
Certificate trustCert = trustStore.getCertificate(aliases.nextElement());
if (trustCert instanceof X509Certificate) {
X509Certificate x509TrustCert = (X509Certificate) trustCert;
if (cert.getIssuerX500Principal()
.equals(x509TrustCert.getSubjectX500Principal())) {
try {
cert.verify(x509TrustCert.getPublicKey());
return x509TrustCert;
} catch (Exception e) {
// Continue searching if verification fails
}
}
}
}
return false;
} catch (KeyStoreException e) {
return false;
throw new CertificateException("Error accessing trust store", e);
}
return null;
}
public boolean isRevoked(X509Certificate cert) {
private boolean isExpired(X509Certificate cert) {
try {
cert.checkValidity();
return false;
@ -135,23 +257,4 @@ public class CertificateValidationService {
return true;
}
}
public boolean validateCertificateChainWithCustomCert(
X509Certificate cert, X509Certificate customCert) {
try {
cert.verify(customCert.getPublicKey());
return true;
} catch (Exception e) {
return false;
}
}
public boolean validateTrustWithCustomCert(X509Certificate cert, X509Certificate customCert) {
try {
// Compare the issuer of the signature certificate with the custom certificate
return cert.getIssuerX500Principal().equals(customCert.getSubjectX500Principal());
} catch (Exception e) {
return false;
}
}
}

Binary file not shown.