Compare commits

..

2 commits

Author SHA1 Message Date
613a73c3ed
Merge 981fcd4e3f into 03b3c9eba8 2024-10-17 14:20:34 +00:00
Alexis Métaireau
981fcd4e3f
Add a --debug flag to the CLI to help retrieve more logs.
When the flag is set:

- The `RUNSC_DEBUG=1` environment variable is added to the outer container ;
- the stderr from the outer container is attached to the exception, and
  displayed to the user on failures.
2024-10-17 16:19:53 +02:00
37 changed files with 755 additions and 1381 deletions

View file

@ -1,10 +1,6 @@
name: Build dev environments name: Build dev environments
on: on:
pull_request:
push: push:
branches:
- main
- "test/**"
schedule: schedule:
- cron: "0 0 * * *" # Run every day at 00:00 UTC. - cron: "0 0 * * *" # Run every day at 00:00 UTC.
@ -37,6 +33,8 @@ jobs:
version: "20.04" version: "20.04"
- distro: ubuntu - distro: ubuntu
version: "22.04" version: "22.04"
- distro: ubuntu
version: "23.10"
- distro: ubuntu - distro: ubuntu
version: "24.04" version: "24.04"
- distro: ubuntu - distro: ubuntu
@ -47,6 +45,8 @@ jobs:
version: bookworm version: bookworm
- distro: debian - distro: debian
version: trixie version: trixie
- distro: fedora
version: "39"
- distro: fedora - distro: fedora
version: "40" version: "40"
- distro: fedora - distro: fedora

View file

@ -1,6 +1,6 @@
name: Check branch conformity name: Check branch conformity
on: on:
pull_request: push:
jobs: jobs:
prevent-fixup-commits: prevent-fixup-commits:

View file

@ -23,6 +23,8 @@ jobs:
version: "24.10" # oracular version: "24.10" # oracular
- distro: ubuntu - distro: ubuntu
version: "24.04" # noble version: "24.04" # noble
- distro: ubuntu
version: "23.10" # mantic
- distro: ubuntu - distro: ubuntu
version: "22.04" # jammy version: "22.04" # jammy
- distro: ubuntu - distro: ubuntu

View file

@ -1,10 +1,8 @@
name: Tests name: Tests
on: on:
pull_request:
push: push:
branches: pull_request:
- main branches: [main]
- "test/**"
schedule: schedule:
- cron: "2 0 * * *" # Run every day at 02:00 UTC. - cron: "2 0 * * *" # Run every day at 02:00 UTC.
workflow_dispatch: workflow_dispatch:
@ -80,7 +78,7 @@ jobs:
path: share/tessdata/ path: share/tessdata/
key: v1-tessdata-${{ hashFiles('./install/common/download-tessdata.py') }} key: v1-tessdata-${{ hashFiles('./install/common/download-tessdata.py') }}
enableCrossOsArchive: true enableCrossOsArchive: true
- uses: actions/setup-python@v5 - uses: actions/setup-python@v4
with: with:
python-version: '3.11' python-version: '3.11'
- name: Download Tessdata - name: Download Tessdata
@ -93,8 +91,7 @@ jobs:
windows: windows:
runs-on: windows-latest runs-on: windows-latest
needs: needs: download-tessdata
- download-tessdata
env: env:
DUMMY_CONVERSION: 1 DUMMY_CONVERSION: 1
steps: steps:
@ -124,8 +121,7 @@ jobs:
macOS: macOS:
name: "macOS (${{ matrix.arch }})" name: "macOS (${{ matrix.arch }})"
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.runner }}
needs: needs: download-tessdata
- download-tessdata
strategy: strategy:
matrix: matrix:
include: include:
@ -153,10 +149,9 @@ jobs:
run: poetry run make test run: poetry run make test
build-deb: build-deb:
needs:
- build-container-image
name: "build-deb (${{ matrix.distro }} ${{ matrix.version }})" name: "build-deb (${{ matrix.distro }} ${{ matrix.version }})"
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build-container-image
strategy: strategy:
matrix: matrix:
include: include:
@ -164,6 +159,8 @@ jobs:
version: "20.04" version: "20.04"
- distro: ubuntu - distro: ubuntu
version: "22.04" version: "22.04"
- distro: ubuntu
version: "23.10"
- distro: ubuntu - distro: ubuntu
version: "24.04" version: "24.04"
- distro: ubuntu - distro: ubuntu
@ -224,8 +221,7 @@ jobs:
install-deb: install-deb:
name: "install-deb (${{ matrix.distro }} ${{ matrix.version }})" name: "install-deb (${{ matrix.distro }} ${{ matrix.version }})"
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs: build-deb
- build-deb
strategy: strategy:
matrix: matrix:
include: include:
@ -233,6 +229,8 @@ jobs:
version: "20.04" version: "20.04"
- distro: ubuntu - distro: ubuntu
version: "22.04" version: "22.04"
- distro: ubuntu
version: "23.10"
- distro: ubuntu - distro: ubuntu
version: "24.04" version: "24.04"
- distro: ubuntu - distro: ubuntu
@ -279,12 +277,11 @@ jobs:
build-install-rpm: build-install-rpm:
name: "build-install-rpm (${{ matrix.distro }} ${{matrix.version}})" name: "build-install-rpm (${{ matrix.distro }} ${{matrix.version}})"
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs: build-container-image
- build-container-image
strategy: strategy:
matrix: matrix:
distro: ["fedora"] distro: ["fedora"]
version: ["40", "41"] version: ["39", "40", "41"]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -328,7 +325,7 @@ jobs:
run: | run: |
./dev_scripts/env.py --distro ${{ matrix.distro }} \ ./dev_scripts/env.py --distro ${{ matrix.distro }} \
--version ${{ matrix.version }} \ --version ${{ matrix.version }} \
build build --download-pyside6
- name: Run a test command - name: Run a test command
run: | run: |
@ -353,6 +350,8 @@ jobs:
version: "20.04" version: "20.04"
- distro: ubuntu - distro: ubuntu
version: "22.04" version: "22.04"
- distro: ubuntu
version: "23.10"
- distro: ubuntu - distro: ubuntu
version: "24.04" version: "24.04"
- distro: ubuntu - distro: ubuntu
@ -363,6 +362,8 @@ jobs:
version: bookworm version: bookworm
- distro: debian - distro: debian
version: trixie version: trixie
- distro: fedora
version: "39"
- distro: fedora - distro: fedora
version: "40" version: "40"
- distro: fedora - distro: fedora

View file

@ -1,22 +0,0 @@
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v9
with:
days-before-issue-stale: 30
days-before-issue-close: 14
stale-issue-label: "stale"
stale-issue-message: "Marking this issue as stale because it has been open for 30 days with no activity. It will be closed in 14 days if there's no activity, or if the `stale` label is not removed. Does anyone want to add something?"
close-issue-message: "Closing this issue now. Don't hesitate to reopen if you have anything to add :-)"
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}
any-of-labels: needs info

View file

@ -1,9 +1,8 @@
name: Scan latest app and container name: Scan latest app and container
on: on:
push: push:
branches:
- main
pull_request: pull_request:
branches: [ main ]
schedule: schedule:
- cron: '0 0 * * *' # Run every day at 00:00 UTC. - cron: '0 0 * * *' # Run every day at 00:00 UTC.
workflow_dispatch: workflow_dispatch:

View file

