mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-05-05 21:21:49 +02:00
Compare commits
44 commits
981fcd4e3f
...
9810ae402b
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9810ae402b | ||
![]() |
c89988654c | ||
![]() |
7eaa0cfe50 | ||
![]() |
9d69e3b261 | ||
![]() |
1d2a91e8c5 | ||
![]() |
82c29b2098 | ||
![]() |
ce5aca4ba1 | ||
![]() |
13f38cc8a9 | ||
![]() |
57df6fdfe5 | ||
![]() |
20354e7c11 | ||
![]() |
d722800a4b | ||
![]() |
4cfc633cdb | ||
![]() |
944d58dd8d | ||
![]() |
f3806b96af | ||
![]() |
c4bb7c28c8 | ||
![]() |
630083bdea | ||
![]() |
504a9e1df2 | ||
![]() |
a54a8f2057 | ||
![]() |
35abd14f5f | ||
![]() |
1bd18a175b | ||
![]() |
96aa56a6dc | ||
![]() |
91932046f5 | ||
![]() |
c8411de433 | ||
![]() |
95150bcfc1 | ||
![]() |
bae109717c | ||
![]() |
00480551ca | ||
![]() |
32deea10c4 | ||
![]() |
f540a67d06 | ||
![]() |
68f8338d20 | ||
![]() |
d561878e03 | ||
![]() |
59e1666c28 | ||
![]() |
95d7d8a4d9 | ||
![]() |
ed2791bbbc | ||
![]() |
c1cf16a705 | ||
![]() |
281432fcaa | ||
![]() |
71cc4b37e5 | ||
![]() |
5ed4a048a0 | ||
![]() |
50627d375c | ||
![]() |
8172195f95 | ||
![]() |
f5242078a9 | ||
![]() |
e68a43bbbf | ||
![]() |
10fb631b8e | ||
![]() |
796ca79289 | ||
![]() |
a95b612e78 |
36 changed files with 1384 additions and 717 deletions
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
|
@ -1,6 +1,10 @@
|
|||
name: Build dev environments
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "test/**"
|
||||
schedule:
|
||||
- cron: "0 0 * * *" # Run every day at 00:00 UTC.
|
||||
|
||||
|
@ -33,8 +37,6 @@ jobs:
|
|||
version: "20.04"
|
||||
- distro: ubuntu
|
||||
version: "22.04"
|
||||
- distro: ubuntu
|
||||
version: "23.10"
|
||||
- distro: ubuntu
|
||||
version: "24.04"
|
||||
- distro: ubuntu
|
||||
|
@ -45,8 +47,6 @@ jobs:
|
|||
version: bookworm
|
||||
- distro: debian
|
||||
version: trixie
|
||||
- distro: fedora
|
||||
version: "39"
|
||||
- distro: fedora
|
||||
version: "40"
|
||||
- distro: fedora
|
||||
|
|
2
.github/workflows/check_push.yml
vendored
2
.github/workflows/check_push.yml
vendored
|
@ -1,6 +1,6 @@
|
|||
name: Check branch conformity
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
prevent-fixup-commits:
|
||||
|
|
2
.github/workflows/check_repos.yml
vendored
2
.github/workflows/check_repos.yml
vendored
|
@ -23,8 +23,6 @@ jobs:
|
|||
version: "24.10" # oracular
|
||||
- distro: ubuntu
|
||||
version: "24.04" # noble
|
||||
- distro: ubuntu
|
||||
version: "23.10" # mantic
|
||||
- distro: ubuntu
|
||||
version: "22.04" # jammy
|
||||
- distro: ubuntu
|
||||
|
|
35
.github/workflows/ci.yml
vendored
35
.github/workflows/ci.yml
vendored
|
@ -1,8 +1,10 @@
|
|||
name: Tests
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "test/**"
|
||||
schedule:
|
||||
- cron: "2 0 * * *" # Run every day at 02:00 UTC.
|
||||
workflow_dispatch:
|
||||
|
@ -78,7 +80,7 @@ jobs:
|
|||
path: share/tessdata/
|
||||
key: v1-tessdata-${{ hashFiles('./install/common/download-tessdata.py') }}
|
||||
enableCrossOsArchive: true
|
||||
- uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Download Tessdata
|
||||
|
@ -91,7 +93,8 @@ jobs:
|
|||
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
needs: download-tessdata
|
||||
needs:
|
||||
- download-tessdata
|
||||
env:
|
||||
DUMMY_CONVERSION: 1
|
||||
steps:
|
||||
|
@ -121,7 +124,8 @@ jobs:
|
|||
macOS:
|
||||
name: "macOS (${{ matrix.arch }})"
|
||||
runs-on: ${{ matrix.runner }}
|
||||
needs: download-tessdata
|
||||
needs:
|
||||
- download-tessdata
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
|
@ -149,9 +153,10 @@ jobs:
|
|||
run: poetry run make test
|
||||
|
||||
build-deb:
|
||||
needs:
|
||||
- build-container-image
|
||||
name: "build-deb (${{ matrix.distro }} ${{ matrix.version }})"
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-container-image
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
|
@ -159,8 +164,6 @@ jobs:
|
|||
version: "20.04"
|
||||
- distro: ubuntu
|
||||
version: "22.04"
|
||||
- distro: ubuntu
|
||||
version: "23.10"
|
||||
- distro: ubuntu
|
||||
version: "24.04"
|
||||
- distro: ubuntu
|
||||
|
@ -221,7 +224,8 @@ jobs:
|
|||
install-deb:
|
||||
name: "install-deb (${{ matrix.distro }} ${{ matrix.version }})"
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-deb
|
||||
needs:
|
||||
- build-deb
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
|
@ -229,8 +233,6 @@ jobs:
|
|||
version: "20.04"
|
||||
- distro: ubuntu
|
||||
version: "22.04"
|
||||
- distro: ubuntu
|
||||
version: "23.10"
|
||||
- distro: ubuntu
|
||||
version: "24.04"
|
||||
- distro: ubuntu
|
||||
|
@ -277,11 +279,12 @@ jobs:
|
|||
build-install-rpm:
|
||||
name: "build-install-rpm (${{ matrix.distro }} ${{matrix.version}})"
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-container-image
|
||||
needs:
|
||||
- build-container-image
|
||||
strategy:
|
||||
matrix:
|
||||
distro: ["fedora"]
|
||||
version: ["39", "40", "41"]
|
||||
version: ["40", "41"]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
@ -325,7 +328,7 @@ jobs:
|
|||
run: |
|
||||
./dev_scripts/env.py --distro ${{ matrix.distro }} \
|
||||
--version ${{ matrix.version }} \
|
||||
build --download-pyside6
|
||||
build
|
||||
|
||||
- name: Run a test command
|
||||
run: |
|
||||
|
@ -350,8 +353,6 @@ jobs:
|
|||
version: "20.04"
|
||||
- distro: ubuntu
|
||||
version: "22.04"
|
||||
- distro: ubuntu
|
||||
version: "23.10"
|
||||
- distro: ubuntu
|
||||
version: "24.04"
|
||||
- distro: ubuntu
|
||||
|
@ -362,8 +363,6 @@ jobs:
|
|||
version: bookworm
|
||||
- distro: debian
|
||||
version: trixie
|
||||
- distro: fedora
|
||||
version: "39"
|
||||
- distro: fedora
|
||||
version: "40"
|
||||
- distro: fedora
|
||||
|
|
22
.github/workflows/close-issues.yml
vendored
Normal file
22
.github/workflows/close-issues.yml
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
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
|
3
.github/workflows/scan.yml
vendored
3
.github/workflows/scan.yml
vendored
|
@ -1,8 +1,9 @@
|
|||
name: Scan latest app and container
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
schedule:
|
||||
- cron: '0 0 * * *' # Run every day at 00:00 UTC.
|
||||
workflow_dispatch:
|
||||
|
|
22
.github/workflows/scan_released.yml
vendored
22
.github/workflows/scan_released.yml
vendored
|
@ -6,16 +6,24 @@ on:
|
|||
|
||||
jobs:
|
||||
security-scan-container:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
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:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Download container image for the latest release
|
||||
- name: Download container image for the latest release and load it
|
||||
run: |
|
||||
VERSION=$(curl https://api.github.com/repos/freedomofpress/dangerzone/releases/latest | jq -r '.tag_name')
|
||||
wget https://github.com/freedomofpress/dangerzone/releases/download/${VERSION}/container.tar.gz
|
||||
- name: Load container image
|
||||
run: docker load -i container.tar.gz
|
||||
VERSION=$(curl https://api.github.com/repos/freedomofpress/dangerzone/releases/latest | grep "tag_name" | cut -d '"' -f 4)
|
||||
CONTAINER_FILENAME=container-${VERSION:1}-${{ matrix.arch }}.tar.gz
|
||||
wget https://github.com/freedomofpress/dangerzone/releases/download/${VERSION}/${CONTAINER_FILENAME} -O ${CONTAINER_FILENAME}
|
||||
docker load -i ${CONTAINER_FILENAME}
|
||||
# NOTE: Scan first without failing, else we won't be able to read the scan
|
||||
# report.
|
||||
- name: Scan container image (no fail)
|
||||
|
@ -30,7 +38,7 @@ jobs:
|
|||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: ${{ steps.scan_container.outputs.sarif }}
|
||||
category: container
|
||||
category: container-${{ matrix.arch }}
|
||||
- name: Inspect container scan report
|
||||
run: cat ${{ steps.scan_container.outputs.sarif }}
|
||||
- name: Scan container image
|
||||
|
|
15
BUILD.md
15
BUILD.md
|
@ -260,11 +260,17 @@ The following instructions require typing commands in a terminal in dom0.
|
|||
|
||||
```
|
||||
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
|
||||
> development. In that case, replace `dz` with the qube of your choice in the
|
||||
> 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
|
||||
allow launching a disposable qube (`dz-dvm`) when Dangerzone converts a
|
||||
|
@ -308,18 +314,9 @@ test it.
|
|||
1. Install the `.rpm` package you just copied
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
### Developing Dangerzone
|
||||
|
|
50
CHANGELOG.md
50
CHANGELOG.md
|
@ -5,16 +5,60 @@ 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/)
|
||||
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.7.1...HEAD)
|
||||
## [Unreleased](https://github.com/freedomofpress/dangerzone/compare/v0.8.0...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
|
||||
|
||||
- Point to the installation instructions that the Tails team maintains for Dangerzone ([announcement](https://tails.net/news/dangerzone/index.en.html))
|
||||
- Platform support: Ubuntu 24.10 and Fedora 41 ([issue #947](https://github.com/freedomofpress/dangerzone/issues/947))
|
||||
- Installation and execution errors are now caught and displayed in the interface ([#193](https://github.com/freedomofpress/dangerzone/issues/193))
|
||||
- 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)
|
||||
|
||||
### Fixed
|
||||
### Fixed
|
||||
|
||||
- Fix an `image-id.txt` mismatch happening on Docker Desktop >= 4.30.0 ([#933](https://github.com/freedomofpress/dangerzone/issues/933))
|
||||
|
||||
|
|
|
@ -74,9 +74,7 @@ FROM alpine:latest
|
|||
RUN apk --no-cache -U upgrade && \
|
||||
apk --no-cache add python3
|
||||
|
||||
# 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)"; \
|
||||
RUN GVISOR_URL="https://storage.googleapis.com/gvisor/releases/release/latest/$(uname -m)"; \
|
||||
wget "${GVISOR_URL}/runsc" "${GVISOR_URL}/runsc.sha512" && \
|
||||
sha512sum -c runsc.sha512 && \
|
||||
rm -f runsc.sha512 && \
|
||||
|
|
42
INSTALL.md
42
INSTALL.md
|
@ -1,8 +1,21 @@
|
|||
## 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
|
||||
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
|
||||
On Linux, Dangerzone uses [Podman](https://podman.io/) instead of Docker Desktop for creating
|
||||
|
@ -11,7 +24,6 @@ an isolated environment. It will be installed automatically when installing Dang
|
|||
Dangerzone is available for:
|
||||
- Ubuntu 24.10 (oracular)
|
||||
- Ubuntu 24.04 (noble)
|
||||
- Ubuntu 23.10 (mantic)
|
||||
- Ubuntu 22.04 (jammy)
|
||||
- Ubuntu 20.04 (focal)
|
||||
- Debian 13 (trixie)
|
||||
|
@ -19,7 +31,6 @@ Dangerzone is available for:
|
|||
- Debian 11 (bullseye)
|
||||
- Fedora 41
|
||||
- Fedora 40
|
||||
- Fedora 39
|
||||
- Tails
|
||||
- Qubes OS (beta support)
|
||||
|
||||
|
@ -126,23 +137,6 @@ sudo apt install -y dangerzone
|
|||
|
||||
### 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:
|
||||
|
||||
```
|
||||
|
@ -290,7 +284,7 @@ Our [GitHub Releases page](https://github.com/freedomofpress/dangerzone/releases
|
|||
hosts the following files:
|
||||
* Windows installer (`Dangerzone-<version>.msi`)
|
||||
* macOS archives (`Dangerzone-<version>-<arch>.dmg`)
|
||||
* Container image (`container.tar.gz`)
|
||||
* Container images (`container-<version>-<arch>.tar.gz`)
|
||||
* Source package (`dangerzone-<version>.tar.gz`)
|
||||
|
||||
All these files are accompanied by signatures (as `.asc` files). We'll explain
|
||||
|
@ -315,10 +309,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
|
||||
```
|
||||
|
||||
For the container image:
|
||||
For the container images:
|
||||
|
||||
```
|
||||
gpg --verify container.tar.gz.asc container.tar.gz
|
||||
gpg --verify container-0.6.1-i686.tar.gz.asc container-0.6.1-i686.tar.gz
|
||||
```
|
||||
|
||||
For the source package:
|
||||
|
|
30
README.md
30
README.md
|
@ -6,33 +6,21 @@ Take potentially dangerous PDFs, office documents, or images and convert them to
|
|||
|  | 
|
||||
|--|--|
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
_Read more about Dangerzone in the [official site](https://dangerzone.rocks/about/)._
|
||||
|
||||
## Getting started
|
||||
|
||||
### 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)
|
||||
Follow the instructions for each platform:
|
||||
|
||||
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
|
||||
|
||||
- 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.
|
||||
* [macOS](https://github.com/freedomofpress/dangerzone/blob/v0.8.0//INSTALL.md#macos)
|
||||
* [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)
|
||||
* [Debian Linux](https://github.com/freedomofpress/dangerzone/blob/v0.8.0/INSTALL.md#ubuntu-debian)
|
||||
* [Fedora Linux](https://github.com/freedomofpress/dangerzone/blob/v0.8.0/INSTALL.md#fedora)
|
||||
* [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)
|
||||
|
||||
## Some features
|
||||
|
||||
|
|
22
RELEASE.md
22
RELEASE.md
|
@ -9,7 +9,6 @@ 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\>**
|
||||
- [ ] [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`
|
||||
- [ ] [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 `share/version.txt`
|
||||
- [ ] Update the "Version" field in `install/linux/dangerzone.spec`
|
||||
|
@ -44,16 +43,6 @@ In case of an EOL version:
|
|||
* Consult the previous paragraph, but also `grep` your way around.
|
||||
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
|
||||
|
||||
Parallel to the QA process, the release candidate should be put through the large document tests in a dedicated machine to run overnight.
|
||||
|
@ -285,6 +274,11 @@ 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`
|
||||
- [ ] Verify and checkout the git tag for this release
|
||||
- [ ] 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`
|
||||
- [ ] Make sure that the build application works with the containerd graph
|
||||
driver (see [#933](https://github.com/freedomofpress/dangerzone/issues/933))
|
||||
|
@ -403,6 +397,8 @@ Build the latest container:
|
|||
python3 ./install/common/build-image.py
|
||||
```
|
||||
|
||||
Copy the container image to the assets folder on `dangerzone-$VERSION-i686.tar.gz`.
|
||||
|
||||
Create a .rpm:
|
||||
|
||||
```sh
|
||||
|
@ -449,9 +445,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/)
|
||||
* You can use `./dev_scripts/upload-asset.py`, if you want to upload an asset
|
||||
using an access token.
|
||||
- [ ] Upload the `container.tar.gz` i686 image that was created in the previous step
|
||||
- [ ] Upload the `container-$VERSION-i686.tar.gz` and `container-$VERSION-arm64.tar.gz` images that were created in the previous step
|
||||
|
||||
**Important:** Make sure that it's the same container image as the ones that
|
||||
**Important:** Make sure that it's the same container images as the ones that
|
||||
are shipped in other platforms (see our [Pre-release](#Pre-release) section)
|
||||
|
||||
- [ ] Upload the detached signatures (.asc) and checksum file.
|
||||
|
|
|
@ -42,6 +42,11 @@ def print_header(s: str) -> None:
|
|||
type=click.UNPROCESSED,
|
||||
callback=args.validate_input_filenames,
|
||||
)
|
||||
@click.option(
|
||||
"--debug",
|
||||
"debug",
|
||||
flag_value=True,
|
||||
help="Run Dangerzone in debug mode, to get logs from gVisor.")
|
||||
@click.version_option(version=get_version(), message="%(version)s")
|
||||
@errors.handle_document_errors
|
||||
def cli_main(
|
||||
|
@ -50,6 +55,7 @@ def cli_main(
|
|||
filenames: List[str],
|
||||
archive: bool,
|
||||
dummy_conversion: bool,
|
||||
debug: bool,
|
||||
) -> None:
|
||||
setup_logging()
|
||||
|
||||
|
@ -58,7 +64,7 @@ def cli_main(
|
|||
elif is_qubes_native_conversion():
|
||||
dangerzone = DangerzoneCore(Qubes())
|
||||
else:
|
||||
dangerzone = DangerzoneCore(Container())
|
||||
dangerzone = DangerzoneCore(Container(debug=debug))
|
||||
|
||||
display_banner()
|
||||
if len(filenames) == 1 and output_filename:
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import logging
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import tempfile
|
||||
import typing
|
||||
from multiprocessing.pool import ThreadPool
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
# FIXME: See https://github.com/freedomofpress/dangerzone/issues/320 for more details.
|
||||
|
@ -20,15 +20,19 @@ else:
|
|||
from PySide6.QtWidgets import QTextEdit
|
||||
except ImportError:
|
||||
from PySide2 import QtCore, QtGui, QtSvg, QtWidgets
|
||||
from PySide2.QtWidgets import QAction, QTextEdit
|
||||
from PySide2.QtCore import Qt
|
||||
from PySide2.QtWidgets import QAction, QTextEdit
|
||||
|
||||
from .. import errors
|
||||
from ..document import SAFE_EXTENSION, Document
|
||||
from ..isolation_provider.container import Container, NoContainerTechException
|
||||
from ..isolation_provider.container import (
|
||||
Container,
|
||||
NoContainerTechException,
|
||||
NotAvailableContainerTechException,
|
||||
)
|
||||
from ..isolation_provider.dummy import Dummy
|
||||
from ..isolation_provider.qubes import Qubes, is_qubes_native_conversion
|
||||
from ..util import get_resource_path, get_subprocess_startupinfo, get_version
|
||||
from ..util import format_exception, get_resource_path, get_version
|
||||
from .logic import Alert, CollapsibleBox, DangerzoneGui, UpdateDialog
|
||||
from .updater import UpdateReport
|
||||
|
||||
|
@ -57,6 +61,13 @@ about updates.</p>
|
|||
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:
|
||||
"""Load an SVG image from a filename.
|
||||
|
||||
|
@ -388,15 +399,24 @@ class MainWindow(QtWidgets.QMainWindow):
|
|||
|
||||
|
||||
class InstallContainerThread(QtCore.QThread):
|
||||
finished = QtCore.Signal()
|
||||
finished = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, dangerzone: DangerzoneGui) -> None:
|
||||
super(InstallContainerThread, self).__init__()
|
||||
self.dangerzone = dangerzone
|
||||
|
||||
def run(self) -> None:
|
||||
self.dangerzone.isolation_provider.install()
|
||||
self.finished.emit()
|
||||
error = None
|
||||
try:
|
||||
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):
|
||||
|
@ -423,9 +443,10 @@ class TracebackWidget(QTextEdit):
|
|||
# Enable copying
|
||||
self.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
||||
|
||||
def set_content(self, error: str) -> None:
|
||||
self.setPlainText(error)
|
||||
self.setVisible(True)
|
||||
def set_content(self, error: Optional[str] = None) -> None:
|
||||
if error:
|
||||
self.setPlainText(error)
|
||||
self.setVisible(True)
|
||||
|
||||
|
||||
class WaitingWidgetContainer(WaitingWidget):
|
||||
|
@ -438,7 +459,6 @@ class WaitingWidgetContainer(WaitingWidget):
|
|||
#
|
||||
# Linux states
|
||||
# - "install_container"
|
||||
finished = QtCore.Signal()
|
||||
|
||||
def __init__(self, dangerzone: DangerzoneGui) -> None:
|
||||
super(WaitingWidgetContainer, self).__init__()
|
||||
|
@ -480,49 +500,61 @@ class WaitingWidgetContainer(WaitingWidget):
|
|||
error: Optional[str] = None
|
||||
|
||||
try:
|
||||
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()
|
||||
self.dangerzone.isolation_provider.is_runtime_available()
|
||||
except NoContainerTechException as e:
|
||||
log.error(str(e))
|
||||
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:
|
||||
# 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"
|
||||
state = "install_container"
|
||||
|
||||
# Update the state
|
||||
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:
|
||||
if state == "not_installed":
|
||||
if platform.system() == "Linux":
|
||||
self.label.setText(
|
||||
self.show_error(
|
||||
"<strong>Dangerzone requires Podman</strong><br><br>"
|
||||
"Install it and retry."
|
||||
)
|
||||
else:
|
||||
self.label.setText(
|
||||
self.show_error(
|
||||
"<strong>Dangerzone requires Docker Desktop</strong><br><br>"
|
||||
"<a href='https://www.docker.com/products/docker-desktop'>Download Docker Desktop</a>"
|
||||
", install it, and open it."
|
||||
)
|
||||
self.buttons.show()
|
||||
|
||||
elif state == "not_running":
|
||||
if platform.system() == "Linux":
|
||||
# "not_running" here means that the `podman image ls` command failed.
|
||||
|
@ -530,27 +562,20 @@ class WaitingWidgetContainer(WaitingWidget):
|
|||
"<strong>Dangerzone requires Podman</strong><br><br>"
|
||||
"Podman is installed but cannot run properly. See errors below"
|
||||
)
|
||||
if error:
|
||||
self.traceback.set_content(error)
|
||||
|
||||
self.label.setText(message)
|
||||
|
||||
else:
|
||||
self.label.setText(
|
||||
message = (
|
||||
"<strong>Dangerzone requires Docker Desktop</strong><br><br>"
|
||||
"Docker is installed but isn't running.<br><br>"
|
||||
"Open Docker and make sure it's running in the background."
|
||||
)
|
||||
self.buttons.show()
|
||||
self.show_error(message, error)
|
||||
else:
|
||||
self.label.setText(
|
||||
self.show_message(
|
||||
"Installing the Dangerzone container image.<br><br>"
|
||||
"This might take a few minutes..."
|
||||
)
|
||||
self.buttons.hide()
|
||||
self.traceback.setVisible(False)
|
||||
self.install_container_t = InstallContainerThread(self.dangerzone)
|
||||
self.install_container_t.finished.connect(self.finished)
|
||||
self.install_container_t.finished.connect(self.installation_finished)
|
||||
self.install_container_t.start()
|
||||
|
||||
|
||||
|
@ -562,6 +587,17 @@ class ContentWidget(QtWidgets.QWidget):
|
|||
self.dangerzone = dangerzone
|
||||
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
|
||||
self.doc_selection_widget = DocSelectionWidget(self.dangerzone)
|
||||
self.doc_selection_widget.documents_selected.connect(self.documents_selected)
|
||||
|
@ -587,6 +623,8 @@ class ContentWidget(QtWidgets.QWidget):
|
|||
|
||||
# Layout
|
||||
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.documents_list, stretch=1)
|
||||
layout.addWidget(self.doc_selection_wrapper, stretch=1)
|
||||
|
|
|
@ -6,7 +6,6 @@ import signal
|
|||
import subprocess
|
||||
import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from typing import IO, Callable, Iterator, Optional
|
||||
|
||||
import fitz
|
||||
|
@ -87,12 +86,20 @@ class IsolationProvider(ABC):
|
|||
Abstracts an isolation provider
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
if getattr(sys, "dangerzone_dev", False) is True:
|
||||
def __init__(self, debug: bool = False) -> None:
|
||||
self.debug = debug
|
||||
if self.should_capture_stderr():
|
||||
self.proc_stderr = subprocess.PIPE
|
||||
else:
|
||||
self.proc_stderr = subprocess.DEVNULL
|
||||
|
||||
def should_capture_stderr(self) -> bool:
|
||||
return self.debug or getattr(sys, "dangerzone_dev", False)
|
||||
|
||||
@staticmethod
|
||||
def is_runtime_available() -> bool:
|
||||
return True
|
||||
|
||||
@abstractmethod
|
||||
def install(self) -> bool:
|
||||
pass
|
||||
|
@ -335,9 +342,9 @@ class IsolationProvider(ABC):
|
|||
)
|
||||
|
||||
# Read the stderr of the process only if:
|
||||
# * Dev mode is enabled.
|
||||
# * We're in debug mode
|
||||
# * The process has exited (else we risk hanging).
|
||||
if getattr(sys, "dangerzone_dev", False) and p.poll() is not None:
|
||||
if self.should_capture_stderr() and p.poll() is not None:
|
||||
assert p.stderr
|
||||
debug_log = read_debug_text(p.stderr, MAX_CONVERSION_LOG_CHARS)
|
||||
log.info(
|
||||
|
|
|
@ -30,6 +30,21 @@ class NoContainerTechException(Exception):
|
|||
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):
|
||||
# Name of the dangerzone container
|
||||
CONTAINER_NAME = "dangerzone.rocks/dangerzone"
|
||||
|
@ -156,7 +171,7 @@ class Container(IsolationProvider):
|
|||
startupinfo=get_subprocess_startupinfo(),
|
||||
)
|
||||
|
||||
chunk_size = 10240
|
||||
chunk_size = 4 << 20
|
||||
compressed_container_path = get_resource_path("container.tar.gz")
|
||||
with gzip.open(compressed_container_path) as f:
|
||||
while True:
|
||||
|
@ -166,19 +181,42 @@ class Container(IsolationProvider):
|
|||
p.stdin.write(chunk)
|
||||
else:
|
||||
break
|
||||
p.communicate()
|
||||
_, err = 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():
|
||||
log.error("Failed to install the container image")
|
||||
if not Container.is_container_installed(raise_on_error=True):
|
||||
return False
|
||||
|
||||
log.info("Container image installed")
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def is_container_installed() -> bool:
|
||||
def is_runtime_available() -> 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 podman container is installed. Linux only.
|
||||
See if the container is installed.
|
||||
"""
|
||||
# Get the image id
|
||||
with open(get_resource_path("image-id.txt")) as f:
|
||||
|
@ -203,8 +241,18 @@ class Container(IsolationProvider):
|
|||
if found_image_id in expected_image_ids:
|
||||
installed = True
|
||||
elif found_image_id == "":
|
||||
pass
|
||||
if raise_on_error:
|
||||
raise ImageNotPresentException(
|
||||
"Image is not listed after installation. Bailing out."
|
||||
)
|
||||
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")
|
||||
|
||||
try:
|
||||
|
@ -251,12 +299,17 @@ class Container(IsolationProvider):
|
|||
) -> subprocess.Popen:
|
||||
container_runtime = self.get_runtime()
|
||||
security_args = self.get_runtime_security_args()
|
||||
debug_args = []
|
||||
if self.debug:
|
||||
debug_args += ["-e", "RUNSC_DEBUG=1"]
|
||||
|
||||
enable_stdin = ["-i"]
|
||||
set_name = ["--name", name]
|
||||
prevent_leakage_args = ["--rm"]
|
||||
args = (
|
||||
["run"]
|
||||
+ security_args
|
||||
+ debug_args
|
||||
+ prevent_leakage_args
|
||||
+ enable_stdin
|
||||
+ set_name
|
||||
|
|
|
@ -70,14 +70,18 @@ class Qubes(IsolationProvider):
|
|||
standard streams explicitly, so that we can afterwards use `Popen.wait()` to
|
||||
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
|
||||
"""
|
||||
if p.stdin:
|
||||
p.stdin.close()
|
||||
if p.stdout:
|
||||
p.stdout.close()
|
||||
if p.stderr:
|
||||
p.stderr.close()
|
||||
|
||||
def teleport_dz_module(self, wpipe: IO[bytes]) -> None:
|
||||
"""Send the dangerzone module to another qube, as a zipfile."""
|
||||
|
|
|
@ -2,6 +2,7 @@ import pathlib
|
|||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
import unicodedata
|
||||
|
||||
import appdirs
|
||||
|
@ -117,3 +118,13 @@ def replace_control_chars(untrusted_str: str, keep_newlines: bool = False) -> st
|
|||
else:
|
||||
sanitized_str += "<EFBFBD>"
|
||||
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
6
debian/changelog
vendored
|
@ -1,3 +1,9 @@
|
|||
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
|
||||
|
||||
* Released Dangerzone 0.7.1
|
||||
|
|
|
@ -16,42 +16,6 @@ DEFAULT_USER = "user"
|
|||
DEFAULT_DRY = False
|
||||
DEFAULT_DEV = 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.
|
||||
# FIXME: Add a version mapping to avoid mistakes.
|
||||
|
@ -232,11 +196,6 @@ RUN apt-get update \
|
|||
&& 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"""
|
||||
RUN dnf install -y mupdf thunar && dnf clean all
|
||||
|
||||
|
@ -390,74 +349,6 @@ def get_files_in(*folders: list[str]) -> list[pathlib.Path]:
|
|||
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:
|
||||
"""A class that implements actions on Dangerzone environments"""
|
||||
|
||||
|
@ -696,8 +587,6 @@ class Env:
|
|||
DOCKERFILE_CONMON_UPDATE + DOCKERFILE_BUILD_DEV_DEBIAN_DEPS
|
||||
)
|
||||
elif self.distro == "ubuntu" and self.version in (
|
||||
"23.10",
|
||||
"mantic",
|
||||
"24.04",
|
||||
"noble",
|
||||
"24.10",
|
||||
|
@ -738,7 +627,6 @@ class Env:
|
|||
def build(
|
||||
self,
|
||||
show_dockerfile=DEFAULT_SHOW_DOCKERFILE,
|
||||
download_pyside6=DEFAULT_DOWNLOAD_PYSIDE6,
|
||||
):
|
||||
"""Build a Linux environment and install Dangerzone in it."""
|
||||
build_dir = distro_build(self.distro, self.version)
|
||||
|
@ -751,28 +639,6 @@ class Env:
|
|||
package = package_src.name
|
||||
package_dst = build_dir / package
|
||||
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:
|
||||
install_deps = DOCKERFILE_BUILD_DEBIAN_DEPS
|
||||
if self.distro == "ubuntu" and self.version in ("20.04", "focal"):
|
||||
|
@ -784,8 +650,6 @@ class Env:
|
|||
# package (see https://github.com/freedomofpress/dangerzone/issues/685)
|
||||
install_deps = DOCKERFILE_CONMON_UPDATE + DOCKERFILE_BUILD_DEBIAN_DEPS
|
||||
elif self.distro == "ubuntu" and self.version in (
|
||||
"23.10",
|
||||
"mantic",
|
||||
"24.04",
|
||||
"noble",
|
||||
"24.10",
|
||||
|
@ -848,7 +712,6 @@ def env_build(args):
|
|||
env = Env.from_args(args)
|
||||
return env.build(
|
||||
show_dockerfile=args.show_dockerfile,
|
||||
download_pyside6=args.download_pyside6,
|
||||
)
|
||||
|
||||
|
||||
|
@ -945,12 +808,6 @@ def parse_args():
|
|||
action="store_true",
|
||||
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()
|
||||
|
||||
|
|
254
dev_scripts/generate-release-notes.py
Executable file
254
dev_scripts/generate-release-notes.py
Executable file
|
@ -0,0 +1,254 @@
|
|||
#!/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()
|
|
@ -3,14 +3,20 @@
|
|||
import abc
|
||||
import argparse
|
||||
import difflib
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import selectors
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PYTHON_VERSION = "3.12"
|
||||
EOL_PYTHON_URL = "https://endoflife.date/api/python.json"
|
||||
|
||||
CONTENT_QA = r"""## QA
|
||||
|
||||
To ensure that new releases do not introduce regressions, and support existing
|
||||
|
@ -776,6 +782,10 @@ class QABase(abc.ABC):
|
|||
self.prompt("Does it pass?", choices=["y", "n"])
|
||||
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
|
||||
@abc.abstractmethod
|
||||
def get_id(cls):
|
||||
|
@ -802,6 +812,40 @@ class QAWindows(QABase):
|
|||
while msvcrt.kbhit():
|
||||
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)
|
||||
def install_docker(self):
|
||||
logger.info("Checking if Docker Desktop is installed and running")
|
||||
|
@ -816,7 +860,7 @@ class QAWindows(QABase):
|
|||
)
|
||||
def install_poetry(self):
|
||||
self.run("python", "-m", "pip", "install", "poetry")
|
||||
self.run("poetry", "install")
|
||||
self.run("poetry", "install", "--sync")
|
||||
|
||||
@QABase.task("Build Dangerzone container image", ref=REF_BUILD, auto=True)
|
||||
def build_image(self):
|
||||
|
@ -838,9 +882,11 @@ class QAWindows(QABase):
|
|||
return "windows"
|
||||
|
||||
def start(self):
|
||||
self.install_python()
|
||||
self.install_docker()
|
||||
self.install_poetry()
|
||||
self.build_image()
|
||||
self.download_tessdata()
|
||||
self.run_tests()
|
||||
self.build_dangerzone_exe()
|
||||
|
||||
|
@ -915,7 +961,6 @@ class QALinux(QABase):
|
|||
"--version",
|
||||
self.VERSION,
|
||||
"build",
|
||||
"--download-pyside6",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
|
@ -933,6 +978,7 @@ class QALinux(QABase):
|
|||
def start(self):
|
||||
self.build_dev_image()
|
||||
self.build_container_image()
|
||||
self.download_tessdata()
|
||||
self.run_tests()
|
||||
self.build_package()
|
||||
self.build_qa_image()
|
||||
|
@ -978,11 +1024,6 @@ class QAUbuntu2204(QADebianBased):
|
|||
VERSION = "22.04"
|
||||
|
||||
|
||||
class QAUbuntu2310(QADebianBased):
|
||||
DISTRO = "ubuntu"
|
||||
VERSION = "23.10"
|
||||
|
||||
|
||||
class QAUbuntu2404(QADebianBased):
|
||||
DISTRO = "ubuntu"
|
||||
VERSION = "24.04"
|
||||
|
@ -1014,10 +1055,6 @@ class QAFedora40(QAFedora):
|
|||
VERSION = "40"
|
||||
|
||||
|
||||
class QAFedora39(QAFedora):
|
||||
VERSION = "39"
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog=sys.argv[0],
|
||||
|
|
|
@ -11,7 +11,8 @@ log = logging.getLogger(__name__)
|
|||
|
||||
|
||||
DZ_ASSETS = [
|
||||
"container.tar.gz",
|
||||
"container-{version}-i686.tar.gz",
|
||||
"container-{version}-arm64.tar.gz",
|
||||
"Dangerzone-{version}.msi",
|
||||
"Dangerzone-{version}-arm64.dmg",
|
||||
"Dangerzone-{version}-i686.dmg",
|
||||
|
|
|
@ -32,7 +32,7 @@ Name: dangerzone-qubes
|
|||
Name: dangerzone
|
||||
%endif
|
||||
|
||||
Version: 0.7.1
|
||||
Version: 0.8.0
|
||||
Release: 1%{?dist}
|
||||
Summary: Take potentially dangerous PDFs, office documents, or images and convert them to safe PDFs
|
||||
|
||||
|
|
|
@ -10,11 +10,5 @@
|
|||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<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>
|
||||
</plist>
|
||||
</plist>
|
||||
|
|
889
poetry.lock
generated
889
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "dangerzone"
|
||||
version = "0.7.1"
|
||||
version = "0.8.0"
|
||||
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>"]
|
||||
license = "AGPL-3.0"
|
||||
|
@ -31,7 +31,7 @@ dangerzone-cli = 'dangerzone:main'
|
|||
# Dependencies required for packaging the code on various platforms.
|
||||
[tool.poetry.group.package.dependencies]
|
||||
setuptools = "*"
|
||||
cx_freeze = {version = "^7.1.1", platform = "win32"}
|
||||
cx_freeze = {version = "^7.2.5", platform = "win32"}
|
||||
pywin32 = {version = "*", platform = "win32"}
|
||||
pyinstaller = {version = "*", platform = "darwin"}
|
||||
|
||||
|
@ -51,9 +51,14 @@ pytest-mock = "^3.10.0"
|
|||
pytest-qt = "^4.2.0"
|
||||
pytest-cov = "^5.0.0"
|
||||
strip-ansi = "*"
|
||||
pytest-subprocess = "^1.5.2"
|
||||
pytest-rerunfailures = "^14.0"
|
||||
|
||||
[tool.poetry.group.container.dependencies]
|
||||
pymupdf = "^1.24.10"
|
||||
pymupdf = "1.24.11" # Last version to support python 3.8 (needed for Ubuntu Focal support)
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
httpx = "^0.27.2"
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
|
|
|
@ -4,7 +4,6 @@ from cx_Freeze import Executable, setup
|
|||
with open("share/version.txt") as f:
|
||||
version = f.read().strip()
|
||||
|
||||
packages = ["dangerzone", "dangerzone.gui"]
|
||||
|
||||
setup(
|
||||
name="dangerzone",
|
||||
|
@ -12,10 +11,9 @@ setup(
|
|||
# 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
|
||||
description="Dangerzone",
|
||||
packages=packages,
|
||||
options={
|
||||
"build_exe": {
|
||||
"packages": packages,
|
||||
"packages": ["dangerzone", "dangerzone.gui"],
|
||||
"excludes": ["test", "tkinter"],
|
||||
"include_files": [("share", "share"), ("LICENSE", "LICENSE")],
|
||||
"include_msvcr": True,
|
||||
|
|
|
@ -55,4 +55,20 @@ QTextEdit[style="traceback"] {
|
|||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.7.1
|
||||
0.8.0
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import shutil
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
from pytest import MonkeyPatch, fixture
|
||||
from pytest_mock import MockerFixture
|
||||
from pytest_subprocess import FakeProcess
|
||||
from pytestqt.qtbot import QtBot
|
||||
|
||||
from dangerzone.document import Document
|
||||
|
@ -13,12 +15,21 @@ from dangerzone.gui import MainWindow
|
|||
from dangerzone.gui import main_window as main_window_module
|
||||
from dangerzone.gui import updater as updater_module
|
||||
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,
|
||||
InstallContainerThread,
|
||||
QtCore,
|
||||
QtGui,
|
||||
WaitingWidgetContainer,
|
||||
)
|
||||
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
|
||||
|
||||
|
@ -492,3 +503,90 @@ def test_drop_1_invalid_2_valid_documents(
|
|||
content_widget.doc_selection_wrapper.dropEvent(
|
||||
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()
|
||||
|
|
|
@ -7,7 +7,6 @@ from pytest_mock import MockerFixture
|
|||
from dangerzone.conversion import errors
|
||||
from dangerzone.document import Document
|
||||
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.
|
||||
|
||||
|
@ -165,6 +164,7 @@ class IsolationProviderTermination:
|
|||
terminate_proc_mock = mocker.patch.object(
|
||||
provider, "terminate_doc_to_pixels_proc", return_value=None
|
||||
)
|
||||
kill_pg_orig = base.kill_process_group
|
||||
kill_pg_mock = mocker.patch(
|
||||
"dangerzone.isolation_provider.base.kill_process_group", return_value=None
|
||||
)
|
||||
|
@ -179,6 +179,7 @@ class IsolationProviderTermination:
|
|||
|
||||
# Reset the function to the original state.
|
||||
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
|
||||
# complete.
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
from pytest_subprocess import FakeProcess
|
||||
|
||||
from dangerzone.isolation_provider.container import Container
|
||||
from dangerzone.isolation_provider.container import (
|
||||
Container,
|
||||
ImageInstallationException,
|
||||
ImageNotPresentException,
|
||||
NotAvailableContainerTechException,
|
||||
)
|
||||
from dangerzone.isolation_provider.qubes import is_qubes_native_conversion
|
||||
|
||||
from .base import IsolationProviderTermination, IsolationProviderTest
|
||||
|
@ -22,7 +27,94 @@ def provider() -> Container:
|
|||
|
||||
|
||||
class TestContainer(IsolationProviderTest):
|
||||
pass
|
||||
def test_is_runtime_available_raises(
|
||||
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):
|
||||
|
|
|
@ -335,6 +335,7 @@ class TestCliConversion(TestCliBasic):
|
|||
|
||||
class TestExtraFormats(TestCli):
|
||||
@for_each_external_doc("*hwp*")
|
||||
@pytest.mark.flaky(reruns=2)
|
||||
def test_hancom_office(self, doc: str) -> None:
|
||||
if is_qubes_native_conversion():
|
||||
pytest.skip("HWP / HWPX formats are not supported on this platform")
|
||||
|
|
BIN
tests/test_docs/sample-odt-mp4.odt
Normal file
BIN
tests/test_docs/sample-odt-mp4.odt
Normal file
Binary file not shown.
Loading…
Reference in a new issue