Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions .eslintrc.js

This file was deleted.

23 changes: 23 additions & 0 deletions .github/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# configure github generated release notes
# https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuring-automatically-generated-release-notes
# uses labels that can be affected implicitly based on PR titles, see workflow/conventional-label.yaml

changelog:
exclude:
labels:
- ignore-for-release
authors:
- octocat
categories:
- title: Breaking Changes 🛠
labels:
- breaking
- title: New Features 🎉
labels:
- feature
- title: Fixes 🔧
labels:
- fix
- title: Other Changes
labels:
- "*"
2 changes: 1 addition & 1 deletion .github/workflows/commits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ jobs:
needs: build
runs-on: ubuntu-latest
steps:
- name: Send Keel notification to staging-koumoul.com
- name: Send Keel notifications to staging-koumoul.com
run: |
curl -s --fail --show-error -X POST https://keel.admin.staging-koumoul.com/v1/webhooks/native -u ${{ secrets.KEEL_STAGING_USER }}:${{ secrets.KEEL_STAGING_PASSWORD }} -d '{"name": "ghcr.io/${{ github.repository }}", "tag": "${{ github.ref_name }}"}'
12 changes: 12 additions & 0 deletions .github/workflows/conventional-label.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# automatic labelling of pull request based on conventional commit names in the PR titles
# see https://github.com/marketplace/actions/conventional-release-labels

on:
pull_request_target:
types: [ opened, edited ]
name: conventional-release-labels
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: bcoe/conventional-release-labels@v1
5 changes: 3 additions & 2 deletions .github/workflows/releases.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
- id: get_version
uses: battila7/get-version-action@v2

- name: Send Keel notification to koumoul.com
- name: Send Keel notifications to koumoul.com
run: |
curl -s --fail --show-error -X POST https://keel.admin.koumoul.com/v1/webhooks/native -u ${{ secrets.KEEL_PROD_USER }}:${{ secrets.KEEL_PROD_PASSWORD }} -d '{"name": "ghcr.io/${{ github.repository }}", "tag": "${{ steps.get_version.outputs.major }}"}'
curl -s --fail --show-error -X POST https://keel.admin.koumoul.com/v1/webhooks/native -u ${{ secrets.KEEL_PROD_USER }}:${{ secrets.KEEL_PROD_PASSWORD }} -d '{"name": "ghcr.io/${{ github.repository }}", "tag": "${{ steps.get_version.outputs.major }}"}'

38 changes: 16 additions & 22 deletions .github/workflows/reuse-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,6 @@ jobs:
- name: Checkout git repository
uses: actions/checkout@v4

- name: Prepare docker image tags
id: docker_meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

Expand All @@ -37,20 +25,26 @@ jobs:
echo '{"version": "${{github.ref_name}}", "repository": "${{github.server_url}}/${{github.repository}}", "commit": "${{github.sha}}", "date": "'$(date -Is)'"}' > BUILD.json
cat BUILD.json

- name: Build and push docker images
- name: Prepare docker tags for image
id: docker_meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}