@ -6,24 +6,16 @@ on:
jobs: jobs:
security-scan-container: security-scan-container:
strategy: runs-on: ubuntu-latest
matrix:
include:
- runs-on: ubuntu-latest
arch: i686
# Do not scan Silicon mac for now to avoid masking release scan results for other plaforms.
# - runs-on: macos-latest
# arch: arm64
runs-on: ${{ matrix.runs-on }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Download container image for the latest release and load it - name: Download container image for the latest release
run: | run: |
VERSION=$(curl https://api.github.com/repos/freedomofpress/dangerzone/releases/latest | grep "tag_name" | cut -d '"' -f 4) VERSION=$(curl https://api.github.com/repos/freedomofpress/dangerzone/releases/latest | jq -r '.tag_name')
CONTAINER_FILENAME=container-${VERSION:1}-${{ matrix.arch }}.tar.gz wget https://github.com/freedomofpress/dangerzone/releases/download/${VERSION}/container.tar.gz
wget https://github.com/freedomofpress/dangerzone/releases/download/${VERSION}/${CONTAINER_FILENAME} -O ${CONTAINER_FILENAME} - name: Load container image
docker load -i ${CONTAINER_FILENAME} run: docker load -i container.tar.gz
# NOTE: Scan first without failing, else we won't be able to read the scan # NOTE: Scan first without failing, else we won't be able to read the scan
# report. # report.
- name: Scan container image (no fail) - name: Scan container image (no fail)
@ -38,7 +30,7 @@ jobs:
uses: github/codeql-action/upload-sarif@v3 uses: github/codeql-action/upload-sarif@v3
with: with:
sarif_file: ${{ steps.scan_container.outputs.sarif }} sarif_file: ${{ steps.scan_container.outputs.sarif }}
category: container-${{ matrix.arch }} category: container
- name: Inspect container scan report - name: Inspect container scan report
run: cat ${{ steps.scan_container.outputs.sarif }} run: cat ${{ steps.scan_container.outputs.sarif }}
- name: Scan container image - name: Scan container image

View file

@ -260,17 +260,11 @@ The following instructions require typing commands in a terminal in dom0.
``` ```
qvm-create --class AppVM --label red --template fedora-40-dz dz qvm-create --class AppVM --label red --template fedora-40-dz dz
qvm-volume resize dz:private $(numfmt --from=auto 20Gi)
``` ```
> :bulb: Alternatively, you can use a different app qube for Dangerzone > :bulb: Alternatively, you can use a different app qube for Dangerzone
> development. In that case, replace `dz` with the qube of your choice in the > development. In that case, replace `dz` with the qube of your choice in the
> steps below. > steps below.
>
> In the commands above, we also resize the private volume of the `dz` qube
> to 20GiB, since you may need some extra storage space when developing on
> Dangerzone (e.g., for container images, Tesseract data, and Python
> virtualenvs).
4. Add an RPC policy (`/etc/qubes/policy.d/50-dangerzone.policy`) that will 4. Add an RPC policy (`/etc/qubes/policy.d/50-dangerzone.policy`) that will
allow launching a disposable qube (`dz-dvm`) when Dangerzone converts a allow launching a disposable qube (`dz-dvm`) when Dangerzone converts a
@ -314,9 +308,18 @@ test it.
1. Install the `.rpm` package you just copied 1. Install the `.rpm` package you just copied
```sh ```sh
sudo dnf install 'dnf-command(config-manager)'
sudo dnf-3 config-manager --add-repo=https://packages.freedom.press/yum-tools-prod/dangerzone/dangerzone.repo
sudo dnf install ~/QubesIncoming/dz/*.rpm sudo dnf install ~/QubesIncoming/dz/*.rpm
``` ```
In the above steps, we add the Dangerzone repo because it includes the
necessary PySide6 RPM in order to make Dangerzone work.
> [!NOTE]
> During the installation, you will be asked to
> [verify the Dangerzone GPG key](INSTALL.md#verifying-dangerzone-gpg-key).
2. Shutdown the `fedora-40-dz` template 2. Shutdown the `fedora-40-dz` template
### Developing Dangerzone ### Developing Dangerzone

View file

@ -5,60 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
since 0.4.1, and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). since 0.4.1, and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased](https://github.com/freedomofpress/dangerzone/compare/v0.8.0...HEAD) ## [Unreleased](https://github.com/freedomofpress/dangerzone/compare/v0.7.1...HEAD)
### Added
- Disable gVisor's DirectFS feature ([#226](https://github.com/freedomofpress/dangerzone/issues/226)).
Thanks [EtiennePerot](https://github.com/EtiennePerot) for the contribution.
### Removed
- Platform support: Drop support for Fedora 39, since it's end-of-life ([#999](https://github.com/freedomofpress/dangerzone/pull/999))
## [0.8.0](https://github.com/freedomofpress/dangerzone/compare/v0.8.0...0.7.1)
### Added ### Added
- Point to the installation instructions that the Tails team maintains for Dangerzone ([announcement](https://tails.net/news/dangerzone/index.en.html)) - Point to the installation instructions that the Tails team maintains for Dangerzone ([announcement](https://tails.net/news/dangerzone/index.en.html))
- Installation and execution errors are now caught and displayed in the interface ([#193](https://github.com/freedomofpress/dangerzone/issues/193)) - Platform support: Ubuntu 24.10 and Fedora 41 ([issue #947](https://github.com/freedomofpress/dangerzone/issues/947))
- Prevent users from using illegal characters in output filename ([#362](https://github.com/freedomofpress/dangerzone/issues/362)). Thanks [@bnewc](https://github.com/bnewc) for the contribution!
- Add support for Fedora 41 ([#947](https://github.com/freedomofpress/dangerzone/issues/947))
- Add support for Ubuntu Oracular (24.10) ([#954](https://github.com/freedomofpress/dangerzone/pull/954))
### Fixed
- Update our macOS entitlements, removing now unneeded privileges ([#638](https://github.com/freedomofpress/dangerzone/issues/638))
- Make Dangerzone work on Linux systems with SELinux in enforcing mode ([#880](https://github.com/freedomofpress/dangerzone/issues/880))
- Process documents with embedded multimedia files without crashing ([#877](https://github.com/freedomofpress/dangerzone/issues/877))
- Search for applications that can read PDF files in a more reliable way on Linux ([#899](https://github.com/freedomofpress/dangerzone/issues/899))
- Handle and report some stray conversion errors ([#776](https://github.com/freedomofpress/dangerzone/issues/776)). Thanks [@amnak613](https://github.com/amnak613) for the contribution!
- Replace occurrences of the word "Docker" in Podman-related error messages in Linux ([#212](https://github.com/freedomofpress/dangerzone/issues/212))
### Changed
- The second phase of the conversion (pixels to PDF) now happens on the host. Instead of first grabbing all of the pixel data from the first container, storing them on disk, and then reconstructing the PDF on a second container, Dangerzone now immediately reconstructs the PDF **on the host**, while the doc to pixels conversion is still running on the first container. The sanitation is no less safe, since the boundaries between the sandbox and the host are still respected ([#625](https://github.com/freedomofpress/dangerzone/issues/625))
- PyMuPDF is now vendorized for Debian packages. This is done because the PyMuPDF package from the Debian repos lacks OCR support ([#940](https://github.com/freedomofpress/dangerzone/pull/940))
- Always use our own seccomp policy as a default ([#908](https://github.com/freedomofpress/dangerzone/issues/908))
- Debian packages are now amd64 only, which removes some warnings in Linux distros with 32-bit repos enabled ([#394](https://github.com/freedomofpress/dangerzone/issues/394))
- Allow choosing installation directory on Windows platforms ([#148](https://github.com/freedomofpress/dangerzone/issues/148)). Thanks [@jkarasti](https://github.com/jkarasti) for the contribution!
- Bumped H2ORestart LibreOffice extension to version 0.6.6 ([#943](https://github.com/freedomofpress/dangerzone/issues/943))
- Platform support: Ubuntu Focal (20.04) is now deprecated, and support will be dropped with the next release ([#965](https://github.com/freedomofpress/dangerzone/issues/965))
### Removed
- Platform support: Drop Ubuntu Mantic (23.10), since it's end-of-life ([#977](https://github.com/freedomofpress/dangerzone/pull/977))
### Development changes
- Build Debian packages with pybuild ([#773](https://github.com/freedomofpress/dangerzone/issues/773))
- Test Dangerzone on Intel macOS machines as well ([#932](https://github.com/freedomofpress/dangerzone/issues/932))
- Switch from CircleCI runners to Github actions ([#674](https://github.com/freedomofpress/dangerzone/issues/674))
- Sign Windows executables and installer with SHA256 rather than SHA1 ([#931](https://github.com/freedomofpress/dangerzone/pull/931)). Thanks [@jkarasti](https://github.com/jkarasti) for the contribution!
## [0.7.1](https://github.com/freedomofpress/dangerzone/compare/v0.7.1...v0.7.0) ## [0.7.1](https://github.com/freedomofpress/dangerzone/compare/v0.7.1...v0.7.0)
### Fixed ### Fixed
- Fix an `image-id.txt` mismatch happening on Docker Desktop >= 4.30.0 ([#933](https://github.com/freedomofpress/dangerzone/issues/933)) - Fix an `image-id.txt` mismatch happening on Docker Desktop >= 4.30.0 ([#933](https://github.com/freedomofpress/dangerzone/issues/933))

View file

@ -74,7 +74,9 @@ FROM alpine:latest
RUN apk --no-cache -U upgrade && \ RUN apk --no-cache -U upgrade && \
apk --no-cache add python3 apk --no-cache add python3
RUN GVISOR_URL="https://storage.googleapis.com/gvisor/releases/release/latest/$(uname -m)"; \ # Temporarily pin gVisor to the latest working version (release-20240826.0).
# See: https://github.com/freedomofpress/dangerzone/issues/928
RUN GVISOR_URL="https://storage.googleapis.com/gvisor/releases/release/20240826/$(uname -m)"; \
wget "${GVISOR_URL}/runsc" "${GVISOR_URL}/runsc.sha512" && \ wget "${GVISOR_URL}/runsc" "${GVISOR_URL}/runsc.sha512" && \
sha512sum -c runsc.sha512 && \ sha512sum -c runsc.sha512 && \
rm -f runsc.sha512 && \ rm -f runsc.sha512 && \

View file

@ -1,21 +1,8 @@
## MacOS ## MacOS
See instructions in [README.md](README.md#macos).
- Download [Dangerzone 0.8.0 for Mac (Apple Silicon CPU)](https://github.com/freedomofpress/dangerzone/releases/download/v0.8.0/Dangerzone-0.8.0-arm64.dmg)
- Download [Dangerzone 0.8.0 for Mac (Intel CPU)](https://github.com/freedomofpress/dangerzone/releases/download/v0.8.0/Dangerzone-0.8.0-i686.dmg)
You can also install Dangerzone for Mac using [Homebrew](https://brew.sh/): `brew install --cask dangerzone`
> **Note**: you will also need to install [Docker Desktop](https://www.docker.com/products/docker-desktop/).
> This program needs to run alongside Dangerzone at all times, since it is what allows Dangerzone to
> create the secure environment.
## Windows ## Windows
See instructions in [README.md](README.md#windows).
- Download [Dangerzone 0.8.0 for Windows](https://github.com/freedomofpress/dangerzone/releases/download/v0.8.0/Dangerzone-0.8.0.msi)
> **Note**: you will also need to install [Docker Desktop](https://www.docker.com/products/docker-desktop/).
> This program needs to run alongside Dangerzone at all times, since it is what allows Dangerzone to
> create the secure environment.
## Linux ## Linux
On Linux, Dangerzone uses [Podman](https://podman.io/) instead of Docker Desktop for creating On Linux, Dangerzone uses [Podman](https://podman.io/) instead of Docker Desktop for creating
@ -24,6 +11,7 @@ an isolated environment. It will be installed automatically when installing Dang
Dangerzone is available for: Dangerzone is available for:
- Ubuntu 24.10 (oracular) - Ubuntu 24.10 (oracular)
- Ubuntu 24.04 (noble) - Ubuntu 24.04 (noble)
- Ubuntu 23.10 (mantic)
- Ubuntu 22.04 (jammy) - Ubuntu 22.04 (jammy)
- Ubuntu 20.04 (focal) - Ubuntu 20.04 (focal)
- Debian 13 (trixie) - Debian 13 (trixie)
@ -31,6 +19,7 @@ Dangerzone is available for:
- Debian 11 (bullseye) - Debian 11 (bullseye)
- Fedora 41 - Fedora 41
- Fedora 40 - Fedora 40
- Fedora 39
- Tails - Tails
- Qubes OS (beta support) - Qubes OS (beta support)
@ -137,6 +126,23 @@ sudo apt install -y dangerzone
### Fedora ### Fedora
<table>
<tr>
<td>
<details>
<summary><i>:information_source: Backport notice for Fedora users regarding the <code>python3-pyside6</code> package</i></summary>
</br>
Fedora 39+ onwards does not provide official Python bindings for Qt. For
this reason, we provide our own `python3-pyside6` package (see
[build instructions](https://github.com/freedomofpress/maint-dangerzone-pyside6))
from our YUM repo. For a deeper dive on this subject, you may read
[this issue](https://github.com/freedomofpress/dangerzone/issues/211#issuecomment-1827777122).
</details>
</td>
</tr>
</table>
Type the following commands in a terminal: Type the following commands in a terminal:
``` ```
@ -284,7 +290,7 @@ Our [GitHub Releases page](https://github.com/freedomofpress/dangerzone/releases
hosts the following files: hosts the following files:
* Windows installer (`Dangerzone-<version>.msi`) * Windows installer (`Dangerzone-<version>.msi`)
* macOS archives (`Dangerzone-<version>-<arch>.dmg`) * macOS archives (`Dangerzone-<version>-<arch>.dmg`)
* Container images (`container-<version>-<arch>.tar.gz`) * Container image (`container.tar.gz`)
* Source package (`dangerzone-<version>.tar.gz`) * Source package (`dangerzone-<version>.tar.gz`)
All these files are accompanied by signatures (as `.asc` files). We'll explain All these files are accompanied by signatures (as `.asc` files). We'll explain
@ -309,10 +315,10 @@ gpg --verify Dangerzone-0.6.1-arm64.dmg.asc Dangerzone-0.6.1-arm64.dmg
gpg --verify Dangerzone-0.6.1-i686.dmg.asc Dangerzone-0.6.1-i686.dmg gpg --verify Dangerzone-0.6.1-i686.dmg.asc Dangerzone-0.6.1-i686.dmg
``` ```
For the container images: For the container image:
``` ```
gpg --verify container-0.6.1-i686.tar.gz.asc container-0.6.1-i686.tar.gz gpg --verify container.tar.gz.asc container.tar.gz
``` ```
For the source package: For the source package:

View file

@ -6,21 +6,33 @@ Take potentially dangerous PDFs, office documents, or images and convert them to
| ![Settings](./assets/screenshot1.png) | ![Converting](./assets/screenshot2.png) | ![Settings](./assets/screenshot1.png) | ![Converting](./assets/screenshot2.png)
|--|--| |--|--|
Dangerzone works like this: You give it a document that you don't know if you can trust (for example, an email attachment). Inside of a sandbox, Dangerzone converts the document to a PDF (if it isn't already one), and then converts the PDF into raw pixel data: a huge list of RGB color values for each page. Then, outside of the sandbox, Dangerzone takes this pixel data and converts it back into a PDF. Dangerzone works like this: You give it a document that you don't know if you can trust (for example, an email attachment). Inside of a sandbox, Dangerzone converts the document to a PDF (if it isn't already one), and then converts the PDF into raw pixel data: a huge list of RGB color values for each page. Then, in a separate sandbox, Dangerzone takes this pixel data and converts it back into a PDF.
_Read more about Dangerzone in the [official site](https://dangerzone.rocks/about/)._ _Read more about Dangerzone in the [official site](https://dangerzone.rocks/about/)._
## Getting started ## Getting started
Follow the instructions for each platform: ### MacOS
- Download [Dangerzone 0.7.1 for Mac (Apple Silicon CPU)](https://github.com/freedomofpress/dangerzone/releases/download/v0.7.1/Dangerzone-0.7.1-arm64.dmg)
- Download [Dangerzone 0.7.1 for Mac (Intel CPU)](https://github.com/freedomofpress/dangerzone/releases/download/v0.7.1/Dangerzone-0.7.1-i686.dmg)
* [macOS](https://github.com/freedomofpress/dangerzone/blob/v0.8.0//INSTALL.md#macos) You can also install Dangerzone for Mac using [Homebrew](https://brew.sh/): `brew install --cask dangerzone`
* [Windows](https://github.com/freedomofpress/dangerzone/blob/v0.8.0//INSTALL.md#windows)
* [Ubuntu Linux](https://github.com/freedomofpress/dangerzone/blob/v0.8.0/INSTALL.md#ubuntu-debian) > **Note**: you will also need to install [Docker Desktop](https://www.docker.com/products/docker-desktop/).
* [Debian Linux](https://github.com/freedomofpress/dangerzone/blob/v0.8.0/INSTALL.md#ubuntu-debian) > This program needs to run alongside Dangerzone at all times, since it is what allows Dangerzone to
* [Fedora Linux](https://github.com/freedomofpress/dangerzone/blob/v0.8.0/INSTALL.md#fedora) > create the secure environment.
* [Qubes OS (beta)](https://github.com/freedomofpress/dangerzone/blob/v0.8.0/INSTALL.md#qubes-os)
* [Tails](https://github.com/freedomofpress/dangerzone/blob/v0.8.0/INSTALL.md#tails) ### Windows
- Download [Dangerzone 0.7.1 for Windows](https://github.com/freedomofpress/dangerzone/releases/download/v0.7.1/Dangerzone-0.7.1.msi)
> **Note**: you will also need to install [Docker Desktop](https://www.docker.com/products/docker-desktop/).
> This program needs to run alongside Dangerzone at all times, since it is what allows Dangerzone to
> create the secure environment.
### Linux
See [installing Dangerzone](INSTALL.md#linux) for adding the Linux repositories to your system.
## Some features ## Some features

View file

@ -9,6 +9,7 @@ Before making a release, all of these should be complete:
- [ ] Copy the checkboxes from these instructions onto a new issue and call it **QA and Release version \<VERSION\>** - [ ] Copy the checkboxes from these instructions onto a new issue and call it **QA and Release version \<VERSION\>**
- [ ] [Add new Linux platforms and remove obsolete ones](https://github.com/freedomofpress/dangerzone/blob/main/RELEASE.md#add-new-platforms-and-remove-obsolete-ones) - [ ] [Add new Linux platforms and remove obsolete ones](https://github.com/freedomofpress/dangerzone/blob/main/RELEASE.md#add-new-platforms-and-remove-obsolete-ones)
- [ ] Bump the Python dependencies using `poetry lock` - [ ] Bump the Python dependencies using `poetry lock`
- [ ] [Check for official PySide6 versions](https://github.com/freedomofpress/dangerzone/blob/main/RELEASE.md#check-for-official-pyside6-versions)
- [ ] Update `version` in `pyproject.toml` - [ ] Update `version` in `pyproject.toml`
- [ ] Update `share/version.txt` - [ ] Update `share/version.txt`
- [ ] Update the "Version" field in `install/linux/dangerzone.spec` - [ ] Update the "Version" field in `install/linux/dangerzone.spec`
@ -43,6 +44,16 @@ In case of an EOL version:
* Consult the previous paragraph, but also `grep` your way around. * Consult the previous paragraph, but also `grep` your way around.
2. Add a notice in our `CHANGELOG.md` about the version removal. 2. Add a notice in our `CHANGELOG.md` about the version removal.
## Check for official PySide6 versions
PySide6 6.7.0 is available from the Fedora Rawhide repo, and we expect that a
similar version will be pushed soon to the rest of the stable releases. Prior to
a release, we should check if this has happened already. Once this happens, we
should update our CI tests accordingly, and remove this notice.
For more info, read:
https://github.com/freedomofpress/maint-dangerzone-pyside6/issues/5
## Large Document Testing ## Large Document Testing
Parallel to the QA process, the release candidate should be put through the large document tests in a dedicated machine to run overnight. Parallel to the QA process, the release candidate should be put through the large document tests in a dedicated machine to run overnight.
@ -274,11 +285,6 @@ Once we are confident that the release will be out shortly, and doesn't need any
* You can verify the correct Python version is used with `poetry debug info` * You can verify the correct Python version is used with `poetry debug info`
- [ ] Verify and checkout the git tag for this release - [ ] Verify and checkout the git tag for this release
- [ ] Run `poetry install --sync` - [ ] Run `poetry install --sync`
- [ ] On the silicon mac, build the container image:
```
python3 ./install/common/build-image.py
```
Then copy the `share/container.tar.gz` to the assets folder on `dangerzone-$VERSION-arm64.tar.gz`, along with the `share/image-id.txt` file.
- [ ] Run `poetry run ./install/macos/build-app.py`; this will make `dist/Dangerzone.app` - [ ] Run `poetry run ./install/macos/build-app.py`; this will make `dist/Dangerzone.app`
- [ ] Make sure that the build application works with the containerd graph - [ ] Make sure that the build application works with the containerd graph
driver (see [#933](https://github.com/freedomofpress/dangerzone/issues/933)) driver (see [#933](https://github.com/freedomofpress/dangerzone/issues/933))
@ -397,8 +403,6 @@ Build the latest container:
python3 ./install/common/build-image.py python3 ./install/common/build-image.py
``` ```
Copy the container image to the assets folder on `dangerzone-$VERSION-i686.tar.gz`.
Create a .rpm: Create a .rpm:
```sh ```sh
@ -445,9 +449,9 @@ To publish the release:
* Copy the release notes text from the template at [`docs/templates/release-notes`](https://github.com/freedomofpress/dangerzone/tree/main/docs/templates/) * Copy the release notes text from the template at [`docs/templates/release-notes`](https://github.com/freedomofpress/dangerzone/tree/main/docs/templates/)
* You can use `./dev_scripts/upload-asset.py`, if you want to upload an asset * You can use `./dev_scripts/upload-asset.py`, if you want to upload an asset
using an access token. using an access token.
- [ ] Upload the `container-$VERSION-i686.tar.gz` and `container-$VERSION-arm64.tar.gz` images that were created in the previous step - [ ] Upload the `container.tar.gz` i686 image that was created in the previous step
**Important:** Make sure that it's the same container images as the ones that **Important:** Make sure that it's the same container image as the ones that
are shipped in other platforms (see our [Pre-release](#Pre-release) section) are shipped in other platforms (see our [Pre-release](#Pre-release) section)
- [ ] Upload the detached signatures (.asc) and checksum file. - [ ] Upload the detached signatures (.asc) and checksum file.

View file

@ -46,7 +46,7 @@ def print_header(s: str) -> None:
"--debug", "--debug",
"debug", "debug",
flag_value=True, flag_value=True,
help="Run Dangerzone in debug mode, to get logs from gVisor.") help="Run Dangerzone in debug mode, and get more logs from the conversion process.")
@click.version_option(version=get_version(), message="%(version)s") @click.version_option(version=get_version(), message="%(version)s")
@errors.handle_document_errors @errors.handle_document_errors
def cli_main( def cli_main(

View file

@ -18,11 +18,20 @@ class ConversionException(Exception):
error_message = "Unspecified error" error_message = "Unspecified error"
error_code = ERROR_SHIFT error_code = ERROR_SHIFT
def __init__(self, error_message: Optional[str] = None) -> None: def __init__(
self, error_message: Optional[str] = None, logs: Optional[str] = None
) -> None:
if error_message: if error_message:
self.error_message = error_message self.error_message = error_message
self.logs = logs
super().__init__(self.error_message) super().__init__(self.error_message)
def __str__(self):
msg = f"{self.error_message}"
if self.logs:
msg += f" {self.logs}"
return msg
@classmethod @classmethod
def get_subclasses(cls) -> List[Type["ConversionException"]]: def get_subclasses(cls) -> List[Type["ConversionException"]]:
subclasses = [cls] subclasses = [cls]
@ -100,9 +109,10 @@ class UnexpectedConversionError(ConversionException):
def exception_from_error_code( def exception_from_error_code(
error_code: int, error_code: int,
logs: Optional[str] = None,
) -> Union[ConversionException, ValueError]: ) -> Union[ConversionException, ValueError]:
"""returns the conversion exception corresponding to the error code""" """returns the conversion exception corresponding to the error code"""
for cls in ConversionException.get_subclasses(): for cls in ConversionException.get_subclasses():
if cls.error_code == error_code: if cls.error_code == error_code:
return cls() return cls(logs=logs)
return UnexpectedConversionError(f"Unknown error code '{error_code}'") return UnexpectedConversionError(f"Unknown error code '{error_code}', logs= {logs}")

View file

@ -1,10 +1,10 @@
import logging import logging
import os import os
import platform import platform
import subprocess
import tempfile import tempfile
import typing import typing
from multiprocessing.pool import ThreadPool from multiprocessing.pool import ThreadPool
from pathlib import Path
from typing import List, Optional from typing import List, Optional
# FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details. # FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details.
@ -20,19 +20,15 @@ else:
from PySide6.QtWidgets import QTextEdit from PySide6.QtWidgets import QTextEdit
except ImportError: except ImportError:
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets from PySide2 import QtCore, QtGui, QtSvg, QtWidgets
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QAction, QTextEdit from PySide2.QtWidgets import QAction, QTextEdit
from PySide2.QtCore import Qt
from .. import errors from .. import errors
from ..document import SAFE_EXTENSION, Document from ..document import SAFE_EXTENSION, Document
from ..isolation_provider.container import ( from ..isolation_provider.container import Container, NoContainerTechException
Container,
NoContainerTechException,
NotAvailableContainerTechException,
)
from ..isolation_provider.dummy import Dummy from ..isolation_provider.dummy import Dummy
from ..isolation_provider.qubes import Qubes, is_qubes_native_conversion from ..isolation_provider.qubes import Qubes, is_qubes_native_conversion
from ..util import format_exception, get_resource_path, get_version from ..util import get_resource_path, get_subprocess_startupinfo, get_version
from .logic import Alert, CollapsibleBox, DangerzoneGui, UpdateDialog from .logic import Alert, CollapsibleBox, DangerzoneGui, UpdateDialog
from .updater import UpdateReport from .updater import UpdateReport
@ -61,13 +57,6 @@ about updates.</p>
HAMBURGER_MENU_SIZE = 30 HAMBURGER_MENU_SIZE = 30
WARNING_MESSAGE = """\
<p><b>Warning:</b> Ubuntu Focal systems and their derivatives will
stop being supported in subsequent Dangerzone releases. We encourage you to upgrade to a
more recent version of your operating system in order to get security updates.</p>
"""
def load_svg_image(filename: str, width: int, height: int) -> QtGui.QPixmap: def load_svg_image(filename: str, width: int, height: int) -> QtGui.QPixmap:
"""Load an SVG image from a filename. """Load an SVG image from a filename.
@ -399,24 +388,15 @@ class MainWindow(QtWidgets.QMainWindow):
class InstallContainerThread(QtCore.QThread): class InstallContainerThread(QtCore.QThread):
finished = QtCore.Signal(str) finished = QtCore.Signal()
def __init__(self, dangerzone: DangerzoneGui) -> None: def __init__(self, dangerzone: DangerzoneGui) -> None:
super(InstallContainerThread, self).__init__() super(InstallContainerThread, self).__init__()
self.dangerzone = dangerzone self.dangerzone = dangerzone
def run(self) -> None: def run(self) -> None:
error = None self.dangerzone.isolation_provider.install()
try: self.finished.emit()
installed = self.dangerzone.isolation_provider.install()
except Exception as e:
log.error("Container installation problem")
error = format_exception(e)
else:
if not installed:
error = "The image cannot be found. This can be caused by a faulty container image."
finally:
self.finished.emit(error)
class WaitingWidget(QtWidgets.QWidget): class WaitingWidget(QtWidgets.QWidget):
@ -443,10 +423,9 @@ class TracebackWidget(QTextEdit):
# Enable copying # Enable copying
self.setTextInteractionFlags(Qt.TextSelectableByMouse) self.setTextInteractionFlags(Qt.TextSelectableByMouse)
def set_content(self, error: Optional[str] = None) -> None: def set_content(self, error: str) -> None:
if error: self.setPlainText(error)
self.setPlainText(error) self.setVisible(True)
self.setVisible(True)
class WaitingWidgetContainer(WaitingWidget): class WaitingWidgetContainer(WaitingWidget):
@ -459,6 +438,7 @@ class WaitingWidgetContainer(WaitingWidget):
# #
# Linux states # Linux states
# - "install_container" # - "install_container"
finished = QtCore.Signal()
def __init__(self, dangerzone: DangerzoneGui) -> None: def __init__(self, dangerzone: DangerzoneGui) -> None:
super(WaitingWidgetContainer, self).__init__() super(WaitingWidgetContainer, self).__init__()
@ -500,61 +480,49 @@ class WaitingWidgetContainer(WaitingWidget):
error: Optional[str] = None error: Optional[str] = None
try: try:
self.dangerzone.isolation_provider.is_runtime_available() if isinstance( # Sanity check
self.dangerzone.isolation_provider, Container
):
container_runtime = self.dangerzone.isolation_provider.get_runtime()
runtime_name = self.dangerzone.isolation_provider.get_runtime_name()
except NoContainerTechException as e: except NoContainerTechException as e:
log.error(str(e)) log.error(str(e))
state = "not_installed" state = "not_installed"
except NotAvailableContainerTechException as e:
log.error(str(e))
state = "not_running"
error = e.error
except Exception as e:
log.error(str(e))
state = "not_running"
error = format_exception(e)
else: else:
state = "install_container" # Can we run `docker/podman image ls` without an error
with subprocess.Popen(
[container_runtime, "image", "ls"],
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
startupinfo=get_subprocess_startupinfo(),
) as p:
_, stderr = p.communicate()
if p.returncode != 0:
log.error(f"{runtime_name} is not running")
state = "not_running"
error = stderr.decode()
else:
# Always try installing the container
state = "install_container"
# Update the state # Update the state
self.state_change(state, error) self.state_change(state, error)
def show_error(self, msg: str, details: Optional[str] = None) -> None:
self.label.setText(msg)
show_traceback = details is not None
if show_traceback:
self.traceback.set_content(details)
self.traceback.setVisible(show_traceback)
self.buttons.show()
def show_message(self, msg: str) -> None:
self.label.setText(msg)
self.traceback.setVisible(False)
self.buttons.hide()
def installation_finished(self, error: Optional[str] = None) -> None:
if error:
msg = (
"During installation of the dangerzone image, <br>"
"the following error occured:"
)
self.show_error(msg, error)
else:
self.finished.emit()
def state_change(self, state: str, error: Optional[str] = None) -> None: def state_change(self, state: str, error: Optional[str] = None) -> None:
if state == "not_installed": if state == "not_installed":
if platform.system() == "Linux": if platform.system() == "Linux":
self.show_error( self.label.setText(
"<strong>Dangerzone requires Podman</strong><br><br>" "<strong>Dangerzone requires Podman</strong><br><br>"
"Install it and retry." "Install it and retry."
) )
else: else:
self.show_error( self.label.setText(
"<strong>Dangerzone requires Docker Desktop</strong><br><br>" "<strong>Dangerzone requires Docker Desktop</strong><br><br>"
"<a href='https://www.docker.com/products/docker-desktop'>Download Docker Desktop</a>" "<a href='https://www.docker.com/products/docker-desktop'>Download Docker Desktop</a>"
", install it, and open it." ", install it, and open it."
) )
self.buttons.show()
elif state == "not_running": elif state == "not_running":
if platform.system() == "Linux": if platform.system() == "Linux":
# "not_running" here means that the `podman image ls` command failed. # "not_running" here means that the `podman image ls` command failed.
@ -562,20 +530,27 @@ class WaitingWidgetContainer(WaitingWidget):
"<strong>Dangerzone requires Podman</strong><br><br>" "<strong>Dangerzone requires Podman</strong><br><br>"
"Podman is installed but cannot run properly. See errors below" "Podman is installed but cannot run properly. See errors below"
) )
if error:
self.traceback.set_content(error)
self.label.setText(message)
else: else:
message = ( self.label.setText(
"<strong>Dangerzone requires Docker Desktop</strong><br><br>" "<strong>Dangerzone requires Docker Desktop</strong><br><br>"
"Docker is installed but isn't running.<br><br>" "Docker is installed but isn't running.<br><br>"
"Open Docker and make sure it's running in the background." "Open Docker and make sure it's running in the background."
) )
self.show_error(message, error) self.buttons.show()
else: else:
self.show_message( self.label.setText(
"Installing the Dangerzone container image.<br><br>" "Installing the Dangerzone container image.<br><br>"
"This might take a few minutes..." "This might take a few minutes..."
) )
self.buttons.hide()
self.traceback.setVisible(False)
self.install_container_t = InstallContainerThread(self.dangerzone) self.install_container_t = InstallContainerThread(self.dangerzone)
self.install_container_t.finished.connect(self.installation_finished) self.install_container_t.finished.connect(self.finished)
self.install_container_t.start() self.install_container_t.start()
@ -587,17 +562,6 @@ class ContentWidget(QtWidgets.QWidget):
self.dangerzone = dangerzone self.dangerzone = dangerzone
self.conversion_started = False self.conversion_started = False
self.warning_label = None
if platform.system() == "Linux":
# Add the warning message only for ubuntu focal
os_release_path = Path("/etc/os-release")
if os_release_path.exists():
os_release = os_release_path.read_text()
if "Ubuntu 20.04" in os_release or "focal" in os_release:
self.warning_label = QtWidgets.QLabel(WARNING_MESSAGE)
self.warning_label.setWordWrap(True)
self.warning_label.setProperty("style", "warning")
# Doc selection widget # Doc selection widget
self.doc_selection_widget = DocSelectionWidget(self.dangerzone) self.doc_selection_widget = DocSelectionWidget(self.dangerzone)
self.doc_selection_widget.documents_selected.connect(self.documents_selected) self.doc_selection_widget.documents_selected.connect(self.documents_selected)
@ -623,8 +587,6 @@ class ContentWidget(QtWidgets.QWidget):
# Layout # Layout
layout = QtWidgets.QVBoxLayout() layout = QtWidgets.QVBoxLayout()
if self.warning_label:
layout.addWidget(self.warning_label) # Add warning at the top
layout.addWidget(self.settings_widget, stretch=1) layout.addWidget(self.settings_widget, stretch=1)
layout.addWidget(self.documents_list, stretch=1) layout.addWidget(self.documents_list, stretch=1)
layout.addWidget(self.doc_selection_wrapper, stretch=1) layout.addWidget(self.doc_selection_wrapper, stretch=1)

View file

@ -5,7 +5,9 @@ import platform
import signal import signal
import subprocess import subprocess
import sys import sys
import tempfile
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from pathlib import Path
from typing import IO, Callable, Iterator, Optional from typing import IO, Callable, Iterator, Optional
import fitz import fitz
@ -86,20 +88,19 @@ class IsolationProvider(ABC):
Abstracts an isolation provider Abstracts an isolation provider
""" """
def __init__(self, debug: bool = False) -> None: def __init__(self, debug) -> None:
self.debug = debug self.debug = debug
if self.should_capture_stderr(): if self.should_display_errors():
self.proc_stderr = subprocess.PIPE self.proc_stderr = subprocess.PIPE
else: else:
# We do not trust STDERR from the inner container,
# which could be exploited to ask users to open the document with
# a default PDF reader for instance.
self.proc_stderr = subprocess.DEVNULL self.proc_stderr = subprocess.DEVNULL
def should_capture_stderr(self) -> bool: def should_display_errors(self) -> bool:
return self.debug or getattr(sys, "dangerzone_dev", False) return self.debug or getattr(sys, "dangerzone_dev", False)
@staticmethod
def is_runtime_available() -> bool:
return True
@abstractmethod @abstractmethod
def install(self) -> bool: def install(self) -> bool:
pass pass
@ -227,6 +228,17 @@ class IsolationProvider(ABC):
text = "Successfully converted document" text = "Successfully converted document"
self.print_progress(document, False, text, 100) self.print_progress(document, False, text, 100)
if self.should_display_errors():
assert p.stderr
debug_log = read_debug_text(p.stderr, MAX_CONVERSION_LOG_CHARS)
p.stderr.close()
log.debug(
"Conversion output (doc to pixels)\n"
f"{DOC_TO_PIXELS_LOG_START}\n"
f"{debug_log}" # no need for an extra newline here
f"{DOC_TO_PIXELS_LOG_END}"
)
def print_progress( def print_progress(
self, document: Document, error: bool, text: str, percentage: float self, document: Document, error: bool, text: str, percentage: float
) -> None: ) -> None:
@ -259,7 +271,10 @@ class IsolationProvider(ABC):
"Encountered an I/O error during document to pixels conversion," "Encountered an I/O error during document to pixels conversion,"
f" but the status of the conversion process is unknown (PID: {p.pid})" f" but the status of the conversion process is unknown (PID: {p.pid})"
) )
return errors.exception_from_error_code(error_code) logs = None
if self.debug:
logs = "".join([line.decode() for line in p.stderr.readlines()])
return errors.exception_from_error_code(error_code, logs=logs)
@abstractmethod @abstractmethod
def get_max_parallel_conversions(self) -> int: def get_max_parallel_conversions(self) -> int:
@ -342,9 +357,9 @@ class IsolationProvider(ABC):
) )
# Read the stderr of the process only if: # Read the stderr of the process only if:
# * We're in debug mode # * Dev mode is enabled.
# * The process has exited (else we risk hanging). # * The process has exited (else we risk hanging).
if self.should_capture_stderr() and p.poll() is not None: if getattr(sys, "dangerzone_dev", False) and p.poll() is not None:
assert p.stderr assert p.stderr
debug_log = read_debug_text(p.stderr, MAX_CONVERSION_LOG_CHARS) debug_log = read_debug_text(p.stderr, MAX_CONVERSION_LOG_CHARS)
log.info( log.info(

View file

@ -30,21 +30,6 @@ class NoContainerTechException(Exception):
super().__init__(f"{container_tech} is not installed") super().__init__(f"{container_tech} is not installed")
class NotAvailableContainerTechException(Exception):
def __init__(self, container_tech: str, error: str) -> None:
self.error = error
self.container_tech = container_tech
super().__init__(f"{container_tech} is not available")
class ImageNotPresentException(Exception):
pass
class ImageInstallationException(Exception):
pass
class Container(IsolationProvider): class Container(IsolationProvider):
# Name of the dangerzone container # Name of the dangerzone container
CONTAINER_NAME = "dangerzone.rocks/dangerzone" CONTAINER_NAME = "dangerzone.rocks/dangerzone"
@ -108,7 +93,7 @@ class Container(IsolationProvider):
return runtime return runtime
@staticmethod @staticmethod
def get_runtime_security_args() -> List[str]: def get_runtime_security_args(debug: bool) -> List[str]:
"""Security options applicable to the outer Dangerzone container. """Security options applicable to the outer Dangerzone container.
Our security precautions for the outer Dangerzone container are the following: Our security precautions for the outer Dangerzone container are the following:
@ -152,6 +137,9 @@ class Container(IsolationProvider):
security_args += ["--network=none"] security_args += ["--network=none"]
security_args += ["-u", "dangerzone"] security_args += ["-u", "dangerzone"]
if debug:
security_args += ["-e", "RUNSC_DEBUG=1"]
return security_args return security_args
@staticmethod @staticmethod
@ -171,7 +159,7 @@ class Container(IsolationProvider):
startupinfo=get_subprocess_startupinfo(), startupinfo=get_subprocess_startupinfo(),
) )
chunk_size = 4 << 20 chunk_size = 10240
compressed_container_path = get_resource_path("container.tar.gz") compressed_container_path = get_resource_path("container.tar.gz")
with gzip.open(compressed_container_path) as f: with gzip.open(compressed_container_path) as f:
while True: while True:
@ -181,42 +169,19 @@ class Container(IsolationProvider):
p.stdin.write(chunk) p.stdin.write(chunk)
else: else:
break break
_, err = p.communicate() p.communicate()
if p.returncode < 0:
if err:
error = err.decode()
else:
error = "No output"
raise ImageInstallationException(
f"Could not install container image: {error}"
)
if not Container.is_container_installed(raise_on_error=True): if not Container.is_container_installed():
log.error("Failed to install the container image")
return False return False
log.info("Container image installed") log.info("Container image installed")
return True return True
@staticmethod @staticmethod
def is_runtime_available() -> bool: def is_container_installed() -> bool:
container_runtime = Container.get_runtime()
runtime_name = Container.get_runtime_name()
# Can we run `docker/podman image ls` without an error
with subprocess.Popen(
[container_runtime, "image", "ls"],
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
startupinfo=get_subprocess_startupinfo(),
) as p:
_, stderr = p.communicate()
if p.returncode != 0:
raise NotAvailableContainerTechException(runtime_name, stderr.decode())
return True
@staticmethod
def is_container_installed(raise_on_error: bool = False) -> bool:
""" """
See if the container is installed. See if the podman container is installed. Linux only.
""" """
# Get the image id # Get the image id
with open(get_resource_path("image-id.txt")) as f: with open(get_resource_path("image-id.txt")) as f:
@ -241,18 +206,8 @@ class Container(IsolationProvider):
if found_image_id in expected_image_ids: if found_image_id in expected_image_ids:
installed = True installed = True
elif found_image_id == "": elif found_image_id == "":
if raise_on_error: pass
raise ImageNotPresentException(
"Image is not listed after installation. Bailing out."
)
else: else:
msg = (
f"{Container.CONTAINER_NAME} images found, but IDs do not match."
f" Found: {found_image_id}, Expected: {','.join(expected_image_ids)}"
)
if raise_on_error:
raise ImageNotPresentException(msg)
log.info(msg)
log.info("Deleting old dangerzone container image") log.info("Deleting old dangerzone container image")
try: try:
@ -298,18 +253,13 @@ class Container(IsolationProvider):
extra_args: List[str] = [], extra_args: List[str] = [],
) -> subprocess.Popen: ) -> subprocess.Popen:
container_runtime = self.get_runtime() container_runtime = self.get_runtime()
security_args = self.get_runtime_security_args() security_args = self.get_runtime_security_args(self.debug)
debug_args = []
if self.debug:
debug_args += ["-e", "RUNSC_DEBUG=1"]
enable_stdin = ["-i"] enable_stdin = ["-i"]
set_name = ["--name", name] set_name = ["--name", name]
prevent_leakage_args = ["--rm"] prevent_leakage_args = ["--rm"]
args = ( args = (
["run"] ["run"]
+ security_args + security_args
+ debug_args
+ prevent_leakage_args + prevent_leakage_args
+ enable_stdin + enable_stdin
+ set_name + set_name

View file

@ -70,18 +70,14 @@ class Qubes(IsolationProvider):
standard streams explicitly, so that we can afterwards use `Popen.wait()` to standard streams explicitly, so that we can afterwards use `Popen.wait()` to
learn if the qube terminated. learn if the qube terminated.
Note that we don't close the stderr stream because we want to read debug logs
from it. In the rare case where a qube cannot terminate because it's stuck
writing at stderr (this is not the expected behavior), we expect that the
process will still be forcefully killed after the soft termination timeout
expires.
[1]: https://github.com/freedomofpress/dangerzone/issues/563#issuecomment-2034803232 [1]: https://github.com/freedomofpress/dangerzone/issues/563#issuecomment-2034803232
""" """
if p.stdin: if p.stdin:
p.stdin.close() p.stdin.close()
if p.stdout: if p.stdout:
p.stdout.close() p.stdout.close()
if p.stderr:
p.stderr.close()
def teleport_dz_module(self, wpipe: IO[bytes]) -> None: def teleport_dz_module(self, wpipe: IO[bytes]) -> None:
"""Send the dangerzone module to another qube, as a zipfile.""" """Send the dangerzone module to another qube, as a zipfile."""

View file

@ -2,7 +2,6 @@ import pathlib
import platform import platform
import subprocess import subprocess
import sys import sys
import traceback
import unicodedata import unicodedata
import appdirs import appdirs
@ -118,13 +117,3 @@ def replace_control_chars(untrusted_str: str, keep_newlines: bool = False) -> st
else: else:
sanitized_str += "<EFBFBD>" sanitized_str += "<EFBFBD>"
return sanitized_str return sanitized_str
def format_exception(e: Exception) -> str:
# The signature of traceback.format_exception has changed in python 3.10
if sys.version_info < (3, 10):
output = traceback.format_exception(*sys.exc_info())
else:
output = traceback.format_exception(e)
return "".join(output)

6
debian/changelog vendored
View file

@ -1,9 +1,3 @@
dangerzone (0.8.0) unstable; urgency=low
* Released Dangerzone 0.8.0
-- Freedom of the Press Foundation <info@freedom.press> Tue, 30 Oct 2024 01:56:28 +0300
dangerzone (0.7.1) unstable; urgency=low dangerzone (0.7.1) unstable; urgency=low
* Released Dangerzone 0.7.1 * Released Dangerzone 0.7.1

View file

@ -16,6 +16,42 @@ DEFAULT_USER = "user"
DEFAULT_DRY = False DEFAULT_DRY = False
DEFAULT_DEV = False DEFAULT_DEV = False
DEFAULT_SHOW_DOCKERFILE = False DEFAULT_SHOW_DOCKERFILE = False
DEFAULT_DOWNLOAD_PYSIDE6 = False
PYSIDE6_VERSION = "6.7.1"
PYSIDE6_RPM = "python3-pyside6-{pyside6_version}-1.fc{fedora_version}.x86_64.rpm"
PYSIDE6_URL = (
"https://packages.freedom.press/yum-tools-prod/dangerzone/f{fedora_version}/%s"
% PYSIDE6_RPM
)
PYSIDE6_DL_MESSAGE = """\
Downloading PySide6 RPM from:
{pyside6_url}
into the following local path:
{pyside6_local_path}
The RPM is over 100 MB, so this operation may take a while...
"""
PYSIDE6_NOT_FOUND_ERROR = """\
The following package is not present in your system:
{pyside6_local_path}
You can build it locally and copy it in the expected path, following the instructions
in:
https://github.com/freedomofpress/python3-pyside6-rpm
Alternatively, you can rerun the command adding the '--download-pyside6' flag, which
will download it from:
{pyside6_url}
"""
# The Linux distributions that we currently support. # The Linux distributions that we currently support.
# FIXME: Add a version mapping to avoid mistakes. # FIXME: Add a version mapping to avoid mistakes.
@ -196,6 +232,11 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
""" """
DOCKERFILE_BUILD_FEDORA_39_DEPS = r"""
COPY {pyside6_rpm} /tmp/pyside6.rpm
RUN dnf install -y /tmp/pyside6.rpm
"""
DOCKERFILE_BUILD_FEDORA_DEPS = r""" DOCKERFILE_BUILD_FEDORA_DEPS = r"""
RUN dnf install -y mupdf thunar && dnf clean all RUN dnf install -y mupdf thunar && dnf clean all
@ -349,6 +390,74 @@ def get_files_in(*folders: list[str]) -> list[pathlib.Path]:
return files return files
class PySide6Manager:
"""Provision PySide6 RPMs in our Dangerzone environments.
This class holds all the logic around checking and downloading PySide RPMs. It can
check if the required RPM version is present under "/dist", and optionally download
it.
"""
def __init__(self, distro_name, distro_version):
if distro_name != "fedora":
raise RuntimeError("Managing PySide6 RPMs is available only in Fedora")
self.distro_name = distro_name
self.distro_version = distro_version
@property
def version(self):
"""The version of the PySide6 RPM."""
return PYSIDE6_VERSION
@property
def rpm_name(self):
"""The name of the PySide6 RPM."""
return PYSIDE6_RPM.format(
pyside6_version=self.version, fedora_version=self.distro_version
)
@property
def rpm_url(self):
"""The URL of the PySide6 RPM, as hosted in FPF's RPM repo."""
return PYSIDE6_URL.format(
pyside6_version=self.version,
fedora_version=self.distro_version,
)
@property
def rpm_local_path(self):
"""The local path where this script will look for the PySide6 RPM."""
return git_root() / "dist" / self.rpm_name
@property
def is_rpm_present(self):
"""Check if PySide6 RPM is present in the user's system."""
return self.rpm_local_path.exists()
def download_rpm(self):
"""Download PySide6 from FPF's RPM repo."""
print(
PYSIDE6_DL_MESSAGE.format(
pyside6_url=self.rpm_url,
pyside6_local_path=self.rpm_local_path,
),
file=sys.stderr,
)
try:
with (
urllib.request.urlopen(self.rpm_url) as r,
open(self.rpm_local_path, "wb") as f,
):
shutil.copyfileobj(r, f)
except:
# NOTE: We purposefully catch all exceptions, since we want to catch Ctrl-C
# as well.
print("Download interrupted, removing file", file=sys.stderr)
self.rpm_local_path.unlink()
raise
print("PySide6 was downloaded successfully", file=sys.stderr)
class Env: class Env:
"""A class that implements actions on Dangerzone environments""" """A class that implements actions on Dangerzone environments"""
@ -587,6 +696,8 @@ class Env:
DOCKERFILE_CONMON_UPDATE + DOCKERFILE_BUILD_DEV_DEBIAN_DEPS DOCKERFILE_CONMON_UPDATE + DOCKERFILE_BUILD_DEV_DEBIAN_DEPS
) )
elif self.distro == "ubuntu" and self.version in ( elif self.distro == "ubuntu" and self.version in (
"23.10",
"mantic",
"24.04", "24.04",
"noble", "noble",
"24.10", "24.10",
@ -627,6 +738,7 @@ class Env:
def build( def build(
self, self,
show_dockerfile=DEFAULT_SHOW_DOCKERFILE, show_dockerfile=DEFAULT_SHOW_DOCKERFILE,
download_pyside6=DEFAULT_DOWNLOAD_PYSIDE6,
): ):
"""Build a Linux environment and install Dangerzone in it.""" """Build a Linux environment and install Dangerzone in it."""
build_dir = distro_build(self.distro, self.version) build_dir = distro_build(self.distro, self.version)
@ -639,6 +751,28 @@ class Env:
package = package_src.name package = package_src.name
package_dst = build_dir / package package_dst = build_dir / package
install_cmd = "dnf install -y" install_cmd = "dnf install -y"
# NOTE: For Fedora 39, we check if a PySide6 RPM package exists in
# the user's system. If not, we either throw an error or download it from
# FPF's repo, according to the user's choice.
if self.version == "39":
pyside6 = PySide6Manager(self.distro, self.version)
if not pyside6.is_rpm_present:
if download_pyside6:
pyside6.download_rpm()
else:
print(
PYSIDE6_NOT_FOUND_ERROR.format(
pyside6_local_path=pyside6.rpm_local_path,
pyside6_url=pyside6.rpm_url,
),
file=sys.stderr,
)
return 1
shutil.copy(pyside6.rpm_local_path, build_dir / pyside6.rpm_name)
install_deps = (
DOCKERFILE_BUILD_FEDORA_DEPS + DOCKERFILE_BUILD_FEDORA_39_DEPS
).format(pyside6_rpm=pyside6.rpm_name)
else: else:
install_deps = DOCKERFILE_BUILD_DEBIAN_DEPS install_deps = DOCKERFILE_BUILD_DEBIAN_DEPS
if self.distro == "ubuntu" and self.version in ("20.04", "focal"): if self.distro == "ubuntu" and self.version in ("20.04", "focal"):
@ -650,6 +784,8 @@ class Env:
# package (see https://github.com/freedomofpress/dangerzone/issues/685) # package (see https://github.com/freedomofpress/dangerzone/issues/685)
install_deps = DOCKERFILE_CONMON_UPDATE + DOCKERFILE_BUILD_DEBIAN_DEPS install_deps = DOCKERFILE_CONMON_UPDATE + DOCKERFILE_BUILD_DEBIAN_DEPS
elif self.distro == "ubuntu" and self.version in ( elif self.distro == "ubuntu" and self.version in (
"23.10",
"mantic",
"24.04", "24.04",
"noble", "noble",
"24.10", "24.10",
@ -712,6 +848,7 @@ def env_build(args):
env = Env.from_args(args) env = Env.from_args(args)
return env.build( return env.build(
show_dockerfile=args.show_dockerfile, show_dockerfile=args.show_dockerfile,
download_pyside6=args.download_pyside6,
) )
@ -808,6 +945,12 @@ def parse_args():
action="store_true", action="store_true",
help="Do not build, only show the Dockerfile", help="Do not build, only show the Dockerfile",
) )
parser_build.add_argument(
"--download-pyside6",
default=DEFAULT_DOWNLOAD_PYSIDE6,
action="store_true",
help="Download PySide6 from FPF's RPM repo",
)
return parser.parse_args() return parser.parse_args()

View file

@ -1,254 +0,0 @@
#!/usr/bin/env python3
import argparse
import asyncio
import re
import sys
from datetime import datetime
from typing import Dict, List, Optional, Tuple
import httpx
REPOSITORY = "https://github.com/freedomofpress/dangerzone/"
TEMPLATE = "- {title} ([#{number}]({url}))"
def parse_version(version: str) -> Tuple[int, int]:
"""Extract major.minor from version string, ignoring patch"""
match = re.match(r"v?(\d+)\.(\d+)", version)
if not match:
raise ValueError(f"Invalid version format: {version}")
return (int(match.group(1)), int(match.group(2)))
async def get_last_minor_release(
client: httpx.AsyncClient, owner: str, repo: str
) -> Optional[str]:
"""Get the latest minor release date (ignoring patches)"""
response = await client.get(f"https://api.github.com/repos/{owner}/{repo}/releases")
response.raise_for_status()
releases = response.json()
if not releases:
return None
# Get the latest minor version by comparing major.minor numbers
current_version = parse_version(releases[0]["tag_name"])
latest_date = None
for release in releases:
try:
version = parse_version(release["tag_name"])
if version < current_version:
latest_date = release["published_at"]
break
except ValueError:
continue
return latest_date
async def get_issue_details(
client: httpx.AsyncClient, owner: str, repo: str, issue_number: int
) -> Optional[dict]:
"""Get issue title and number if it exists"""
response = await client.get(
f"https://api.github.com/repos/{owner}/{repo}/issues/{issue_number}"
)
if response.is_success:
data = response.json()
return {
"title": data["title"],
"number": data["number"],
"url": data["html_url"],
}
return None
def extract_issue_number(pr_body: Optional[str]) -> Optional[int]:
"""Extract issue number from PR body looking for common formats like 'Fixes #123' or 'Closes #123'"""
if not pr_body:
return None
patterns = [
r"(?:closes|fixes|resolves)\s*#(\d+)",
r"(?:close|fix|resolve)\s*#(\d+)",
]
for pattern in patterns:
match = re.search(pattern, pr_body.lower())
if match:
return int(match.group(1))
return None
async def verify_commit_in_master(
client: httpx.AsyncClient, owner: str, repo: str, commit_id: str
) -> bool:
"""Verify if a commit exists in master"""
response = await client.get(
f"https://api.github.com/repos/{owner}/{repo}/commits/{commit_id}"
)
return response.is_success and response.json().get("commit") is not None
async def process_issue_events(
client: httpx.AsyncClient, owner: str, repo: str, issue: Dict
) -> Optional[Dict]:
"""Process events for a single issue"""
events_response = await client.get(f"{issue['url']}/events")
if not events_response.is_success:
return None
for event in events_response.json():
if event["event"] == "closed" and event.get("commit_id"):
if await verify_commit_in_master(client, owner, repo, event["commit_id"]):
return {
"title": issue["title"],
"number": issue["number"],
"url": issue["html_url"],
}
return None
async def get_closed_issues(
client: httpx.AsyncClient, owner: str, repo: str, since: str
) -> List[Dict]:
"""Get issues closed by commits to master since the given date"""
response = await client.get(
f"https://api.github.com/repos/{owner}/{repo}/issues",
params={
"state": "closed",
"sort": "updated",
"direction": "desc",
"since": since,
"per_page": 100,
},
)
response.raise_for_status()
tasks = []
since_date = datetime.strptime(since, "%Y-%m-%dT%H:%M:%SZ")
for issue in response.json():
if "pull_request" in issue:
continue
closed_at = datetime.strptime(issue["closed_at"], "%Y-%m-%dT%H:%M:%SZ")
if closed_at <= since_date:
continue
tasks.append(process_issue_events(client, owner, repo, issue))
results = await asyncio.gather(*tasks)
return [r for r in results if r is not None]
async def process_pull_request(
client: httpx.AsyncClient,
owner: str,
repo: str,
pr: Dict,
closed_issues: List[Dict],
) -> Optional[str]:
"""Process a single pull request"""
issue_number = extract_issue_number(pr.get("body"))
if issue_number:
issue = await get_issue_details(client, owner, repo, issue_number)
if issue:
if not any(i["number"] == issue["number"] for i in closed_issues):
return TEMPLATE.format(**issue)
return None
return TEMPLATE.format(title=pr["title"], number=pr["number"], url=pr["html_url"])
async def get_changes_since_last_release(
owner: str, repo: str, token: Optional[str] = None
) -> List[str]:
headers = {
"Accept": "application/vnd.github.v3+json",
}
if token:
headers["Authorization"] = f"token {token}"
else:
print(
"Warning: No token provided. API rate limiting may occur.", file=sys.stderr
)
async with httpx.AsyncClient(headers=headers, timeout=30.0) as client:
# Get the date of last minor release
since = await get_last_minor_release(client, owner, repo)
if not since:
return []
changes = []
# Get issues closed by commits to master
closed_issues = await get_closed_issues(client, owner, repo, since)
changes.extend([TEMPLATE.format(**issue) for issue in closed_issues])
# Get merged PRs
response = await client.get(
f"https://api.github.com/repos/{owner}/{repo}/pulls",
params={
"state": "closed",
"sort": "updated",
"direction": "desc",
"per_page": 100,
},
)
response.raise_for_status()
# Process PRs in parallel
pr_tasks = []
for pr in response.json():
if not pr["merged_at"]:
continue
if since and pr["merged_at"] <= since:
break
pr_tasks.append(
process_pull_request(client, owner, repo, pr, closed_issues)
)
pr_results = await asyncio.gather(*pr_tasks)
changes.extend([r for r in pr_results if r is not None])
return changes
async def main_async():
parser = argparse.ArgumentParser(description="Generate release notes from GitHub")
parser.add_argument("--token", "-t", help="the file path to the GitHub API token")
args = parser.parse_args()
token = None
if args.token:
with open(args.token) as f:
token = f.read().strip()
try:
url_path = REPOSITORY.rstrip("/").split("github.com/")[1]
owner, repo = url_path.split("/")[-2:]
except (ValueError, IndexError):
print("Error: Invalid GitHub URL", file=sys.stderr)
sys.exit(1)
try:
notes = await get_changes_since_last_release(owner, repo, token)
print("\n".join(notes))
except httpx.HTTPError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
def main():
asyncio.run(main_async())
if __name__ == "__main__":
main()

View file

@ -3,20 +3,14 @@
import abc import abc
import argparse import argparse
import difflib import difflib
import json
import logging import logging
import re import re
import selectors import selectors
import subprocess import subprocess
import sys import sys
import urllib.request
from pathlib import Path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
PYTHON_VERSION = "3.12"
EOL_PYTHON_URL = "https://endoflife.date/api/python.json"
CONTENT_QA = r"""## QA CONTENT_QA = r"""## QA
To ensure that new releases do not introduce regressions, and support existing To ensure that new releases do not introduce regressions, and support existing
@ -782,10 +776,6 @@ class QABase(abc.ABC):
self.prompt("Does it pass?", choices=["y", "n"]) self.prompt("Does it pass?", choices=["y", "n"])
logger.info("Successfully completed QA scenarios") logger.info("Successfully completed QA scenarios")
@task("Download Tesseract data", auto=True)
def download_tessdata(self):
self.run("python", str(Path("install", "common", "download-tessdata.py")))
@classmethod @classmethod
@abc.abstractmethod @abc.abstractmethod
def get_id(cls): def get_id(cls):
@ -812,40 +802,6 @@ class QAWindows(QABase):
while msvcrt.kbhit(): while msvcrt.kbhit():
msvcrt.getch() msvcrt.getch()
def get_latest_python_release(self):
with urllib.request.urlopen(EOL_PYTHON_URL) as f:
resp = f.read()
releases = json.loads(resp)
for release in releases:
if release["cycle"] == PYTHON_VERSION:
# Transform the Python version string (e.g., "3.12.7") into a list
# (e.g., [3, 12, 7]), and return it
return [int(num) for num in release["latest"].split(".")]
raise RuntimeError(
f"Could not find a Python release for version {PYTHON_VERSION}"
)
@QABase.task(
f"Install the latest version of Python {PYTHON_VERSION}", ref=REF_BUILD
)
def install_python(self):
logger.info("Getting latest Python release")
try:
latest_version = self.get_latest_python_release()
except Exception:
logger.error("Could not verify that the latest Python version is installed")
cur_version = list(sys.version_info[:3])
if latest_version > cur_version:
self.prompt(
f"You need to install the latest Python version ({latest_version})"
)
elif latest_version == cur_version:
logger.info(
f"Verified that the latest Python version ({latest_version}) is installed"
)
@QABase.task("Install and Run Docker Desktop", ref=REF_BUILD) @QABase.task("Install and Run Docker Desktop", ref=REF_BUILD)
def install_docker(self): def install_docker(self):
logger.info("Checking if Docker Desktop is installed and running") logger.info("Checking if Docker Desktop is installed and running")
@ -860,7 +816,7 @@ class QAWindows(QABase):
) )
def install_poetry(self): def install_poetry(self):
self.run("python", "-m", "pip", "install", "poetry") self.run("python", "-m", "pip", "install", "poetry")
self.run("poetry", "install", "--sync") self.run("poetry", "install")
@QABase.task("Build Dangerzone container image", ref=REF_BUILD, auto=True) @QABase.task("Build Dangerzone container image", ref=REF_BUILD, auto=True)
def build_image(self): def build_image(self):
@ -882,11 +838,9 @@ class QAWindows(QABase):
return "windows" return "windows"
def start(self): def start(self):
self.install_python()
self.install_docker() self.install_docker()
self.install_poetry() self.install_poetry()
self.build_image() self.build_image()
self.download_tessdata()
self.run_tests() self.run_tests()
self.build_dangerzone_exe() self.build_dangerzone_exe()
@ -961,6 +915,7 @@ class QALinux(QABase):
"--version", "--version",
self.VERSION, self.VERSION,
"build", "build",
"--download-pyside6",
) )
@classmethod @classmethod
@ -978,7 +933,6 @@ class QALinux(QABase):
def start(self): def start(self):
self.build_dev_image() self.build_dev_image()
self.build_container_image() self.build_container_image()
self.download_tessdata()
self.run_tests() self.run_tests()
self.build_package() self.build_package()
self.build_qa_image() self.build_qa_image()
@ -1024,6 +978,11 @@ class QAUbuntu2204(QADebianBased):
VERSION = "22.04" VERSION = "22.04"
class QAUbuntu2310(QADebianBased):
DISTRO = "ubuntu"
VERSION = "23.10"
class QAUbuntu2404(QADebianBased): class QAUbuntu2404(QADebianBased):
DISTRO = "ubuntu" DISTRO = "ubuntu"
VERSION = "24.04" VERSION = "24.04"
@ -1055,6 +1014,10 @@ class QAFedora40(QAFedora):
VERSION = "40" VERSION = "40"
class QAFedora39(QAFedora):
VERSION = "39"
def parse_args(): def parse_args():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog=sys.argv[0], prog=sys.argv[0],

View file

@ -11,8 +11,7 @@ log = logging.getLogger(__name__)
DZ_ASSETS = [ DZ_ASSETS = [
"container-{version}-i686.tar.gz", "container.tar.gz",
"container-{version}-arm64.tar.gz",
"Dangerzone-{version}.msi", "Dangerzone-{version}.msi",
"Dangerzone-{version}-arm64.dmg", "Dangerzone-{version}-arm64.dmg",
"Dangerzone-{version}-i686.dmg", "Dangerzone-{version}-i686.dmg",

View file

@ -32,7 +32,7 @@ Name: dangerzone-qubes
Name: dangerzone Name: dangerzone
%endif %endif
Version: 0.8.0 Version: 0.7.1
Release: 1%{?dist} Release: 1%{?dist}
Summary: Take potentially dangerous PDFs, office documents, or images and convert them to safe PDFs Summary: Take potentially dangerous PDFs, office documents, or images and convert them to safe PDFs

View file

@ -10,5 +10,11 @@
<true/> <true/>
<key>com.apple.security.network.client</key> <key>com.apple.security.network.client</key>
<true/> <true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.hypervisor</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict> </dict>
</plist> </plist>

889
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "dangerzone" name = "dangerzone"
version = "0.8.0" version = "0.7.1"
description = "Take potentially dangerous PDFs, office documents, or images and convert them to safe PDFs" description = "Take potentially dangerous PDFs, office documents, or images and convert them to safe PDFs"
authors = ["Freedom of the Press Foundation <info@freedom.press>", "Micah Lee <micah.lee@theintercept.com>"] authors = ["Freedom of the Press Foundation <info@freedom.press>", "Micah Lee <micah.lee@theintercept.com>"]
license = "AGPL-3.0" license = "AGPL-3.0"
@ -31,7 +31,7 @@ dangerzone-cli = 'dangerzone:main'
# Dependencies required for packaging the code on various platforms. # Dependencies required for packaging the code on various platforms.
[tool.poetry.group.package.dependencies] [tool.poetry.group.package.dependencies]
setuptools = "*" setuptools = "*"
cx_freeze = {version = "^7.2.5", platform = "win32"} cx_freeze = {version = "^7.1.1", platform = "win32"}
pywin32 = {version = "*", platform = "win32"} pywin32 = {version = "*", platform = "win32"}
pyinstaller = {version = "*", platform = "darwin"} pyinstaller = {version = "*", platform = "darwin"}
@ -51,14 +51,9 @@ pytest-mock = "^3.10.0"
pytest-qt = "^4.2.0" pytest-qt = "^4.2.0"
pytest-cov = "^5.0.0" pytest-cov = "^5.0.0"
strip-ansi = "*" strip-ansi = "*"
pytest-subprocess = "^1.5.2"
pytest-rerunfailures = "^14.0"
[tool.poetry.group.container.dependencies] [tool.poetry.group.container.dependencies]
pymupdf = "1.24.11" # Last version to support python 3.8 (needed for Ubuntu Focal support) pymupdf = "^1.24.10"
[tool.poetry.group.dev.dependencies]
httpx = "^0.27.2"
[tool.isort] [tool.isort]
profile = "black" profile = "black"

View file

@ -4,6 +4,7 @@ from cx_Freeze import Executable, setup
with open("share/version.txt") as f: with open("share/version.txt") as f:
version = f.read().strip() version = f.read().strip()
packages = ["dangerzone", "dangerzone.gui"]
setup( setup(
name="dangerzone", name="dangerzone",
@ -11,9 +12,10 @@ setup(
# On Windows description will show as the app's name in the "Open With" menu. See: # On Windows description will show as the app's name in the "Open With" menu. See:
# https://github.com/freedomofpress/dangerzone/issues/283#issuecomment-1365148805 # https://github.com/freedomofpress/dangerzone/issues/283#issuecomment-1365148805
description="Dangerzone", description="Dangerzone",
packages=packages,
options={ options={
"build_exe": { "build_exe": {
"packages": ["dangerzone", "dangerzone.gui"], "packages": packages,
"excludes": ["test", "tkinter"], "excludes": ["test", "tkinter"],
"include_files": [("share", "share"), ("LICENSE", "LICENSE")], "include_files": [("share", "share"), ("LICENSE", "LICENSE")],
"include_msvcr": True, "include_msvcr": True,

View file

@ -55,20 +55,4 @@ QTextEdit[style="traceback"] {
background-color: #ffffff; background-color: #ffffff;
color: #000000; color: #000000;
padding: 10px; padding: 10px;
} }
QLabel[style="warning"] {
background-color: #FFF3CD;
color: #856404;
border: 1px solid #FFEEBA;
border-radius: 4px;
padding: 10px;
margin: 10px;
}
MainWindow[OSColorMode="dark"] QLabel[style="warning"] {
background-color: #332D00;
color: #FFD970;
border-color: #665A00;
}

View file

@ -1 +1 @@
0.8.0 0.7.1

View file

@ -1,13 +1,11 @@
import os import os
import pathlib import pathlib
import platform
import shutil import shutil
import time import time
from typing import List from typing import List
from pytest import MonkeyPatch, fixture from pytest import MonkeyPatch, fixture
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from pytest_subprocess import FakeProcess
from pytestqt.qtbot import QtBot from pytestqt.qtbot import QtBot
from dangerzone.document import Document from dangerzone.document import Document
@ -15,21 +13,12 @@ from dangerzone.gui import MainWindow
from dangerzone.gui import main_window as main_window_module from dangerzone.gui import main_window as main_window_module
from dangerzone.gui import updater as updater_module from dangerzone.gui import updater as updater_module
from dangerzone.gui.logic import DangerzoneGui from dangerzone.gui.logic import DangerzoneGui
from dangerzone.gui.main_window import ( # import Pyside related objects from here to avoid duplicating import logic.
# import Pyside related objects from here to avoid duplicating import logic.
from dangerzone.gui.main_window import (
ContentWidget, ContentWidget,
InstallContainerThread,
QtCore, QtCore,
QtGui, QtGui,
WaitingWidgetContainer,
) )
from dangerzone.gui.updater import UpdateReport, UpdaterThread from dangerzone.gui.updater import UpdateReport, UpdaterThread
from dangerzone.isolation_provider.container import (
Container,
NoContainerTechException,
NotAvailableContainerTechException,
)
from .test_updater import assert_report_equal, default_updater_settings from .test_updater import assert_report_equal, default_updater_settings
@ -503,90 +492,3 @@ def test_drop_1_invalid_2_valid_documents(
content_widget.doc_selection_wrapper.dropEvent( content_widget.doc_selection_wrapper.dropEvent(
drag_1_invalid_and_2_valid_files_event drag_1_invalid_and_2_valid_files_event
) )
def test_not_available_container_tech_exception(
qtbot: QtBot, mocker: MockerFixture
) -> None:
# Setup
mock_app = mocker.MagicMock()
dummy = mocker.MagicMock()
dummy.is_runtime_available.side_effect = NotAvailableContainerTechException(
"podman", "podman image ls logs"
)
dz = DangerzoneGui(mock_app, dummy)
widget = WaitingWidgetContainer(dz)
qtbot.addWidget(widget)
# Assert that the error is displayed in the GUI
if platform.system() in ["Darwin", "Windows"]:
assert "Dangerzone requires Docker Desktop" in widget.label.text()
else:
assert "Podman is installed but cannot run properly" in widget.label.text()
assert "podman image ls logs" in widget.traceback.toPlainText()
def test_no_container_tech_exception(qtbot: QtBot, mocker: MockerFixture) -> None:
# Setup
mock_app = mocker.MagicMock()
dummy = mocker.MagicMock()
# Raise
dummy.is_runtime_available.side_effect = NoContainerTechException("podman")
dz = DangerzoneGui(mock_app, dummy)
widget = WaitingWidgetContainer(dz)
qtbot.addWidget(widget)
# Assert that the error is displayed in the GUI
if platform.system() in ["Darwin", "Windows"]:
assert "Dangerzone requires Docker Desktop" in widget.label.text()
else:
assert "Dangerzone requires Podman" in widget.label.text()
def test_installation_failure_exception(qtbot: QtBot, mocker: MockerFixture) -> None:
"""Ensures that if an exception is raised during image installation,
it is shown in the GUI.
"""
# Setup install to raise an exception
mock_app = mocker.MagicMock()
dummy = mocker.MagicMock(spec=Container)
dummy.install.side_effect = RuntimeError("Error during install")
dz = DangerzoneGui(mock_app, dummy)
# Mock the InstallContainerThread to call the original run method instead of
# starting a new thread
mocker.patch.object(InstallContainerThread, "start", InstallContainerThread.run)
widget = WaitingWidgetContainer(dz)
qtbot.addWidget(widget)
assert dummy.install.call_count == 1
assert "Error during install" in widget.traceback.toPlainText()
assert "RuntimeError" in widget.traceback.toPlainText()
def test_installation_failure_return_false(qtbot: QtBot, mocker: MockerFixture) -> None:
"""Ensures that if the installation returns False, the error is shown in the GUI."""
# Setup install to return False
mock_app = mocker.MagicMock()
dummy = mocker.MagicMock(spec=Container)
dummy.install.return_value = False
dz = DangerzoneGui(mock_app, dummy)
# Mock the InstallContainerThread to call the original run method instead of
# starting a new thread
mocker.patch.object(InstallContainerThread, "start", InstallContainerThread.run)
widget = WaitingWidgetContainer(dz)
qtbot.addWidget(widget)
assert dummy.install.call_count == 1
assert "the following error occured" in widget.label.text()
assert "The image cannot be found" in widget.traceback.toPlainText()

View file

@ -7,6 +7,7 @@ from pytest_mock import MockerFixture
from dangerzone.conversion import errors from dangerzone.conversion import errors
from dangerzone.document import Document from dangerzone.document import Document
from dangerzone.isolation_provider import base from dangerzone.isolation_provider import base
from dangerzone.isolation_provider.qubes import running_on_qubes
TIMEOUT_STARTUP = 60 # Timeout in seconds until the conversion sandbox starts. TIMEOUT_STARTUP = 60 # Timeout in seconds until the conversion sandbox starts.
@ -164,7 +165,6 @@ class IsolationProviderTermination:
terminate_proc_mock = mocker.patch.object( terminate_proc_mock = mocker.patch.object(
provider, "terminate_doc_to_pixels_proc", return_value=None provider, "terminate_doc_to_pixels_proc", return_value=None
) )
kill_pg_orig = base.kill_process_group
kill_pg_mock = mocker.patch( kill_pg_mock = mocker.patch(
"dangerzone.isolation_provider.base.kill_process_group", return_value=None "dangerzone.isolation_provider.base.kill_process_group", return_value=None
) )
@ -179,7 +179,6 @@ class IsolationProviderTermination:
# Reset the function to the original state. # Reset the function to the original state.
provider.terminate_doc_to_pixels_proc = terminate_proc_orig # type: ignore [method-assign] provider.terminate_doc_to_pixels_proc = terminate_proc_orig # type: ignore [method-assign]
base.kill_process_group = kill_pg_orig
# Really kill the spawned process, so that it doesn't linger after the tests # Really kill the spawned process, so that it doesn't linger after the tests
# complete. # complete.

View file

@ -1,15 +1,10 @@
import os import os
import subprocess
import time
import pytest import pytest
from pytest_mock import MockerFixture
from pytest_subprocess import FakeProcess
from dangerzone.isolation_provider.container import ( from dangerzone.isolation_provider.container import Container
Container,
ImageInstallationException,
ImageNotPresentException,
NotAvailableContainerTechException,
)
from dangerzone.isolation_provider.qubes import is_qubes_native_conversion from dangerzone.isolation_provider.qubes import is_qubes_native_conversion
from .base import IsolationProviderTermination, IsolationProviderTest from .base import IsolationProviderTermination, IsolationProviderTest
@ -27,94 +22,7 @@ def provider() -> Container:
class TestContainer(IsolationProviderTest): class TestContainer(IsolationProviderTest):
def test_is_runtime_available_raises( pass
self, provider: Container, fp: FakeProcess
) -> None:
"""
NotAvailableContainerTechException should be raised when
the "podman image ls" command fails.
"""
fp.register_subprocess(
[provider.get_runtime(), "image", "ls"],
returncode=-1,
stderr="podman image ls logs",
)
with pytest.raises(NotAvailableContainerTechException):
provider.is_runtime_available()
def test_is_runtime_available_works(
self, provider: Container, fp: FakeProcess
) -> None:
"""
No exception should be raised when the "podman image ls" can return properly.
"""
fp.register_subprocess(
[provider.get_runtime(), "image", "ls"],
)
provider.is_runtime_available()
def test_install_raise_if_image_cant_be_installed(
self, mocker: MockerFixture, provider: Container, fp: FakeProcess
) -> None:
"""When an image installation fails, an exception should be raised"""
fp.register_subprocess(
[provider.get_runtime(), "image", "ls"],
)
# First check should return nothing.
fp.register_subprocess(
[
provider.get_runtime(),
"image",
"list",
"--format",
"{{.ID}}",
"dangerzone.rocks/dangerzone",
],
occurrences=2,
)
# Make podman load fail
mocker.patch("gzip.open", mocker.mock_open(read_data=""))
fp.register_subprocess(
[provider.get_runtime(), "load"],
returncode=-1,
)
with pytest.raises(ImageInstallationException):
provider.install()
def test_install_raises_if_still_not_installed(
self, mocker: MockerFixture, provider: Container, fp: FakeProcess
) -> None:
"""When an image keep being not installed, it should return False"""
fp.register_subprocess(
[provider.get_runtime(), "image", "ls"],
)
# First check should return nothing.
fp.register_subprocess(
[
provider.get_runtime(),
"image",
"list",
"--format",
"{{.ID}}",
"dangerzone.rocks/dangerzone",
],
occurrences=2,
)
# Patch gzip.open and podman load so that it works
mocker.patch("gzip.open", mocker.mock_open(read_data=""))
fp.register_subprocess(
[provider.get_runtime(), "load"],
)
with pytest.raises(ImageNotPresentException):
provider.install()
class TestContainerTermination(IsolationProviderTermination): class TestContainerTermination(IsolationProviderTermination):

View file

@ -335,7 +335,6 @@ class TestCliConversion(TestCliBasic):
class TestExtraFormats(TestCli): class TestExtraFormats(TestCli):
@for_each_external_doc("*hwp*") @for_each_external_doc("*hwp*")
@pytest.mark.flaky(reruns=2)
def test_hancom_office(self, doc: str) -> None: def test_hancom_office(self, doc: str) -> None:
if is_qubes_native_conversion(): if is_qubes_native_conversion():
pytest.skip("HWP / HWPX formats are not supported on this platform") pytest.skip("HWP / HWPX formats are not supported on this platform")

Binary file not shown.