- name: Build and push image
id: docker_build
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
push: true
tags: ${{ steps.docker_meta.outputs.tags }}
platforms: linux/amd64
labels: ${{ steps.docker_meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Stop test dependencies
run: docker compose stop

- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
cache-from: type=registry,ref=ghcr.io/${{ github.repository }}/buildcache
cache-to: type=registry,ref=ghcr.io/${{ github.repository }}/buildcache,mode=max
2 changes: 1 addition & 1 deletion .github/workflows/reuse-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:

- uses: actions/setup-node@v3
with:
node-version: 20
node-version: 22
cache: 'npm'

- name: Install dependencies
Expand Down
3 changes: 0 additions & 3 deletions .husky/commit-msg
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx --no-install commitlint --edit ""
3 changes: 0 additions & 3 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run lint
3 changes: 0 additions & 3 deletions .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run quality
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v21
24
31 changes: 18 additions & 13 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
# See https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md#running-puppeteer-in-docker for the base
# Also see https://github.com/puppeteer/puppeteer/blob/main/docker/Dockerfile

##########################
FROM node:24.12.0-trixie-slim AS base

WORKDIR /app
ENV NODE_ENV=production

############################################################################################################
# Stage: prepare a base image with all native utils pre-installed, used both by builder and definitive image

FROM node:21.6.0-bookworm-slim AS nativedeps
FROM base AS nativedeps

ARG TARGETARCH
RUN echo "Building for architecture $TARGETARCH"
Expand All @@ -15,7 +21,7 @@ RUN apt-get update && \
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/googlechrome-linux-keyring.gpg && \
sh -c 'echo "deb [arch=amd64 signed-by=/usr/share/keyrings/googlechrome-linux-keyring.gpg] https://dl-ssl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' && \
apt-get update && \
apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-khmeros fonts-kacst fonts-freefont-ttf libxss1 dbus dbus-x11 --no-install-recommends && \
apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-khmeros fonts-kacst-one fonts-freefont-ttf libxss1 dbus dbus-x11 --no-install-recommends && \
service dbus start

ENV DBUS_SESSION_BUS_ADDRESS autolaunch:
Expand All @@ -35,43 +41,42 @@ ENV PUPPETEER_SKIP_DOWNLOAD true
# Stage: nodejs dependencies and build
FROM nativedeps AS builder

WORKDIR /webapp
ADD package.json .
ADD package-lock.json .

# use clean-modules on the same line as npm ci to be lighter in the cache
RUN npm i -g clean-modules@2.0.6
RUN npm ci --omit=dev &&\
clean-modules --yes --exclude exceljs/lib/doc/ --exclude mocha/lib/test.js --exclude "**/*.mustache"
clean-modules --yes --exclude exceljs/lib/doc/ --exclude "**/*.mustache"

##################################
# Stage: main nodejs service stage
FROM nativedeps
MAINTAINER "contact@koumoul.com"
LABEL org.opencontainers.image.vendor="Koumoul"
LABEL org.opencontainers.image.authors="contact@koumoul.com"
LABEL org.opencontainers.image.licenses="AGPL-3.0-only"

# Add user so we don't need --no-sandbox.
RUN groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
&& mkdir -p /home/pptruser/Downloads \
&& chown -R pptruser:pptruser /home/pptruser \
&& mkdir -p /webapp \
&& chown -R pptruser:pptruser /webapp
&& mkdir -p /app \
&& chown -R pptruser:pptruser /app

# Run everything after as non-privileged user.
USER pptruser

# The nodejs service
ENV NODE_ENV production
WORKDIR /webapp
COPY --from=builder /webapp/node_modules /webapp/node_modules
ADD server server
COPY --from=builder /app/node_modules /app/node_modules
ADD api api
ADD config config
ADD contract contract
ADD package.json .
ADD README.md BUILD.json* ./
ADD LICENSE .
ADD test test
ADD test-it/resources test/resources

EXPOSE 8080

ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "--max-http-header-size", "64000", "server"]
CMD ["node", "--max-http-header-size", "64000", "--disable-warning", "ExperimentalWarning", "api/index.ts"]
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Then start the server:

Or build and run the docker image:

docker buildx build -t capture --progress plain --load . && docker run --rm -it --security-opt seccomp=$(pwd)/seccomp.json -p 5607:5607 -p 9090:9090 -e DEBUG=capture,timer -e ONLY_SAME_HOST=false -e PORT=5607 --name capture capture
docker buildx build -t capture --progress plain --load . && docker run --rm -it --security-opt seccomp=$(pwd)/seccomp.json -p 5607:5607 -p 9090:9090 -e DEBUG=capture,timer -e ONLY_SAME_HOST=false -e SECRET_CAPTURE=capture -e HELMET_ACTIVE=false -e PORT=5607 --name capture capture

Check the service with these examples:

Expand All @@ -33,9 +33,8 @@ Check the service with these examples:
- [animated gif screenshot](http://localhost:5607/api/v1/screenshot?key=capture&type=gif&target=http://localhost:5607/test/resources/test-anim.html)
- [animated gif screenshot with custom filename](http://localhost:5607/api/v1/screenshot?key=capture&type=gif&filename=test.gif&target=http://localhost:5607/test/resources/test-anim.html)
- [fallback to standard screenshot with custom filename](http://localhost:5607/api/v1/screenshot?key=capture&type=gif&filename=test.gif&target=http://localhost:5607/test/resources/test1.html)
- [screenshot converted to jpg](http://localhost:5607/api/v1/screenshot?key=capture&type=jpg&target=http://localhost:5607/test/resources/test-anim.html)
- [screenshot with custom jpg filename](http://localhost:5607/api/v1/screenshot?key=capture&filename=test.jpg&target=http://localhost:5607/test/resources/test-anim.html)
- [remote url with a webgl based map](http://localhost:5607/api/v1/print?key=capture&target=https://staging-koumoul.com/cadastre/parcel?id=56251000AV0142)
- [remote url with a webgl based map](http://localhost:5617/api/v1/print?key=capture&target=https://staging-koumoul.com/cadastre/parcel?id=56251000AV0142)

## Security

Consider reading this article https://github.com/Zenika/alpine-chrome#3-ways-to-securely-use-chrome-headless-with-this-image
Expand Down
40 changes: 40 additions & 0 deletions api/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { session, errorHandler, createSiteMiddleware } from '@data-fair/lib-express/index.js'
import express from 'express'
import helmet from 'helmet'
import cookieParser from 'cookie-parser'
import apiDocs from '../contract/api-docs.ts'
import { router } from './routers/capture.ts'
import config from '#config'

const app = express()
export default app

app.use(cookieParser())

if (config.helmet.active) {
app.use(helmet({
contentSecurityPolicy: {
useDefaults: false,
directives: {
// very restrictive by default, index.html of the UI will have custom rules defined in createSpaMiddleware
// https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html#security-headers
'frame-ancestors': ["'none'"],
'default-src': ["'none'"]
}
}
}))
}

// no fancy embedded arrays, just string and arrays of strings in req.query
app.set('query parser', 'simple')
app.use(express.json())

app.use(createSiteMiddleware('capture', { prefixOptional: true }))
if (config.privateDirectoryUrl) app.use(session.middleware())

app.use('/api/v1', router)
app.get('/api/v1/api-docs.json', (req, res, next) => res.send(apiDocs))
app.use('/test', express.static('./test-it'))
app.use('/api', (req, res) => res.status(404).send('unknown api endpoint'))

app.use(errorHandler)
14 changes: 14 additions & 0 deletions api/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ApiConfig } from '../config/type/index.ts'
import { assertValid } from '../config/type/index.ts'
import config from 'config'

// we reload the config instead of using the singleton from the config module for testing purposes
// @ts-ignore
const apiConfig = process.env.NODE_ENV === 'test' ? config.util.loadFileConfigs(process.env.NODE_CONFIG_DIR, { skipConfigSources: true }) : config
assertValid(apiConfig, { lang: 'en', name: 'config', internal: true })

export default apiConfig as ApiConfig

export type UiConfig = {}

export const uiConfig: UiConfig = {}
17 changes: 17 additions & 0 deletions api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { start, stop } from './server.ts'

start().then(() => {}, err => {
console.error('Failure while starting service', err)
process.exit(1)
})

process.on('SIGTERM', function onSigterm () {
console.info('Received SIGTERM signal, shutdown gracefully...')
stop().then(() => {
console.log('shutting down now')
process.exit()
}, err => {
console.error('Failure while stopping service', err)
process.exit(1)
})
})
Loading