From dea5561ec6b068bb400a6dfa48645b44fd71581f Mon Sep 17 00:00:00 2001 From: Mattias Axelsson Date: Tue, 2 Apr 2024 13:23:22 +0200 Subject: [PATCH] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 1fbfdeeae76983998c560e9cb5c6545441315f8e Author: Stephen Garrett <114565398+stepheng-axis@users.noreply.github.com> Date: Tue Apr 2 10:28:24 2024 +0200 Allow multiple headers in certs (#162) * Refactor valid_cert() to allow multiple header addition. Change-Id: I13030e24ac1d4077b223b31535bab60f83ee94a7 * Refactor headers & footers. Add PRIVATE_KEY cert_type. Change-Id: I41f1145f46363bdeef96ed7a571c0f8fdbff5c3d * Allow multiple cert_types for uploaded TLS certificates. Change-Id: Ic4da466b3aa5d323b275d23a1ab61ccc86546df1 commit 69967148a1faa8c4d57d32ef7d372e05db7811e4 Author: Madelen Andersson Date: Mon Mar 25 16:13:04 2024 +0100 bump Docker Engine to 26.0.0 (#148) * bump Docker Engine to 26.0.0 --------- Co-authored-by: madelen-at-work commit 48e3971673cb5d6650d4de76f580a467308d75c6 Author: Madelen Andersson Date: Fri Mar 22 12:49:34 2024 +0100 TLS cert upload for rootless (#124) * First draft of tls upload * Preliminary functional version including documentation. * Resolve aarch64 compilation errors. Change-Id: I647ef17eeafff9269187051fd3baa8609cc70e6f * Corrections to logging and documentation following review. Change-Id: I694f419ec1e3d8670293b631fb465f0abf639c11 * Functional cert upload to /tmp, copy to ../localdata & cleanup. Change-Id: Ib0bd184a4a38d1f93b750ee932c902080d5aa0e7 * Intial restart on certificate functionality change to allow testing. Change-Id: I71f3d10918ee72c79e7b36948b1bfce5191dc301 * Refactor stop & start to load daemon. Enable pending cgi requests. Change-Id: I96869dd4eb1ed9c796e5a6fe4f813e88383f1cb5 * clang-formatted & logging reduced. Change-Id: Ica457ba1e2cd9cc473ab3bdb7c0cf3b5343a485e * Remove commented out lines from Dockerfile. Change-Id: I0d69febc0691e31d2ff4e5e959e3fa1a6f0dff26 --------- Co-authored-by: madelen-axis Co-authored-by: Stephen Garrett commit 6b39d9e9ff42c482121269403eda4d37620b0ea4 Author: Madelen Andersson Date: Thu Mar 21 09:06:10 2024 +0100 set path for internal storage (#138) Co-authored-by: madelen-at-work commit ff8055dee0c3c88e9ac9c1db458cd9c9f4d909c4 Author: Madelen Andersson Date: Thu Mar 7 13:41:29 2024 +0100 don't exclude .vscode commit 74ac4680acb673feea038249e327b3f42c0ed739 Merge: 1d829f9 5f7d2af Author: Madelen Andersson Date: Thu Mar 7 13:37:04 2024 +0100 Merge branch 'main' into rootless_shadow commit 5f7d2afc0ddff269aae7342024cc619d3a682da4 Author: Madelen Andersson Date: Thu Mar 7 13:31:46 2024 +0100 Add CONTRIBUTING and .vscode (#132) commit 1f42b2919998639b224bf0e8dfdaace64d2a0d44 Author: Madelen Andersson Date: Thu Mar 7 12:07:04 2024 +0100 combined update of depenadbot recomendations (#131) * combined changes for depenadbot and other action updates commit 1d829f9e9d922d96ccc68ea6bed43a10a6938037 Author: Madelen Andersson Date: Thu Mar 7 09:51:55 2024 +0100 fix for SDK change commit 5a076e8498edf45cf2214ed0344d370092983d10 Author: Madelen Andersson Date: Thu Mar 7 09:28:03 2024 +0100 tweaks after merge to main commit 96a83f7124242f4ebe4cf4b538dd1aec1c9868af Merge: 9642900 4781797 Author: Madelen Andersson Date: Thu Mar 7 09:25:33 2024 +0100 Merge branch 'main' into rootless_shadow commit 478179752693b922a337d2cee169d47ca7164a0a Author: Madelen Andersson Date: Thu Mar 7 09:19:38 2024 +0100 remove experimental codeql setup commit 964290064662cf816f675c43280584fbc1907dee Author: Madelen Andersson Date: Wed Mar 6 10:34:19 2024 +0100 Remove last root requirements (#130) * remove last root requirements NB! signing will not pass untill manifest schema is updated and available in SDK --------- Co-authored-by: madelen-axis commit 53082fa4623a1631934e611953e6c1d5f1ea9b3d Author: Deepika Shanmugam <92788697+deepikas20@users.noreply.github.com> Date: Mon Mar 4 13:33:51 2024 +0100 Remove the script of handling directories owned by root (#129) commit e7401a7218016117143625a9f8ae5f6babff867c Author: madelen-axis Date: Tue Feb 27 11:23:59 2024 +0100 fix to preuninstall script and remove unused binary commit 1d8fcbcb30c3190cf3988893f83b904a01919084 Author: Deepika Shanmugam <92788697+deepikas20@users.noreply.github.com> Date: Mon Feb 12 15:05:48 2024 +0100 Set required environment variables for rootless docker ACAP (#127) commit 1c92226437180c5be67a86a295ae4dcd82b082ac Author: Madelen Andersson Date: Thu Feb 8 11:47:55 2024 +0100 backdown SDK version to be LTS 10.12 compliant (#123) Co-authored-by: madelen-axis commit 0b18ef158376c609e297be379926f733f06dcfe3 Author: Angelo Delli Santi Date: Fri Jan 19 17:45:25 2024 +0100 Add note about root requirement (#125) * Add note about root requirement commit 3f6b6292964d4089362224f1230eaae98b9d919d Author: madelen-axis Date: Fri Jan 5 09:08:15 2024 +0100 remove new[u/g]idmap and user-services commit 6ed70c81dd2e3993bd132bbb23963505f279324b Author: Madelen Andersson Date: Thu Nov 30 15:54:50 2023 +0100 Added sub-groups for the ACAP user (#118) * Added sub-groups for the ACAP user --------- Co-authored-by: madelen-axis commit 44ead62c5a97197719cf30e73c934100e4663bd0 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Nov 20 07:23:52 2023 +0000 Bump actions/github-script from 6 to 7 Bumps [actions/github-script](https://github.com/actions/github-script) from 6 to 7. - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/github-script dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] commit c2bbc1bd9edff0c5096e3f8dd032dbb9469e9cf3 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Nov 13 07:35:23 2023 +0000 Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] commit 537f11e09d193326fe6262482c62e418ba489f65 Author: Madelen Andersson Date: Fri Nov 24 10:13:23 2023 +0100 Use super-linter/super-linter and activate clang validation (#110) * switched to super-linter/super-linter * Update lint.yml --------- Co-authored-by: madelen-axis commit 05c8c256fa57b7e9bd6104b9080818ad29d8fa54 Author: Madelen Andersson Date: Fri Nov 10 09:54:59 2023 +0100 Documentation for rootless preview (#109) * Added documentation for rootless Docker ACAP Co-authored-by: madelen-axis commit 313a74d000929d5f0957d4f271182570f83e94ef Author: Madelen Andersson Date: Fri Nov 10 08:46:56 2023 +0100 rootless Docker ACAP requiring AllowRoot to install (#107) rootless implementation --------- Co-authored-by: madelen-axis Co-authored-by: Mattias Axelsson commit 1a53e5c3881ff4fa5fe1a369ff46c63ebfe1c634 Author: Patrik Åkesson <66364872+pataxis@users.noreply.github.com> Date: Wed Nov 8 13:30:35 2023 +0100 Correct codeql.yml GitHub action format commit c4b2ab9b1f49151b8430732fe6793411411eda29 Author: Patrik Åkesson <66364872+pataxis@users.noreply.github.com> Date: Wed Nov 8 13:23:47 2023 +0100 Correct codeql.yml wrong yaml syntax commit 218e50db4e592c8e0af93f00c8158c40278e3e40 Author: Patrik Åkesson <66364872+pataxis@users.noreply.github.com> Date: Wed Nov 8 09:54:49 2023 +0100 Update codeql.yml with custom build script commit 8f033eafb87c513d06ec127a28c4c8605d3af9e5 Author: Patrik Åkesson <66364872+pataxis@users.noreply.github.com> Date: Wed Nov 8 09:31:16 2023 +0100 Create codeql.yml with manual trigger --- .dockerignore | 1 + .../actions/docker-build-action/action.yml | 6 +- .github/actions/metadata-action/action.yml | 2 +- .github/workflows/cd.yml | 8 +- .github/workflows/lint.yml | 3 +- .vscode/extensions.json | 7 + .vscode/settings.json | 17 + CONTRIBUTING.md | 225 ++++ Dockerfile | 162 ++- README.md | 312 ++++- app/Makefile | 10 +- app/dockerdwrapper.c | 1064 ++++++++++++++--- app/fastcgi_cert_manager.c | 420 +++++++ app/fastcgi_cert_manager.h | 14 + app/manifest.json | 39 +- app/postinstallscript.sh | 22 +- build.sh | 23 +- 17 files changed, 2062 insertions(+), 273 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 CONTRIBUTING.md create mode 100644 app/fastcgi_cert_manager.c create mode 100644 app/fastcgi_cert_manager.h diff --git a/.dockerignore b/.dockerignore index 820e358..b95d2cc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,3 @@ build-* tmp +.vscode diff --git a/.github/actions/docker-build-action/action.yml b/.github/actions/docker-build-action/action.yml index 79b389d..72a5e4b 100644 --- a/.github/actions/docker-build-action/action.yml +++ b/.github/actions/docker-build-action/action.yml @@ -53,12 +53,12 @@ runs: using: composite steps: - name: Set up Docker buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Set up QEMU if: ${{ inputs.use_qemu == 'true'}} - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Build image - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: context: . push: false diff --git a/.github/actions/metadata-action/action.yml b/.github/actions/metadata-action/action.yml index 78e44d1..2138e8c 100644 --- a/.github/actions/metadata-action/action.yml +++ b/.github/actions/metadata-action/action.yml @@ -44,7 +44,7 @@ runs: steps: - name: Create metadata for docker image id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: ${{ inputs.repository }} # adds the suffix for all tags, even latest. diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 0f1f747..3559a31 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -8,6 +8,7 @@ on: push: branches: - 'main' + - 'rootless_shadow' tags: # semver, e.g. 1.2.0 (does not match 0.1.2) - '[1-9]+.[0-9]+.[0-9]+' @@ -22,6 +23,7 @@ on: pull_request: branches: - 'main' + - 'rootless_shadow' jobs: # Builds docker ACAP using the build.sh script, then signs the eap-file in @@ -91,7 +93,7 @@ jobs: echo "HTTP_RESPONSE is empty or not a valid integer: $HTTP_RESPONSE" fi - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.SIGNED_EAP_FILE }} path: build/${{ env.SIGNED_EAP_FILE }} @@ -123,7 +125,7 @@ jobs: id: vars run: echo "TAG=${GITHUB_REF#refs/*/}" >> ${GITHUB_ENV} - name: Create prerelease - uses: actions/github-script@v6 + uses: actions/github-script@v7 id: prerelease env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -173,7 +175,7 @@ jobs: echo "::error::Non valid architecture '${{ matrix.arch }}' encountered" fi - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: ${{ env.EAP_FILE }} path: ./ diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fdcd140..667f2b2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,7 +16,7 @@ jobs: fetch-depth: 0 - name: Lint codebase - uses: github/super-linter/slim@v5 + uses: super-linter/super-linter/slim@v6 env: VALIDATE_ALL_CODEBASE: true DEFAULT_BRANCH: main @@ -24,6 +24,7 @@ jobs: LINTER_RULES_PATH: / IGNORE_GITIGNORED_FILES: true VALIDATE_BASH: true + VALIDATE_CLANG_FORMAT: true VALIDATE_DOCKERFILE_HADOLINT: true VALIDATE_MARKDOWN: true VALIDATE_SHELL_SHFMT: true diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..e63e6f4 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "DavidAnson.vscode-markdownlint", + "editorconfig.editorconfig", + "streetsidesoftware.code-spell-checker" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0c554db --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "C_Cpp.clang_format_style": "file", + "[markdown]": { + "editor.defaultFormatter": "DavidAnson.vscode-markdownlint", + "editor.formatOnSave": true, + "editor.formatOnPaste": true + }, + "markdown.extension.list.indentationSize": "inherit", + "markdown.extension.toc.levels": "1..3", + "cSpell.words": [ + "anyauth", + "Buildx", + "containerd", + "rootpasswd", + "VAPIX" + ] +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3a0727a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,225 @@ + +# Regarding contributions + +All types of contributions are encouraged and valued. See the [Table of contents](#table-of-contents) +for different ways to help and details about how this project handles them. Please make sure to read +the relevant section before making your contribution. It will make it a lot easier for us maintainers +and smooth out the experience for all involved. We look forward to your contributions. + +> And if you like the project, but just don't have time to contribute, that's fine. There are other +> easy ways to support the project and show your appreciation, which we would also be very happy about: +> +> - Star the project +> - Tweet about it +> - Refer this project in your project's readme +> - Mention the project at local meetups and tell your friends/colleagues + + +## Table of contents + +- [I have a question](#i-have-a-question) +- [I want to contribute](#i-want-to-contribute) + - [Reporting bugs](#reporting-bugs) + - [Suggesting enhancements](#suggesting-enhancements) + - [Your first code contribution](#your-first-code-contribution) + - [Lint of codebase](#lint-of-codebase) + +## I have a question + +Before you ask a question, it is best to search for existing [issues][issues] that might help you. +In case you have found a suitable issue and still need clarification, you can write your question in +this issue. It is also advisable to search the internet for answers first. + +If you then still feel the need to ask a question and need clarification, please +follow the steps in [Reporting bugs](#reporting-bugs). + +## I want to contribute + +### Reporting bugs + +#### Before submitting a bug report + +A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we +ask you to investigate carefully, collect information and describe the issue in detail in your report. +Please complete the following steps in advance to help us fix any potential bug as fast as possible: + +- Make sure that you are using the latest version. +- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment + components/versions. +- To see if other users have experienced (and potentially already solved) the same issue you are having, + check if there is not already a bug report existing for your bug or error in the [bug tracker][issues_bugs]. +- Also make sure to search the internet to see if users outside of the GitHub community have discussed + the issue. +- Collect information about the bug: + - Axis device model + - Axis device firmware version + - Stack trace + - OS and version (Windows, Linux, macOS, x86, ARM) + - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what + seems relevant + - Possibly your input and the output + - Can you reliably reproduce the issue? And can you also reproduce it with older versions? + +#### How do I submit a good bug report? + +We use GitHub issues to track bugs and errors. If you run into an issue with the project: + +- Open an [issue][issues_new]. +- Explain the behavior you would expect and the actual behavior. +- Please provide as much context as possible and describe the *reproduction steps* that someone else + can follow to recreate the issue on their own. +- Provide the information you collected in the previous section. + +Once it's filed: + +- The project team will label the issue accordingly. +- A team member will try to reproduce the issue with your provided steps. If there are no reproduction + steps or no obvious way to reproduce the issue, the team will ask you for those steps. Bugs without + steps will not be addressed until they can be reproduced. +- If the team is able to reproduce the issue, it will be prioritized according to severity. + +### Suggesting enhancements + +This section guides you through submitting an enhancement suggestion, +**including completely new features and minor improvements to existing functionality**. +Following these guidelines will help maintainers and the community to understand your suggestion and +find related suggestions. + +#### Before Submitting an Enhancement + +- Make sure that you are using the latest version. +- Read the documentation carefully and find out if the functionality is already covered, maybe by an + individual configuration. +- Perform a [search][issues] to see if the enhancement has already been suggested. If it has, add a + comment to the existing issue instead of opening a new one. +- Find out whether your idea fits with the scope and aims of the project. Keep in mind that we want + features that will be useful to the majority of our users and not just a small subset. + +#### How do I submit a good enhancement suggestion? + +Enhancement suggestions are tracked as [GitHub issues][issues]. + +- Use a **clear and descriptive title** for the issue to identify the suggestion. +- Provide a **step-by-step description of the suggested enhancement** in as many details as possible. +- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. + At this point you can also tell which alternatives do not work for you. +- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or + point out the part which the suggestion is related to. +- **Explain why this enhancement would be useful** to most users. You may also want to point out the + other projects that solved it better and which could serve as inspiration. + +### Your first code contribution + +Start by [forking the repository](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo), +i.e. copying the repository to your account to grant you write access. Continue with cloning the +forked repository to your local machine. + +```sh +git clone https://github.com//AxisCommunications/docker-acap.git +``` + +Navigate into the cloned directory and create a new branch: + +```sh +cd docker-acap +git switch -c +``` + +Update the code according to your requirements, and commit the changes using the +[conventional commits](https://www.conventionalcommits.org) message style: + +```sh +git commit -a -m 'Follow the conventional commit messages style to write this message' +``` + +Continue with pushing the local commits to GitHub: + +```sh +git push origin +``` + +Before opening a Pull Request (PR), please consider the following guidelines: + +- Please make sure that the code builds perfectly fine on your local system. +- Make sure that all linters pass, see [Lint of codebase](#lint-of-codebase) +- The PR will have to meet the code standard already available in the repository. +- Explanatory comments related to code functions are required. Please write code comments for a better + understanding of the code for other developers. +- Note that code changes or additions to the `.github` folder (or sub-folders) will not be accepted. + +And finally when you are satisfied with your changes, open a new PR. + +### Lint of codebase + +A set of different linters test the codebase and these must pass in order to get a pull request approved. + +#### Linters in GitHub Action + +When you create a pull request, a set of linters will run syntax and format checks on different file +types in GitHub actions by making use of a tool called [super-linter][super-linter]. If any of the +linters gives an error, this will be shown in the action connected to the pull request. + +In order to speed up development, it's possible to run linters as part of your local development environment. + +#### Run super-linter locally + +Since super-linter is using a Docker image in GitHub Actions, users of other editors may run it locally +to lint the codebase. For complete instructions and guidance, see super-linter page for [running locally][super-linter-local]. + +To run a number of linters on the codebase from command line: + +```sh +docker run --rm \ + -v $PWD:/tmp/lint \ + -e RUN_LOCAL=true \ + -e LINTER_RULES_PATH=/ \ + -e VALIDATE_BASH=true \ + -e VALIDATE_DOCKERFILE_HADOLINT=true \ + -e VALIDATE_MARKDOWN=true \ + -e VALIDATE_SHELL_SHFMT=true \ + -e VALIDATE_YAML=true \ + ghcr.io/super-linter/super-linter:slim-v6 +``` + +See [`.github/workflows/lint.yml`](.github/workflows/lint.yml) for the exact setup used by this project. + +#### Run super-linter interactively + +It might be more convenient to run super-linter interactively. Run container and enter command line: + +```sh +docker run --rm \ + -v $PWD:/tmp/lint \ + -w /tmp/lint \ + --entrypoint /bin/bash \ + -it ghcr.io/super-linter/super-linter:slim-v6 +``` + +Then from the container terminal, the following commands can lint the the code base for different +file types: + +```sh +# Lint Dockerfile files +hadolint $(find -type f -name "Dockerfile*") + +# Lint Markdown files +markdownlint . + +# Lint YAML files +yamllint . + +# Lint shell script files +shellcheck $(shfmt -f .) +shfmt -d . +``` + +To lint only a specific file, replace `.` or `$(COMMAND)` with the file path. + + +[issues]: https://github.com/AxisCommunications/docker-acap/issues +[issues_new]: https://github.com/AxisCommunications/docker-acap/issues/new +[issues_bugs]: https://github.com/AxisCommunications/docker-acap/issues?q=label%3Abug +[super-linter]: https://github.com/super-linter/super-linter +[super-linter-local]: https://github.com/super-linter/super-linter/blob/main/docs/run-linter-locally.md + + diff --git a/Dockerfile b/Dockerfile index b79f3dc..29d65a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -ARG DOCKER_IMAGE_VERSION=24.0.2 +ARG DOCKER_IMAGE_VERSION=26.0.0 ARG REPO=axisecp ARG ACAPARCH=armv7hf @@ -13,14 +13,9 @@ ARG ACAP3_SDK_VERSION=3.5 ARG ACAP3_UBUNTU_VERSION=20.04 ARG ACAP3_SDK=acap-sdk -FROM ${REPO}/${NATIVE_SDK}:${VERSION}-${ACAPARCH}-ubuntu${UBUNTU_VERSION} as build_image - FROM ${REPO}/${ACAP3_SDK}:${ACAP3_SDK_VERSION}-${ACAPARCH}-ubuntu${ACAP3_UBUNTU_VERSION} as acap-sdk -FROM build_image AS ps -ARG PROCPS_VERSION=v3.3.17 -ARG BUILD_DIR=/build -ARG EXPORT_DIR=/export +FROM ${REPO}/${NATIVE_SDK}:${VERSION}-${ACAPARCH}-ubuntu${UBUNTU_VERSION} as build_image RUN <$BUILD_CACHE \ + && echo ac_cv_func_malloc_0_nonnull=yes >>$BUILD_CACHE +RUN <$BUILD_CACHE \ - && echo ac_cv_func_malloc_0_nonnull=yes >>$BUILD_CACHE + && echo ac_cv_func_malloc_0_nonnull=yes >>$BUILD_CACHE RUN < # The Docker ACAP -This is the ACAP packaging of the Docker Engine to be run on Axis devices with container support. - -## Compatibility - -### Device - -The Docker ACAP requires a container capable device. You may check the compatibility of your device -by running: +The Docker ACAP application provides the means to run rootless Docker on a compatible Axis +device. + +> **Note** +> +> This is a preview of the rootless Docker ACAP. Even though it uses a non-root user at runtime, +> it requires root privileges during installation and uninstallation. This can be accomplished by +> setting the `AllowRoot` toggle to `true` when installing and uninstalling the application. +> See the [VAPIX documentation][vapix-allow-root] for details. +> +> **Known Issues** +> +> * Only uid and gid are properly mapped between device and containers, not the other groups that +> the user is a member of. This means that resources on the device, even if they are volume or device +> mounted can be inaccessible inside the container. This can also affect usage of unsupported dbus +> methods from the container. See [Using host user secondary groups in container](#using-host-user-secondary-groups-in-container) +> for how to handle this. +> * iptables use is disabled. + + +## Table of contents + +* [Overview](#overview) +* [Requirements](#requirements) +* [Installation and Usage](#installation-and-usage) + * [Installation](#installation) + * [Securing the Docker ACAP using TLS](#securing-the-docker-acap-using-tls) + * [Using an SD card as storage](#using-an-sd-card-as-storage) + * [Using the Docker ACAP](#using-the-docker-acap) +* [Building the Docker ACAP](#building-the-docker-acap) +* [Installing a locally built Docker ACAP](#installing-a-locally-built-docker-acap) +* [Contributing](#contributing) +* [License](#license) + +## Overview + +The Docker ACAP provides the means to run a Docker daemon on an Axis device, thereby +making it possible to deploy and run Docker containers on it. When started the daemon +will run in rootless mode, i.e. the user owning the daemon process will not be root, +and in extension, the containers will not have root access to the host system. +See [Rootless Mode][docker-rootless-mode] on Docker.com for details. That page also +contains known limitations when running rootless Docker. + +> **Note** +> +> The Docker ACAP application can be run with TLS authentication or without. +> Be aware that running without TLS authentication is extremely insecure and we +strongly recommend against this. +> See [Securing the Docker ACAP using TLS](#securing-the-docker-acap-using-tls) +for information on how to generate certificates for TLS authentication when using +the Docker ACAP application. + +## Requirements + +The following requirements need to be met. + +* Axis device: + * Axis OS version 11.10 or higher. + * The device needs to have ACAP Native SDK support. See [Axis devices & compatibility][devices] + for more information. + * Additionally, the device must be container capable. To check the compatibility + of your device run: ```sh DEVICE_IP= @@ -22,22 +77,33 @@ ssh root@$DEVICE_IP 'command -v containerd >/dev/null 2>&1 && echo Compatible wi where `` is the IP address of the Axis device and `` is the root password. Please note that you need to enclose your password with quotes (`'`) if it contains special characters. -### Host - -The host machine is required to have [Docker](https://docs.docker.com/get-docker/) and -[Docker Compose](https://docs.docker.com/compose/install/) installed. To build Docker ACAP locally -it is required to also have [Buildx](https://docs.docker.com/build/install-buildx/) installed. +* Computer: + * Either [Docker Desktop][dockerDesktop] version 4.11.1 or higher, or + [Docker Engine][dockerEngine] version 20.10.17 or higher. + * To build Docker ACAP locally it is required to have [Buildx][buildx] installed. -## Installing +## Installation and Usage The Docker ACAP application is available as a **signed** eap-file in [Releases][latest-releases]. +> [!IMPORTANT] +> From AXIS OS 11.8 `root` user is not allowed by default and in 12.0 it will be disallowed completely. Read more on the [Developer Community](https://www.axis.com/developer-community/news/axis-os-root-acap-signing). \ +> Docker ACAP 1.X requires root and work is ongoing to create a version that does not. +> Meanwhile, the solution is to allow root to be able to install the Docker ACAP. +> +> On the web page of the device: +> +> 1. Go to the Apps page, toggle on `Allow root-privileged apps`. +> 1. Go to System -> Account page, under SSH accounts toggle off `Restrict root access` to be able to send the TLS certificates. Make sure to set the password of the `root` SSH user. + The prebuilt Docker ACAP application is signed, read more about signing [here][signing-documentation]. -Install and use any image from [prereleases or releases][all-releases] with +### Installation + +Install and use any signed eap-file from [prereleases or releases][all-releases] with a tag on the form `_`, where `` is the docker-acap release version and `` is either `armv7hf` or `aarch64` depending on device architecture. -E.g. `Docker_Daemon_1_3_0_aarch64_signed.eap`. +E.g. `Docker_Daemon_2_0_0_aarch64_signed.eap`. The eap-file can be installed as an ACAP application on the device, where it can be controlled in the device GUI **Apps** tab. @@ -47,33 +113,20 @@ where it can be controlled in the device GUI **Apps** tab. curl -s https://api.github.com/repos/AxisCommunications/docker-acap/releases/latest | grep "browser_download_url.*Docker_Daemon_.*_\_signed.eap" ``` -### Installation of version 1.3.0 and previous - -To install this ACAP for version 1.3.0 or previous use the pre-built -[docker hub](https://hub.docker.com/r/axisecp/docker-acap) image: - -```sh -docker run --rm axisecp/docker-acap:latest- install -``` - -Where `` is either `armv7hf` or `aarch64` depending on device architecture. - -It's also possible to build and use a locally built image. See the -[Building the Docker ACAP](#building-the-docker-acap) section for more information. - -## Securing the Docker ACAP using TLS +### Securing the Docker ACAP using TLS The Docker ACAP can be run either unsecured or in TLS mode. The Docker ACAP uses TLS as default. Use the "Use TLS" dropdown in the web interface to switch between the two different modes. It's also possible to toggle this option by -calling the parameter management API in [VAPIX](https://www.axis.com/vapix-library/) and setting the +calling the parameter management API in [VAPIX][vapix] and setting the `root.dockerdwrapper.UseTLS` parameter to `yes` or `no`. The following commands would enable TLS: ```sh -DEVICE_IP= -DEVICE_PASSWORD='' +export DEVICE_IP= +export DEVICE_USER='' +export DEVICE_PASSWORD='' -curl -s --anyauth -u "root:$DEVICE_PASSWORD" \ +curl -s --anyauth --user $DEVICE_USER:$DEVICE_PASSWORD \ "http://$DEVICE_IP/axis-cgi/param.cgi?action=update&root.dockerdwrapper.UseTLS=yes" ``` @@ -81,38 +134,78 @@ Note that the dockerd service will be restarted every time TLS is activated or deactivated. Running the ACAP using TLS requires some additional setup, see next chapter. Running the ACAP without TLS requires no further setup. -### TLS Setup +#### TLS Setup -TLS requires a few keys and certificates to work, which are listed in the +TLS requires keys and certificates to work, which are listed in the subsections below. For more information on how to generate these files, please -consult the official [Docker documentation](https://docs.docker.com/engine/security/protect-access/). -Most of these keys and certificates need to be moved to the Axis device. There are multiple ways to +consult the official [Docker documentation][docker-tls]. +Some of these keys and certificates need to be moved to the Axis device. Currently there are multiple ways to achieve this, for example by using `scp` to copy the files from a remote machine onto the device. This can be done by running the following command on the remote machine: ```sh -scp ca.pem server-cert.pem server-key.pem root@:/usr/local/packages/dockerdwrapper/ +scp ca.pem server-cert.pem server-key.pem root@$DEVICE_IP:/usr/local/packages/dockerdwrapper/localdata/ ``` -#### The Certificate Authority (CA) certificate +Once copied to the Axis device the correct permissions need to be set for the certificates: -This certificate needs to be present in the dockerdwrapper package folder on the +```sh +ssh root@$DEVICE_IP 'chown acap-dockerdwrapper /usr/local/packages/dockerdwrapper/localdata/*.pem' + +``` + +When root ssh access is removed from the device (Axis OS version 12.0 and higher) the preferred method is to use the cert_manager.cgi packaged with the ACAP to install the required keys and certificates on the device with the relevant priviledges. A user with admin rights is required in conjunction with the cert_manager.cgi. + +Three HTTP request methods are associated with the cert_manager.cgi, GET, POST and DELETE. GET is used only for accessing help or usage information. POST is used to upload keys or certificates to the device, whereas DELETE is used to remove them. The name of the key or certifcate must be supplied when POST'ing or DELETE'ing. + +GET may be used to confirm valid names and example cURL usage. The cert_manager.cgi usage and help can be obtained from your host machine as follows + +```sh +export DEVICE_IP= +export DEVICE_USER='' +export DEVICE_PASSWORD='' + +curl --anyauth --user $DEVICE_USER:$DEVICE_PASSWORD -X GET "http://$DEVICE_IP/local/dockerdwrapper/cert_manager.cgi" +``` + +A note on where the keys and certificates are stored on the device. Older versions of the ACAP simply stored them in `/usr/local/packages/dockerdwrapper/`. As a consequence any certificates were not preserved on ACAP upgrade. + +Newer versions of the ACAP and the cert:manager.cgi instead place them in `/usr/local/packages/dockerdwrapper/localdata` which will be backed up and restored as appropriate during subsequent upgrade of the ACAP. + +The ACAP expects and requires keys and certificates to be owned by the user `acap-dockerdwrapper` and will not start in TLS mode if it detects that this is not the case. + + +##### The Certificate Authority (CA) certificate + +This certificate needs to be present in the dockerdwrapper package localdata folder on the Axis device and be named `ca.pem`. The full path of the file should be -`/usr/local/packages/dockerdwrapper/ca.pem`. +`/usr/local/packages/dockerdwrapper/localdata/ca.pem`. The cert_manager.cgi can be used to achieve this from your host machine as follows. + +```sh +curl --anyauth --user $DEVICE_USER:$DEVICE_PASSWORD -F file=@ca.pem -X POST "http://$DEVICE_IP/local/dockerdwrapper/cert_manager.cgi?file_name=ca.pem" +``` -#### The server certificate +##### The server certificate This certificate needs to be present in the dockerdwrapper package folder on the Axis device and be named `server-cert.pem`. The full path of the file should be -`/usr/local/packages/dockerdwrapper/server-cert.pem`. +`/usr/local/packages/dockerdwrapper/localdata/server-cert.pem`. The cert_manager.cgi can be used to achieve this from your host machine as follows. + +```sh +curl --anyauth --user $DEVICE_USER:$DEVICE_PASSWORD -F file=@server-cert.pem -X POST "http://$DEVICE_IP/local/dockerdwrapper/cert_manager.cgi?file_name=server-cert.pem" +``` -#### The private server key +##### The private server key This key needs to be present in the dockerdwrapper package folder on the Axis device and be named `server-key.pem`. The full path of the file should be -`/usr/local/packages/dockerdwrapper/server-key.pem`. +`/usr/local/packages/dockerdwrapper/localdata/server-key.pem`. The cert_manager.cgi can be used to achieve this from your host machine as follows. -#### Client key and certificate +```sh +curl --anyauth --user $DEVICE_USER:$DEVICE_PASSWORD -F file=@server-key.pem -X POST "http://$DEVICE_IP/local/dockerdwrapper/cert_manager.cgi?file_name=server-key.pem" +``` + +##### Client key and certificate A client will need to have its own private key, together with a certificate authorized by the CA. Key, certificate and CA shall be used when running Docker against the dockerd daemon on @@ -122,9 +215,9 @@ the Axis device. See below for an example: DOCKER_PORT=2376 docker --tlsverify \ --tlscacert=ca.pem \ - --tlscert=cert.pem \ - --tlskey=key.pem \ - -H=:$DOCKER_PORT \ + --tlscert=client-cert.pem \ + --tlskey=client-key.pem \ + --host tcp://$DEVICE_IP:$DOCKER_PORT \ version ``` @@ -135,14 +228,14 @@ automatically use your key and certificate, please export the `DOCKER_CERT_PATH` export DOCKER_CERT_PATH= DOCKER_PORT=2376 docker --tlsverify \ - -H=:$DOCKER_PORT \ + --host tcp://$DEVICE_IP:$DOCKER_PORT \ version ``` where `` is the directory on your computer where the files `ca.pem`, -`cert.pem` and `key.pem` are stored. +`client-cert.pem` and `client-key.pem` are stored. -## Using an SD card as storage +### Using an SD card as storage An SD card might be necessary to run the Docker ACAP correctly. Docker containers and docker images can be quite large, and putting them on an SD card @@ -150,7 +243,7 @@ gives more freedom in how many and how large images can be stored. Switching between storage on the SD card or internal storage is done by toggling the "SD card support" dropdown in the web interface. It's also possible to toggle this option by calling the parameter management API in -[VAPIX](https://www.axis.com/vapix-library/) (accessing this documentation +[VAPIX][vapix] (accessing this documentation requires creating a free account) and setting the `root.dockerdwrapper.SDCardSupport` parameter to `yes` or `no`. @@ -165,12 +258,20 @@ example if the original file system of the SD card is vfat. Make sure to use an SD card that has enough capacity to hold your applications. Other properties of the SD card, like the speed, might also affect the performance of your applications. For example, the Computer Vision SDK example -[object-detector-python](https://github.com/AxisCommunications/acap-computer-vision-sdk-examples/tree/main/object-detector-python) +[object-detector-python][object-detector-python] has a significantly higher inference time when using a small and slow SD card. To get more informed about specifications, check the -[SD Card Standards](https://www.sdcard.org/developers/sd-standard-overview/). +[SD Card Standards][sd-card-standards]. + +>**Note** +> +>If Docker ACAP v1.4 or previous has been used on the device with SD card as storage +>the storage directory might already be created with root permissions. +>Since v2.0 the Docker ACAP is run in rootless mode and it will then not be able +>to access that directory. To solve this, either reformat the SD card or manually +>remove the directory that is used by the Docker ACAP. -## Using the Docker ACAP +### Using the Docker ACAP The Docker ACAP does not contain the docker client binary. This means that all calls need to be done from a separate machine. This can be achieved by using @@ -186,13 +287,85 @@ the Docker ACAP in unsecured mode: ```sh DOCKER_INSECURE_PORT=2375 -docker -H=:$DOCKER_INSECURE_PORT version +docker --host tcp://$DEVICE_IP:$DOCKER_INSECURE_PORT version ``` See [Client key and certificate](#client-key-and-certificate) for an example of how to remotely run docker commands on a device running a secured Docker ACAP using TLS. +#### Test that the Docker ACAP can run a container + +Make sure the Docker ACAP, using TLS, is running, then pull and run the +[hello-world][docker-hello-world] image from Docker Hub: + +```sh +>docker --tlsverify --host tcp://$DEVICE_IP:$DOCKER_PORT pull hello-world +Using default tag: latest +latest: Pulling from library/hello-world +70f5ac315c5a: Pull complete +Digest: sha256:88ec0acaa3ec199d3b7eaf73588f4518c25f9d34f58ce9a0df68429c5af48e8d +Status: Downloaded newer image for hello-world:latest +docker.io/library/hello-world:latest +>docker --tlsverify --host tcp://$DEVICE_IP:$DOCKER_PORT run hello-world + +Hello from Docker! +This message shows that your installation appears to be working correctly. + +To generate this message, Docker took the following steps: + 1. The Docker client contacted the Docker daemon. + 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. + (arm64v8) + 3. The Docker daemon created a new container from that image which runs the + executable that produces the output you are currently reading. + 4. The Docker daemon streamed that output to the Docker client, which sent it + to your terminal. + +To try something more ambitious, you can run an Ubuntu container with: + $ docker run -it ubuntu bash + +Share images, automate workflows, and more with a free Docker ID: + https://hub.docker.com/ + +For more examples and ideas, visit: + https://docs.docker.com/get-started/ + +``` + +#### Loading images onto a device + +If you have images in a local repository that you want to transfer to a device, or +if you have problems getting the `pull` command to work in your environment, `save` +and `load` can be used. + +```sh +docker save | docker --tlsverify --host tcp://$DEVICE_IP:$DOCKER_PORT load +``` + +#### Using host user secondary groups in container + +The Docker Compose ACAP is run by a non-root user on the device. This user is set +up to be a member in a number of secondary groups as listed in the +[manifest.json](https://github.com/AxisCommunications/docker-compose-acap/blob/rootless-preview/app/manifest.json#L6-L11) +file. When running a container a user called `root`, (uid 0), belonging to group `root`, (gid 0) +will be the default user inside the container. It will be mapped to the non-root user on +the device, and the group will be mapped to the non-root users primary group. +In order to get access inside the container to resources on the device that are group owned by any +of the non-root users secondary groups, these need to be added for the container user. +This can be done by using `group_add` in a docker-compose.yaml (`--group-add` if using Docker cli). +Unfortunately, adding the name of a secondary group is not supported. Instead the *mapped* id +of the group need to be used. At the moment of writing this the mappings are: + +| device group | container group id | +| ------------ | ------------------ | +| datacache | "1" | +| sdk | "2" | +| storage | "3" | +| vdo | "4" | +| optics | "5" | + +Note that the names of the groups will not be correctly displayed inside the container. + ## Building the Docker ACAP To build the Docker ACAP use docker buildx with the provided Dockerfile: @@ -205,6 +378,7 @@ docker buildx build --file Dockerfile --tag docker-acap: --build-arg ACAPA where `` is either `armv7hf` or `aarch64`. To extract the Docker ACAP eap-file use docker cp to copy it to a `build` folder: + ```sh docker cp "$(docker create "docker-acap:")":/opt/app/ ./build ``` @@ -227,10 +401,28 @@ Go to your device web page above > Click on the tab **App** in the device GUI > Add **(+)** sign and browse to the newly built .eap-file > Click **Install** > Run the application by enabling the **Start** switch. +## Contributing + +Take a look at the [CONTRIBUTING.md](CONTRIBUTING.md) file. + +## License + +[Apache 2.0](LICENSE) + [all-releases]: https://github.com/AxisCommunications/docker-acap/releases +[buildx]: https://docs.docker.com/build/install-buildx/ +[devices]: https://axiscommunications.github.io/acap-documentation/docs/axis-devices-and-compatibility#sdk-and-device-compatibility +[dockerDesktop]: https://docs.docker.com/desktop/ +[dockerEngine]: https://docs.docker.com/engine/ +[docker-hello-world]: https://hub.docker.com/_/hello-world +[docker-tls]: https://docs.docker.com/engine/security/protect-access/ +[docker-rootless-mode]: https://docs.docker.com/engine/security/rootless/ [latest-releases]: https://github.com/AxisCommunications/docker-acap/releases/latest +[object-detector-python]: https://github.com/AxisCommunications/acap-computer-vision-sdk-examples/tree/main/object-detector-python +[sd-card-standards]: https://www.sdcard.org/developers/sd-standard-overview/ [signing-documentation]: https://axiscommunications.github.io/acap-documentation/docs/faq/security.html#sign-acap-applications - +[vapix]: https://www.axis.com/vapix-library/ +[vapix-allow-root]: https://www.axis.com/vapix-library/subjects/t10102231/section/t10036126/display?section=t10036126-t10185050 diff --git a/app/Makefile b/app/Makefile index d2ffd3c..11f8dab 100644 --- a/app/Makefile +++ b/app/Makefile @@ -1,10 +1,16 @@ PROG1 = dockerdwrapper -OBJS1 = $(PROG1).c +OBJS1 = $(PROG1).c fastcgi_cert_manager.c -PKGS = gio-2.0 glib-2.0 axparameter +PKGS = gio-2.0 glib-2.0 axparameter fcgi CFLAGS += $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) pkg-config --cflags $(PKGS)) LDLIBS += $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) pkg-config --libs $(PKGS)) +# Link the built library +LDFLAGS = -L./lib -Wl,--no-as-needed,-rpath,'$$ORIGIN/lib' +CFLAGS += -I/opt/build/uriparser/build/include +LDLIBS += -luriparser + +CFLAGS += -D_FORTIFY_SOURCE=2 -Wformat -Wformat-security -Werror=format-security CFLAGS += -W -Wformat=2 -Wpointer-arith -Wbad-function-cast -Wstrict-prototypes \ -Wmissing-prototypes -Winline -Wdisabled-optimization -Wfloat-equal -Wall -Werror \ -Wno-unused-variable diff --git a/app/dockerdwrapper.c b/app/dockerdwrapper.c index 0a85693..508918b 100644 --- a/app/dockerdwrapper.c +++ b/app/dockerdwrapper.c @@ -14,16 +14,47 @@ * under the License. */ +#include #include #include +#include +#include +#include #include +#include #include +#include +#include #include #include +#include #include +#include #include #include #include +#include "fastcgi_cert_manager.h" + +#define syslog_v(...) \ + if (g_verbose) { \ + syslog(__VA_ARGS__); \ + } + +static bool g_verbose = false; + +/** + * @brief Callback called when a well formed fcgi request is received. + */ +void callback_action(__attribute__((unused)) fcgi_handle handle, + int request_method, + char *cert_name, + char *file_path); + +#define APP_NAME "dockerdwrapper" + +/** @brief APP paths in a device */ +#define APP_DIRECTORY "/usr/local/packages/" APP_NAME +#define APP_LOCALDATA "/usr/local/packages/" APP_NAME "/localdata" /** * @brief Callback called when the dockerd process exits. @@ -43,13 +74,215 @@ static int exit_code = 0; static pid_t dockerd_process_pid = -1; // Full path to the SD card -static const char *sd_card_path = "/var/spool/storage/SD_DISK"; - -// True if the dockerd_exited_callback should restart dockerd -static bool restart_dockerd = false; +#define SD_CARD_PATH "/var/spool/storage/SD_DISK" +static const char *sd_card_path = SD_CARD_PATH; // All ax_parameters the acap has -static const char *ax_parameters[] = {"IPCSocket", "SDCardSupport", "UseTLS"}; +static const char *ax_parameters[] = {"IPCSocket", + "SDCardSupport", + "UseTLS", + "Verbose"}; + +static const char *tls_cert_path = APP_LOCALDATA; + +typedef enum { + PEM_CERT = 0, + PRIVATE_KEY, + RSA_PRIVATE_KEY, + NUM_CERT_TYPES +} cert_types; + +typedef struct { + const char *header; + const char *footer; +} cert_spec; + +static const cert_spec cert_specs[NUM_CERT_TYPES] = { + {"-----BEGIN CERTIFICATE-----\n", "-----END CERTIFICATE-----\n"}, + {"-----BEGIN PRIVATE KEY-----\n", "-----END PRIVATE KEY-----\n"}, + {"-----BEGIN RSA PRIVATE KEY-----\n", "-----END RSA PRIVATE KEY-----\n"}}; + +typedef struct { + const char *name; + int *type; +} cert; + +static int ALL_CERTS[] = {PEM_CERT, -1}; +static int ALL_KEYS[] = {PRIVATE_KEY, RSA_PRIVATE_KEY, -1}; + +static cert tls_certs[] = {{"ca.pem", ALL_CERTS}, + {"server-cert.pem", ALL_CERTS}, + {"server-key.pem", ALL_KEYS}}; + +#define NUM_TLS_CERTS sizeof(tls_certs) / sizeof(cert) +#define CERT_FILE_MODE 0400 +#define READ_WRITE_MODE 0600 + +typedef enum { + STOPPING = -1, + STARTED = 0, + START_IN_PROGRESS, + START_PENDING +} status; + +static int g_status = START_IN_PROGRESS; + +/** + * @brief Checks if the certificate name is supported + * and optionally updates the certificate type. + * + * @param cert_name the certificate to check + * @param cert_type pointer to the type(s) to be updated or NULL + * @return true if valid, false otherwise. + */ +static bool +supported_cert(char *cert_name, int **cert_type) +{ + for (size_t i = 0; i < NUM_TLS_CERTS; ++i) { + if (strcmp(cert_name, tls_certs[i].name) == 0) { + if (cert_type) + *cert_type = tls_certs[i].type; /* Update cert_type as well */ + return true; + } + } + + syslog(LOG_ERR, + "The file_name is not supported. Supported names are \"%s\", \"%s\" " + "and \"%s\".", + tls_certs[0].name, + tls_certs[1].name, + tls_certs[2].name); + return false; +} + +/** + * @brief Checks if the file has the same header. + * + * @param fp FILE pointer for the file to check + * @param header the type to validate against + * @return true if found, false otherwise. + */ +static bool +find_header(FILE *fp, const char *header) +{ + char buffer[128]; + size_t toread; + bool found = false; + + /* Check header */ + toread = strlen(header); + if (fseek(fp, 0, SEEK_SET) != 0) { + syslog(LOG_ERR, + "Could not fseek(0, SEEK_SET) bytes, err: %s", + strerror(errno)); + goto end; + } + if (fread(buffer, toread, 1, fp) != 1) { + syslog(LOG_ERR, + "Could not fread %d bytes, err: %s", + (int)toread, + strerror(errno)); + goto end; + } + if (strncmp(buffer, header, toread) != 0) { + syslog_v(LOG_INFO, + "Expecting %.*s, found %.*s", + (int)toread, + header, + (int)toread, + buffer); + goto end; + } + found = true; + +end: + return found; +} + +/** + * @brief Checks if the file has the same footer. + * + * @param fp FILE pointer for the file to check + * @param header the type to validate against + * @return true if found, false otherwise. + */ +static bool +find_footer(FILE *fp, const char *footer) +{ + char buffer[128]; + size_t toread; + bool found = false; + + /* Check footer */ + toread = strlen(footer); + if (fseek(fp, -toread, SEEK_END) != 0) { + syslog(LOG_ERR, + "Could not fseek(%d, SEEK_END) bytes, err: %s", + (int)-toread, + strerror(errno)); + goto end; + } + if (fread(buffer, toread, 1, fp) != 1) { + syslog(LOG_ERR, + "Could not fread %d bytes, err: %s", + (int)toread, + strerror(errno)); + goto end; + } + if (strncmp(buffer, footer, toread) != 0) { + syslog_v(LOG_INFO, + "Expecting %.*s, found %.*s", + (int)toread, + footer, + (int)toread, + buffer); + goto end; + } + found = true; + +end: + return found; +} + +/** + * @brief Checks if the certificate appears valid according to type. + * + * @param file_path the certificate to check + * @param cert_type pointer to the type(s) to validate against + * @return true if valid, false otherwise. + */ +static bool +valid_cert(char *file_path, int *cert_type) +{ + bool valid = false; + uint i; + + FILE *fp = fopen(file_path, "r"); + if (fp == NULL) { + syslog(LOG_ERR, "Could not fopen %s", file_path); + return false; + } + + for (i = 0; (cert_type[i] >= 0) && (cert_type[i] < NUM_CERT_TYPES); i++) { + /* Check for header */ + if (!find_header(fp, cert_specs[cert_type[i]].header)) { + continue; + } + + /* Check for corresponding footer */ + if (!find_footer(fp, cert_specs[cert_type[i]].footer)) { + continue; + } + + valid = true; + goto end; + } + syslog(LOG_ERR, "No valid header & footer combination found"); + +end: + fclose(fp); + return valid; +} /** * @brief Signals handling @@ -99,6 +332,7 @@ is_process_alive(int pid) return false; } else if (return_pid == dockerd_process_pid) { // Child is already exited, so not alive. + dockerd_process_pid = -1; return false; } return true; @@ -197,8 +431,8 @@ get_sd_filesystem(void) static bool setup_sdcard(void) { - const char *data_root = "/var/spool/storage/SD_DISK/dockerd/data"; - const char *exec_root = "/var/spool/storage/SD_DISK/dockerd/exec"; + const char *data_root = SD_CARD_PATH "/dockerd/data"; + const char *exec_root = SD_CARD_PATH "/dockerd/exec"; char *create_droot_command = g_strdup_printf("mkdir -p %s", data_root); char *create_eroot_command = g_strdup_printf("mkdir -p %s", exec_root); int res = system(create_droot_command); @@ -217,25 +451,244 @@ setup_sdcard(void) res); goto end; } - res = 0; end: - free(create_droot_command); free(create_eroot_command); - return res == 0; } +/** + * @brief Gets and verifies the SDCardSupport selection + * + * @param use_sdcard_ret selection to be updated. + * @return True if successful, false otherwise. + */ +static gboolean +get_and_verify_sd_card_selection(bool *use_sdcard_ret) +{ + gboolean return_value = false; + bool use_sdcard = *use_sdcard_ret; + char *use_sd_card_value = get_parameter_value("SDCardSupport"); + char *sd_file_system = NULL; + + if (use_sd_card_value != NULL) { + bool use_sdcard = strcmp(use_sd_card_value, "yes") == 0; + if (use_sdcard) { + // Confirm that the SD card is usable + sd_file_system = get_sd_filesystem(); + if (sd_file_system == NULL) { + syslog(LOG_ERR, + "Couldn't identify the file system of the SD card at %s", + sd_card_path); + /* TODO: Sleep and retry on SD not usable? */ + goto end; + } + + if (strcmp(sd_file_system, "vfat") == 0 || + strcmp(sd_file_system, "exfat") == 0) { + syslog(LOG_ERR, + "The SD card at %s uses file system %s which does not support " + "Unix file permissions. Please reformat to a file system that " + "support Unix file permissions, such as ext4 or xfs.", + sd_card_path, + sd_file_system); + goto end; + } + + gchar card_path[100]; + g_stpcpy(card_path, sd_card_path); + g_strlcat(card_path, "/dockerd", 100); + + if (access(card_path, F_OK) == 0 && access(card_path, W_OK) != 0) { + syslog( + LOG_ERR, + "The application user does not have write permissions to the SD " + "card directory at %s. Please change the directory permissions or " + "remove the directory.", + card_path); + goto end; + } + + if (!setup_sdcard()) { + syslog(LOG_ERR, "Failed to setup SD card."); + goto end; + } + } + syslog(LOG_INFO, "SD card set to %d", use_sdcard); + *use_sdcard_ret = use_sdcard; + return_value = true; + } + +end: + free(use_sd_card_value); + free(sd_file_system); + return return_value; +} + +/** + * @brief Gets and verifies the UseTLS selection + * + * @param use_tls_ret selection to be updated. + * @return True if successful, false otherwise. + */ +static gboolean +get_and_verify_tls_selection(bool *use_tls_ret) +{ + gboolean return_value = false; + bool use_tls = *use_tls_ret; + char *ca_path = NULL; + char *cert_path = NULL; + char *key_path = NULL; + + char *use_tls_value = get_parameter_value("UseTLS"); + if (use_tls_value != NULL) { + use_tls = (strcmp(use_tls_value, "yes") == 0); + if (use_tls) { + char *ca_path = + g_strdup_printf("%s/%s", tls_cert_path, tls_certs[0].name); + char *cert_path = + g_strdup_printf("%s/%s", tls_cert_path, tls_certs[1].name); + char *key_path = + g_strdup_printf("%s/%s", tls_cert_path, tls_certs[2].name); + + bool ca_exists = access(ca_path, F_OK) == 0; + bool cert_exists = access(cert_path, F_OK) == 0; + bool key_exists = access(key_path, F_OK) == 0; + + if (!ca_exists || !cert_exists || !key_exists) { + syslog(LOG_ERR, + "One or more TLS certificates missing. Use cert_manager.cgi to " + "upload valid certificates."); + } + + if (!ca_exists) { + syslog(LOG_ERR, + "Cannot start using TLS, no CA certificate found at %s", + ca_path); + } + if (!cert_exists) { + syslog(LOG_ERR, + "Cannot start using TLS, no server certificate found at %s", + cert_path); + } + if (!key_exists) { + syslog(LOG_ERR, + "Cannot start using TLS, no server key found at %s", + key_path); + } + + if (!ca_exists || !cert_exists || !key_exists) { + goto end; + } + + bool ca_read_only = chmod(ca_path, CERT_FILE_MODE) == 0; + bool cert_read_only = chmod(cert_path, CERT_FILE_MODE) == 0; + bool key_read_only = chmod(key_path, CERT_FILE_MODE) == 0; + + if (!ca_read_only) { + syslog(LOG_ERR, + "Cannot start using TLS, CA certificate not read only: %s", + ca_path); + } + if (!cert_read_only) { + syslog(LOG_ERR, + "Cannot start using TLS, server certificate not read only: %s", + cert_path); + } + if (!key_read_only) { + syslog(LOG_ERR, + "Cannot start using TLS, server key not read only: %s", + key_path); + } + + if (!ca_read_only || !cert_read_only || !key_read_only) { + goto end; + } + } + syslog(LOG_INFO, "TLS set to %d", use_tls); + *use_tls_ret = use_tls; + return_value = true; + } + +end: + free(use_tls_value); + free(ca_path); + free(cert_path); + free(key_path); + return return_value; +} + +/** + * @brief Gets and verifies the IPCSocket selection + * + * @param use_ipc_socket_ret selection to be updated. + * @return True if successful, false otherwise. + */ +static gboolean +get_ipc_socket_selection(bool *use_ipc_socket_ret) +{ + gboolean return_value = false; + bool use_ipc_socket = *use_ipc_socket_ret; + char *use_ipc_socket_value = get_parameter_value("IPCSocket"); + if (use_ipc_socket_value != NULL) { + use_ipc_socket = strcmp(use_ipc_socket_value, "yes") == 0; + syslog(LOG_INFO, "IPC Socket set to %d", use_ipc_socket); + *use_ipc_socket_ret = use_ipc_socket; + return_value = true; + } + free(use_ipc_socket_value); + return return_value; +} + +/** + * @brief Gets the Verbose selection + * + * @param use_verbose_ret selection to be updated. + * @return True if successful, false otherwise. + */ +static gboolean +get_verbose_selection(bool *use_verbose_ret) +{ + gboolean return_value = false; + bool use_verbose = *use_verbose_ret; + char *use_verbose_value = get_parameter_value("Verbose"); + if (use_verbose_value != NULL) { + use_verbose = strcmp(use_verbose_value, "yes") == 0; + syslog(LOG_INFO, "Verbose set to %d", use_verbose); + *use_verbose_ret = use_verbose; + g_verbose = use_verbose; + return_value = true; + } + free(use_verbose_value); + return return_value; +} + /** * @brief Start a new dockerd process. * + * @param use_sdcard start option. + * @param use_tls start option. + * @param use_ipc_socket start option. + * @param use_verbose start option. * @return True if successful, false otherwise */ static bool -start_dockerd(void) +start_dockerd(bool use_sdcard, + bool use_tls, + bool use_ipc_socket, + bool use_verbose) { + syslog_v(LOG_INFO, "start_dockerd: %d", g_status); + + syslog(LOG_INFO, + "starting dockerd with settings: use_sdcard %d, use_tls %d, " + "use_ipc_socket %d, use_verbose %d", + use_sdcard, + use_tls, + use_ipc_socket, + use_verbose); GError *error = NULL; bool return_value = false; @@ -248,82 +701,61 @@ start_dockerd(void) guint args_offset = 0; gchar **args_split = NULL; - // Read parameters - char *use_sd_card_value = get_parameter_value("SDCardSupport"); - char *use_tls_value = get_parameter_value("UseTLS"); - char *use_ipc_socket_value = get_parameter_value("IPCSocket"); - if (use_sd_card_value == NULL || use_tls_value == NULL || - use_ipc_socket_value == NULL) { - goto end; + // get host ip + char host_buffer[256]; + char *IPbuffer; + struct hostent *host_entry; + gethostname(host_buffer, sizeof(host_buffer)); + host_entry = gethostbyname(host_buffer); + IPbuffer = inet_ntoa(*((struct in_addr *)host_entry->h_addr_list[0])); + + // construct the rootlesskit command + args_offset += g_snprintf(args + args_offset, + args_len - args_offset, + "%s %s %s %s %s %s %s %s %s", + "rootlesskit", + "--subid-source=static", + "--net=slirp4netns", + "--disable-host-loopback", + "--copy-up=/etc", + "--copy-up=/run", + "--propagation=rslave", + "--port-driver slirp4netns", + /* don't use same range as company proxy */ + "--cidr=10.0.3.0/24"); + + if (use_verbose) { + args_offset += g_snprintf( + args + args_offset, args_len - args_offset, " %s", "--debug"); } - bool use_sdcard = strcmp(use_sd_card_value, "yes") == 0; - bool use_tls = strcmp(use_tls_value, "yes") == 0; - bool use_ipc_socket = strcmp(use_ipc_socket_value, "yes") == 0; - if (use_sdcard) { - // Confirm that the SD card is usable - char *sd_file_system = get_sd_filesystem(); - if (sd_file_system == NULL) { - syslog(LOG_ERR, - "Couldn't identify the file system of the SD card at %s", - sd_card_path); - goto end; - } - - if (strcmp(sd_file_system, "vfat") == 0 || - strcmp(sd_file_system, "exfat") == 0) { - syslog(LOG_ERR, - "The SD card at %s uses file system %s which does not support " - "Unix file permissions. Please reformat to a file system that " - "support Unix file permissions, such as ext4 or xfs.", - sd_card_path, - sd_file_system); - goto end; - } - - if (!setup_sdcard()) { - syslog(LOG_ERR, "Failed to setup SD card."); - goto end; - } + const uint port = use_tls ? 2376 : 2375; + args_offset += g_snprintf(args + args_offset, + args_len - args_offset, + " -p %s:%d:%d/tcp", + IPbuffer, + port, + port); + + // add dockerd arguments + args_offset += g_snprintf(args + args_offset, + args_len - args_offset, + " %s %s %s", + "dockerd", + "--iptables=false", + "--config-file " APP_LOCALDATA "/daemon.json"); + + if (!use_verbose) { + args_offset += g_snprintf( + args + args_offset, args_len - args_offset, " %s", "--log-level=warn"); } - args_offset += g_snprintf( - args + args_offset, - args_len - args_offset, - "%s %s", - "dockerd", - "--config-file /usr/local/packages/dockerdwrapper/localdata/daemon.json"); g_strlcpy(msg, "Starting dockerd", msg_len); if (use_tls) { - const char *ca_path = "/usr/local/packages/dockerdwrapper/ca.pem"; - const char *cert_path = - "/usr/local/packages/dockerdwrapper/server-cert.pem"; - const char *key_path = "/usr/local/packages/dockerdwrapper/server-key.pem"; - - bool ca_exists = access(ca_path, F_OK) == 0; - bool cert_exists = access(cert_path, F_OK) == 0; - bool key_exists = access(key_path, F_OK) == 0; - - if (!ca_exists) { - syslog(LOG_ERR, - "Cannot start using TLS, no CA certificate found at %s", - ca_path); - } - if (!cert_exists) { - syslog(LOG_ERR, - "Cannot start using TLS, no server certificate found at %s", - cert_path); - } - if (!key_exists) { - syslog(LOG_ERR, - "Cannot start using TLS, no server key found at %s", - key_path); - } - - if (!ca_exists || !cert_exists || !key_exists) { - goto end; - } + const char *ca_path = APP_LOCALDATA "/ca.pem"; + const char *cert_path = APP_LOCALDATA "/server-cert.pem"; + const char *key_path = APP_LOCALDATA "/server-key.pem"; args_offset += g_snprintf(args + args_offset, args_len - args_offset, @@ -348,24 +780,31 @@ start_dockerd(void) g_strlcat(msg, " in unsecured mode", msg_len); } - if (use_sdcard) { - args_offset += - g_snprintf(args + args_offset, - args_len - args_offset, - " %s", - "--data-root /var/spool/storage/SD_DISK/dockerd/data"); + const char *data_root = + use_sdcard ? "/var/spool/storage/SD_DISK/dockerd/data" : + "/usr/local/packages/dockerdwrapper/localdata/data"; + args_offset += g_snprintf( + args + args_offset, args_len - args_offset, " --data-root %s", data_root); + if (use_sdcard) { g_strlcat(msg, " using SD card as storage", msg_len); } else { g_strlcat(msg, " using internal storage", msg_len); } if (use_ipc_socket) { + uid_t uid = getuid(); + uid_t gid = getgid(); + // The socket should reside in the user directory and have same group as + // user args_offset += g_snprintf(args + args_offset, args_len - args_offset, - " %s", - "-H unix:///var/run/docker.sock"); - + " %s %d %s%d%s", + "--group", + gid, + "-H unix:///var/run/user/", + uid, + "/docker.sock"); g_strlcat(msg, " with IPC socket.", msg_len); } else { g_strlcat(msg, " without IPC socket.", msg_len); @@ -373,6 +812,7 @@ start_dockerd(void) // Log startup information to syslog. syslog(LOG_INFO, "%s", msg); + syslog(LOG_INFO, "%s", args); // TODO Remove this before release of rootless args_split = g_strsplit(args, " ", 0); result = g_spawn_async(NULL, @@ -401,60 +841,216 @@ start_dockerd(void) goto end; } + /* But being alive at this point DOESN'T mean we are up and stable. Sleep for + * now..*/ + int post_watch_add_secs = 15; + sleep(post_watch_add_secs); + syslog_v(LOG_INFO, + "start_dockerd: TODO: Alive but not up and stable? sleep(%d)", + post_watch_add_secs); + + g_status = STARTED; return_value = true; end: g_strfreev(args_split); - free(use_sd_card_value); - free(use_tls_value); - free(use_ipc_socket_value); g_clear_error(&error); - return return_value; } +/** + * @brief Attempt to kill process and verify success with specified time. + * + * @param process_id pointer to the process id to kill. + * @param sig the signal to attempt the kill with. + * @param secs the maximum time to wait for verification. + * @return exit_code. 0 if successful, -1 otherwise + */ +static int +kill_and_verify(int *process_id, uint sig, uint secs) +{ + int pid = *process_id; + int exit_code; + if ((exit_code = kill(pid, sig)) != 0) { + syslog(LOG_INFO, + "Failed to send %d to process %d. Error: %s", + sig, + pid, + strerror(errno)); + errno = 0; + return exit_code; + } + + uint i = 0; + while (i++ < secs) { + sleep(1); + if (*process_id == -1) { /* Set in process exited callback */ + syslog_v(LOG_INFO, + "kill_and_verify: stopped(%d) pid %d after %d secs", + sig, + pid, + i); + return 0; + } + } + + syslog_v(LOG_INFO, "Failed to stop(%d) pid %d after %d secs", sig, pid, secs); + return -1; +} + /** * @brief Stop the currently running dockerd process. * - * @return True if successful, false otherwise + * @return exit_code. 0 if successful, -1 otherwise */ -static bool +static int stop_dockerd(void) { - bool killed = false; if (dockerd_process_pid == -1) { - // Nothing to stop. - killed = true; + /* Nothing to stop. */ + exit_code = 0; goto end; } - // Send SIGTERM to the process - bool sigterm_successfully_sent = kill(dockerd_process_pid, SIGTERM) == 0; - if (!sigterm_successfully_sent) { - syslog( - LOG_ERR, "Failed to send SIGTERM to child. Error: %s", strerror(errno)); - errno = 0; + /* Send SIGTERM to the process, wait up to 10 secs */ + if ((exit_code = kill_and_verify(&dockerd_process_pid, SIGTERM, 10)) == 0) { + goto end; } + syslog(LOG_WARNING, "Failed to send and verify SIGTERM to child"); - // Wait before sending a SIGKILL. - // The sleep will be interrupted when the dockerd_process_callback arrives, - // so we will essentially sleep until dockerd has shut down or 10 seconds - // passed. - sleep(10); - - if (dockerd_process_pid == -1) { - killed = true; + /* SIGTERM failed, try SIGKILL instead, wait up to 10 secs */ + if ((exit_code = kill_and_verify(&dockerd_process_pid, SIGKILL, 10)) == 0) { goto end; } + syslog_v(LOG_INFO, "Ignoring apparent failed SIGKILL to child"); + exit_code = 0; + +end: + if (g_status > STARTED) { + /* Restart in progress and|or pending. Continue (quit the main loop).. */ + g_main_loop_quit(loop); + } + + return exit_code; +} + +/** + * @brief Stop dockerd and quit the main loop (to effect a restart). + * + * @param quit. Quits main loop if true, otherwise just stops. + */ +static void +stop_and_quit_main_loop(bool quit) +{ + if (!is_process_alive(dockerd_process_pid)) { + /* dockerd was not started. Just start (quit the main loop) */ + exit_code = 0; + if (quit) { + g_main_loop_quit(loop); + } + } else { + /* Stop the current dockerd process before restarting */ + if ((exit_code = stop_dockerd()) != 0) { + syslog(LOG_ERR, "Failed to stop dockerd process"); + if (quit) { + g_main_loop_quit(loop); + } + } + } +} + +/** + * @brief Start fcgi and dockerd. Called from outside the main loop. + * + * @return exit_code. 0 if successful, -1 otherwise + */ +static int +start(void) +{ + bool use_sdcard = false; + bool use_tls = false; + bool use_ipc_socket = false; + bool use_verbose = false; + + if (g_status > STARTED) { + /* Restarting. Sleep previously part of stop_dockerd.. */ + syslog_v( + LOG_INFO, + "TODO: Child processes cleaned up already? sleep(10) before starting"); + sleep(10); + } - // SIGTERM failed, let's try SIGKILL - killed = kill(dockerd_process_pid, SIGKILL) == 0; - if (!killed) { + if (!get_verbose_selection(&use_verbose)) { syslog( - LOG_ERR, "Failed to send SIGKILL to child. Error: %s", strerror(errno)); + LOG_INFO, + "Failed to get verbose selection. Uninstall and reinstall the acap?"); + exit_code = -1; + goto end; + } + if (!get_ipc_socket_selection(&use_ipc_socket)) { + syslog(LOG_INFO, + "Failed to get ipc socket selection. Uninstall and reinstall the " + "acap?"); + exit_code = -1; + goto end; + } + if (!get_and_verify_tls_selection(&use_tls)) { + syslog(LOG_INFO, "Failed to verify tls selection"); + goto fcgi; /* do not start dockerd */ + } + if (!get_and_verify_sd_card_selection(&use_sdcard)) { + syslog(LOG_INFO, "Failed to setup sd_card"); + goto fcgi; /* do not start dockerd */ + } + + if (!start_dockerd(use_sdcard, use_tls, use_ipc_socket, use_verbose)) { + syslog(LOG_ERR, "Failed to start dockerd"); + goto fcgi; /* could not start dockerd */ } + +fcgi: + /* Start fcgi to cert_manager*/ + if (fcgi_start(callback_action, use_verbose) != 0) { + syslog(LOG_ERR, "Failed to init fcgi_start with callback method"); + exit_code = -1; + goto end; + } + end: - return killed; + /* Update status. Start again if START_PENDING and no errors */ + if ((exit_code == 0) && (g_status-- > STARTED)) { + if (g_status > STARTED) { + stop_and_quit_main_loop(/* No need to quit main loop */ false); + return start(); + } + } else { + g_status = exit_code; + } + + syslog_v( + LOG_INFO, "start: -> g_status %d, exit_code %d", g_status, exit_code); + return exit_code; +} + +/** + * @brief Restart fcgi and dockerd. Called from inside the main loop. + */ +static void +restart(void) +{ + /* Check and update status */ + if (g_status > STARTED) { + g_status = START_PENDING; + return; + } else if (g_status < STARTED) { + syslog(LOG_ERR, "Unable to restart, status %d", g_status); + return; + } + g_status = START_IN_PROGRESS; + + /* Stop dockerd and quit the main loop to effect a restart */ + syslog_v(LOG_INFO, "restart: -> %d", g_status); + stop_and_quit_main_loop(true); } /** @@ -466,36 +1062,58 @@ dockerd_process_exited_callback(__attribute__((unused)) GPid pid, __attribute__((unused)) gpointer user_data) { GError *error = NULL; - if (!g_spawn_check_wait_status(status, &error)) { - syslog(LOG_ERR, "Dockerd process exited with error: %d", status); - g_clear_error(&error); + syslog_v(LOG_INFO, "dockerd_process_exited_callback called: %d", g_status); + + /* Sanity check of pid */ + if (pid != dockerd_process_pid) { + syslog(LOG_WARNING, + "TODO: Fix required? Expecting pid %d, found pid %d: ", + dockerd_process_pid, + pid); + return; + } + if (status == 0) { + /* Graceful exit. All good.. */ + exit_code = 0; + } else if ((status == SIGKILL) || (status == SIGTERM)) { + /* Likely here as a result of stop_dockerd().. */ + syslog_v(LOG_INFO, + "stop_dockerd instigated %s exit", + (status == SIGKILL) ? "SIGKILL" : "SIGTERM"); + exit_code = 0; + } else if (!g_spawn_check_wait_status(status, &error)) { + /* Something went wrong..*/ + syslog(LOG_ERR, + "Dockerd process exited with status: %d, error: %s", + status, + error->message); + g_clear_error(&error); exit_code = -1; + } else { + /* Not clear. Log as warning and continue..*/ + syslog(LOG_WARNING, "Dockerd process exited with status: %d", status); } - dockerd_process_pid = -1; g_spawn_close_pid(pid); // The lockfile might have been left behind if dockerd shut down in a bad // manner. Remove it manually. - remove("/var/run/docker.pid"); + uid_t uid = getuid(); + char *pid_path = g_strdup_printf("/var/run/user/%d/docker.pid", uid); + remove(pid_path); + free(pid_path); - if (restart_dockerd) { - restart_dockerd = false; - if (!start_dockerd()) { - syslog(LOG_ERR, "Failed to restart dockerd, exiting."); - exit_code = -1; - g_main_loop_quit(loop); - } - } else { - // We shouldn't restart, stop instead. + /* Stop if exit was unexpected, otherwise continue */ + dockerd_process_pid = -1; + if (exit_code != 0) { g_main_loop_quit(loop); } } /** - * @brief Callback function called when the SDCardSupport parameter - * changes. Will restart the dockerd process with the new setting. + * @brief Callback function called when any of the parameters + * changes. Will restart processes with the new setting. * * @param name Name of the updated parameter. * @param value Value of the updated parameter. @@ -512,26 +1130,18 @@ parameter_changed_callback(const gchar *name, ++i) { if (strcmp(parname, ax_parameters[i]) == 0) { syslog(LOG_INFO, "%s changed to: %s", ax_parameters[i], value); - restart_dockerd = true; unknown_parameter = false; } } if (unknown_parameter) { syslog(LOG_WARNING, "Parameter %s is not recognized", name); - restart_dockerd = false; - - // No known parameter was changed, do not restart. + /* No known parameter was changed, nothing to do */ return; } - // Stop the currently running process. - if (!stop_dockerd()) { - syslog(LOG_ERR, - "Failed to stop dockerd process. Please restart the acap " - "manually."); - exit_code = -1; - } + /* Restart to pick up the parameter change */ + restart(); } static AXParameter * @@ -581,6 +1191,46 @@ main(void) openlog(NULL, LOG_PID, LOG_USER); syslog(LOG_INFO, "Started logging."); + // Get UID of the current user + uid_t uid = getuid(); + + char path[strlen(APP_DIRECTORY) + 256]; + sprintf(path, + "/bin:/usr/bin:%s:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin", + APP_DIRECTORY); + + char docker_host[256]; + sprintf(docker_host, "unix://run/user/%d/docker.sock", (int)uid); + + char xdg_runtime_dir[256]; + sprintf(xdg_runtime_dir, "/run/user/%d", (int)uid); + + // Set environment variables + if (setenv("PATH", path, 1) != 0) { + syslog(LOG_ERR, "Error setting environment PATH."); + return -1; + } + + if (setenv("HOME", APP_DIRECTORY, 1) != 0) { + syslog(LOG_ERR, "Error setting environment APP_LOCATION."); + return -1; + } + + if (setenv("DOCKER_HOST", docker_host, 1) != 0) { + syslog(LOG_ERR, "Error setting environment DOCKER_HOST."); + return -1; + } + + if (setenv("XDG_RUNTIME_DIR", xdg_runtime_dir, 1) != 0) { + syslog(LOG_ERR, "Error setting environment XDG_RUNTIME_DIR."); + return -1; + } + + syslog(LOG_INFO, "PATH: %s", path); + syslog(LOG_INFO, "HOME: %s", APP_DIRECTORY); + syslog(LOG_INFO, "DOCKER_HOST: %s", docker_host); + syslog(LOG_INFO, "XDG_RUNTIME_DIR: %s", xdg_runtime_dir); + // Setup signal handling. init_signals(); @@ -593,26 +1243,37 @@ main(void) /* Create the GLib event loop. */ loop = g_main_loop_new(NULL, FALSE); + +main_loop: loop = g_main_loop_ref(loop); - if (!start_dockerd()) { - syslog(LOG_ERR, "Starting dockerd failed"); - exit_code = -1; + /* Start fcgi and dockerd */ + if ((exit_code = start()) != 0) { goto end; } /* Run the GLib event loop. */ g_main_loop_run(loop); + + if (exit_code == 0) { + /* Restart */ + goto main_loop; + } g_main_loop_unref(loop); end: - if (stop_dockerd()) { - syslog(LOG_INFO, "Shutting down. dockerd shut down successfully."); - } else { - syslog(LOG_WARNING, "Shutting down. Failed to shut down dockerd."); + /* Cleanup */ + g_status = STOPPING; + if (!is_process_alive(dockerd_process_pid)) { + if ((exit_code = stop_dockerd()) == 0) { + syslog(LOG_INFO, "Shutting down. dockerd shut down successfully."); + } else { + syslog(LOG_WARNING, "Shutting down. Failed to shut down dockerd."); + } } - + fcgi_stop(); if (ax_parameter != NULL) { + syslog_v(LOG_INFO, "Shutting down. unregistering ax_parameter callbacks."); for (size_t i = 0; i < sizeof(ax_parameters) / sizeof(ax_parameters[0]); ++i) { char *parameter_path = @@ -624,5 +1285,104 @@ main(void) } g_clear_error(&error); + if (exit_code != 0) { + syslog(LOG_ERR, "Please restart the acap manually."); + } return exit_code; } + +void +callback_action(__attribute__((unused)) fcgi_handle handle, + int request_method, + char *cert_name, + char *file_path) +{ + char *cert_file_with_path = NULL; + + /* Is cert supported? */ + int *cert_type; + if (!supported_cert(cert_name, &cert_type)) { + goto end; + } + + /* Action requested method */ + switch (request_method) { + case POST: + /* Is cert valid? */ + if (!valid_cert(file_path, cert_type)) { + goto end; + } + + /* If cert already exists make writeable */ + cert_file_with_path = g_strdup_printf("%s/%s", tls_cert_path, cert_name); + if (g_file_test(cert_file_with_path, G_FILE_TEST_EXISTS)) { + if (chmod(cert_file_with_path, READ_WRITE_MODE) != 0) { + syslog(LOG_ERR, + "Failed to make %s writeable, err: %s", + cert_file_with_path, + strerror(errno)); + goto end; + } + } + + /* Copy cert, overwriting any existing, and restore mode */ + syslog(LOG_INFO, "Moving %s to %s", file_path, cert_file_with_path); + GFile *source = g_file_new_for_path(file_path); + GFile *destination = g_file_new_for_path(cert_file_with_path); + GError *error = NULL; + if (!g_file_copy(source, + destination, + G_FILE_COPY_OVERWRITE, + NULL, + NULL, + NULL, + &error)) { + syslog(LOG_ERR, + "Failed to copy %s to %s, err: %s", + file_path, + cert_file_with_path, + error->message); + g_error_free(error); + goto post_end; + } + if (chmod(cert_file_with_path, CERT_FILE_MODE) != 0) { + syslog(LOG_ERR, + "Failed to make %s readonly, err: %s", + cert_file_with_path, + strerror(errno)); + } + post_end: + g_object_unref(source); + g_object_unref(destination); + break; + + case DELETE: + /* Delete cert */ + cert_file_with_path = g_strdup_printf("%s/%s", tls_cert_path, cert_name); + syslog(LOG_INFO, "Removing %s", cert_file_with_path); + if (g_file_test(cert_file_with_path, G_FILE_TEST_EXISTS)) { + if (g_remove(cert_file_with_path) != 0) { + syslog(LOG_ERR, + "Failed to remove %s from %s.", + cert_name, + tls_cert_path); + goto end; + } + } + break; + + default: + syslog(LOG_ERR, "Unsupported request method %i", request_method); + goto end; + } + + /* Restart to pick up the certificate change */ + restart(); + +end: + /* Cleanup */ + free(cert_file_with_path); + if (file_path && (/* Delete original cert */ g_remove(file_path) != 0)) { + syslog(LOG_ERR, "Failed to remove %s, err: %s", file_path, strerror(errno)); + } +} diff --git a/app/fastcgi_cert_manager.c b/app/fastcgi_cert_manager.c new file mode 100644 index 0000000..4080e7e --- /dev/null +++ b/app/fastcgi_cert_manager.c @@ -0,0 +1,420 @@ +#include "fastcgi_cert_manager.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "fcgi_stdio.h" +#include "uriparser/Uri.h" + +#define FCGI_SOCKET_NAME "FCGI_SOCKET_NAME" + +#define syslog_v(...) \ + if (g_verbose) { \ + syslog(__VA_ARGS__); \ + } + +static const char *tmp_path = "/tmp"; + +static fcgi_request_callback g_callback; +const char *g_socket_path = NULL; +static int g_socket = -1; +static GThread *g_thread = NULL; +static bool g_verbose = false; + +static UriQueryListA * +parse_uri(const char *uriString) +{ + UriUriA uri; + UriQueryListA *queryList; + int itemCount; + const char *errorPos; + + syslog(LOG_INFO, "Parsing URI: %s", uriString); + + /* Parse the URI into data structure */ + if (uriParseSingleUriA(&uri, uriString, &errorPos) != URI_SUCCESS) { + syslog(LOG_ERR, "Failed to parse URI"); + return NULL; + } + + /* Parse the query string into data structure */ + if (uriDissectQueryMallocA( + &queryList, &itemCount, uri.query.first, uri.query.afterLast) != + URI_SUCCESS) { + syslog(LOG_ERR, "Failed to parse query"); + uriFreeUriMembersA(&uri); + return NULL; + } + + uriFreeUriMembersA(&uri); + return queryList; +} + +static char * +get_value_from_query_list(const char *search_key, UriQueryListA *queryList) +{ + char *key_value = NULL; + UriQueryListA *queryItem = queryList; + + while (queryItem) { + if (strcmp(queryItem->key, search_key) == 0 && queryItem->value != NULL) { + key_value = g_strdup(queryItem->value); + } + queryItem = queryItem->next; + } + return key_value; +} + +static gchar * +write_file_from_stream(const char *cert_file_name, + int contentLength, + FCGX_Request request) +{ + bool success = false; + const char *MULTIPART_FORM_DATA = "multipart/form-data"; + gchar *file_path = NULL; + int file_des = -1; + const char *datastart = "\r\n\r\n"; + const char *dataend = "\r\n--"; + char *contentType = FCGX_GetParam("CONTENT_TYPE", request.envp); + + syslog_v(LOG_INFO, "Content-Type: %s", contentType); + if (!strncmp( + contentType, MULTIPART_FORM_DATA, sizeof(MULTIPART_FORM_DATA) - 1)) { + char *boundarytext = + strstr(contentType, "boundary="); /* Use to find the endof input */ + boundarytext += strlen("boundary="); + syslog_v(LOG_INFO, "Boundary text %s%s%s", "SB>", boundarytext, " 0) { + if ((written = write(file_des, p_payload + written, towrite)) < 0) { + syslog(LOG_ERR, + "Failed to write %d to %s, err %s", + towrite, + file_path, + strerror(errno)); + goto end; + } + done += written; + towrite -= written; + } + syslog_v(LOG_INFO, + "write: p_payload %p, %d bytes", + p_payload, + (int)(p_payload_end - p_payload)); + syslog_v(LOG_INFO, + "counter %d, bytesRead %d, done %d", + counter, + bytesRead, + done); + + if (post_boundary_found) { + done = contentLength; + success = true; + } else { + if (!pre_boundary_found) { + syslog(LOG_ERR, "No pre boundary found"); + goto end; + } + if (bytesRead != availableLen) { + syslog(LOG_ERR, "No post boundary found"); + goto end; + } + + /* Post boundary may have been partial at payload end. Ensure possible + * rematch */ + p_payload = buffer + boundarytextLen; + memcpy(buffer, p_payload_end, boundarytextLen); + } + } + } + } else { + /* TODO: Handle other Content-Type's */ + syslog(LOG_INFO, + "Content-Type not currently supported. Try \"%s\"..", + MULTIPART_FORM_DATA); + } + +end: + if (file_des != -1) { + if (close(file_des) == -1) { + syslog(LOG_ERR, "Failed to close %s, err %s", file_path, strerror(errno)); + } + } + if (!success && file_path) { + /* Cleanup */ + if (g_remove(file_path) != 0) { + syslog( + LOG_ERR, "Failed to remove %s, err: %s", file_path, strerror(errno)); + } + g_free(file_path); + file_path = NULL; + } + return file_path; +} + +static void +handle_http(void *data, __attribute__((unused)) void *userdata) +{ + FCGX_Request *request = (FCGX_Request *)data; + char *status = "200 OK"; + char *return_message = ""; + char *method = FCGX_GetParam("REQUEST_METHOD", request->envp); + syslog(LOG_INFO, "cert_manager.cgi: %s", method); + + if (strcmp(method, "POST") == 0 || strcmp(method, "DELETE") == 0) { + const char *uriString = FCGX_GetParam("REQUEST_URI", request->envp); + UriQueryListA *queryList = parse_uri(uriString); + if (queryList == NULL) { + status = "400 Bad Request"; + return_message = "URI could not be parsed"; + goto end; + } + + if (strcmp(method, "POST") == 0) { + char *cert_file_name = get_value_from_query_list("file_name", queryList); + if (cert_file_name == NULL) { + status = "400 Bad Request"; + return_message = "URI did not contain required key \"file_name\"."; + goto end; + } + + if (FCGX_GetParam("CONTENT_LENGTH", request->envp) != NULL) { + int contentLength = + strtol(FCGX_GetParam("CONTENT_LENGTH", request->envp), NULL, 10); + char *file_path = + write_file_from_stream(cert_file_name, contentLength, *request); + if (file_path == NULL) { + status = "422 Unprocessable Content"; + return_message = "Upload of temporary cert file failed."; + goto end; + } + g_callback((fcgi_handle)request, POST, cert_file_name, file_path); + g_free(cert_file_name); + g_free(file_path); + } + + } else if (strcmp(method, "DELETE") == 0) { + char *cert_file_name = get_value_from_query_list("file_name", queryList); + + if (cert_file_name == NULL) { + status = "400 Bad Request"; + return_message = "URI did not contain required key \"file_name\"."; + goto end; + } + g_callback((fcgi_handle)request, DELETE, cert_file_name, NULL); + g_free(cert_file_name); + } + } else if (strcmp(method, "GET") == 0) { + /* Return usage/help */ + return_message = + "Usage/Help\n\n" + "Method Action | URI | cURL\n\n" + "GET This Usage/Help\n" + " http://$DEVICE_IP/local/dockerdwrapper/cert_manager.cgi\n" + " curl --anyauth --user $DEVICE_USER:$DEVICE_PASSWORD -X GET " + "\"http://$DEVICE_IP/local/dockerdwrapper/cert_manager.cgi\"\n" + "POST Upload TLS certificate with file_name 'file'\n" + " " + "http://$DEVICE_IP/local/dockerdwrapper/" + "cert_manager.cgi?file_name=file\n" + " curl --anyauth --user $DEVICE_USER:$DEVICE_PASSWORD -F " + "file=@file_path -X POST " + "\"http://$DEVICE_IP/local/dockerdwrapper/" + "cert_manager.cgi?file_name=file\"\n" + "DELETE Remove TLS certificate with file_name 'file'\n" + " " + "http://$DEVICE_IP/local/dockerdwrapper/" + "cert_manager.cgi?file_name=file\n" + " curl --anyauth --user $DEVICE_USER:$DEVICE_PASSWORD -X " + "DELETE " + "\"http://$DEVICE_IP/local/dockerdwrapper/" + "cert_manager.cgi?file_name=file\"\n" + "\n"; + } else { + status = "405 Method Not Allowed"; + return_message = "The used request message is not allowed"; + } + +end: + FCGX_FPrintF(request->out, + "Status: %s\r\n" + "Content-Type: text/html\r\n\r\n" + "%s", + status, + return_message); + FCGX_Finish_r(request); +} + +static void * +handle_fcgi(__attribute__((unused)) void *arg) +{ + GThreadPool *workers = + g_thread_pool_new((GFunc)handle_http, NULL, -1, FALSE, NULL); + while (workers) { + FCGX_Request *request = g_malloc0(sizeof(FCGX_Request)); + FCGX_InitRequest(request, g_socket, FCGI_FAIL_ACCEPT_ON_INTR); + if (FCGX_Accept_r(request) < 0) { + syslog(LOG_INFO, "FCGX_Accept_r: %s", strerror(errno)); + g_free(request); + break; + } + g_thread_pool_push(workers, request, NULL); + } + syslog(LOG_INFO, "Stopping FCGI handler"); + g_thread_pool_free(workers, true, false); + return NULL; +} + +int +fcgi_start(fcgi_request_callback cb, bool verbose) +{ + openlog(NULL, LOG_PID, LOG_DAEMON); + + syslog(LOG_INFO, "Starting FCGI handler"); + g_callback = cb; + g_verbose = verbose; + if (g_thread) { + return EXIT_SUCCESS; /* Already started, just update parameters */ + } + + g_socket_path = getenv(FCGI_SOCKET_NAME); + if (!g_socket_path) { + syslog(LOG_ERR, "Failed to get environment variable FCGI_SOCKET_NAME"); + return EXIT_FAILURE; + } + + if (FCGX_Init() != 0) { + syslog(LOG_ERR, "FCGX_Init failed: %s", strerror(errno)); + return EXIT_FAILURE; + } + + if ((g_socket = FCGX_OpenSocket(g_socket_path, 5)) < 0) { + syslog(LOG_ERR, "FCGX_OpenSocket failed: %s", strerror(errno)); + return EXIT_FAILURE; + } + chmod(g_socket_path, S_IRWXU | S_IRWXG | S_IRWXO); + + /* Create a thread for request handling */ + if ((g_thread = g_thread_new("fcgi_handler", &handle_fcgi, NULL)) == NULL) { + syslog(LOG_ERR, "Failed to launch FCGI handler thread"); + return EXIT_FAILURE; + } + + syslog(LOG_INFO, "Created handler thread"); + return EXIT_SUCCESS; +} + +void +fcgi_stop(void) +{ + FCGX_ShutdownPending(); + + if (g_socket != -1) { + if (shutdown(g_socket, SHUT_RD) != 0) { + syslog( + LOG_WARNING, "Could not shutdown socket, err: %s", strerror(errno)); + } + if (g_unlink(g_socket_path) != 0) { + syslog(LOG_WARNING, "Could not unlink socket, err: %s", strerror(errno)); + } + } + g_thread_join(g_thread); + + closelog(); + g_socket_path = NULL; + g_socket = -1; + g_thread = NULL; +} diff --git a/app/fastcgi_cert_manager.h b/app/fastcgi_cert_manager.h new file mode 100644 index 0000000..cf7a501 --- /dev/null +++ b/app/fastcgi_cert_manager.h @@ -0,0 +1,14 @@ +#include +#include +#include "fcgiapp.h" + +enum HTTP_Request { POST, DELETE }; + +typedef void *fcgi_handle; +typedef void (*fcgi_request_callback)(fcgi_handle handle, + int request_method, + char *cert_name, + char *file_path); + +int fcgi_start(fcgi_request_callback cb, bool verbose); +void fcgi_stop(void); \ No newline at end of file diff --git a/app/manifest.json b/app/manifest.json index a1b5cb0..652a345 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,19 +1,28 @@ { - "schemaVersion": "1.3", + "schemaVersion": "1.8.0", + "resources": { + "linux": { + "user": { + "groups": [ + "datacache", + "optics", + "sdk", + "storage", + "vdo" + ] + } + } + }, "acapPackageConf": { "setup": { - "friendlyName": "Docker Daemon", + "friendlyName": "Docker Daemon Rootless", "appId": "414120", "appName": "dockerdwrapper", "vendor": "Axis Communications", "embeddedSdkVersion": "3.0", - "user": { - "group": "root", - "username": "root" - }, "vendorUrl": "https://www.axis.com", "runMode": "once", - "version": "1.4.0" + "version": "2.1.0-fastcgi" }, "installation": { "postInstallScript": "postinstallscript.sh" @@ -34,6 +43,22 @@ "name": "IPCSocket", "default": "no", "type": "enum:no|No, yes|Yes" + }, + { + "name": "Verbose", + "default": "no", + "type": "enum:no|No, yes|Yes" + } + ], + "containers": { + "containerHost": true, + "createDockerSymlinks": true + }, + "httpConfig": [ + { + "access": "admin", + "name": "cert_manager.cgi", + "type": "fastCgi" } ] } diff --git a/app/postinstallscript.sh b/app/postinstallscript.sh index 946b4f8..37d1944 100644 --- a/app/postinstallscript.sh +++ b/app/postinstallscript.sh @@ -1,19 +1,7 @@ -#!/bin/sh +#!/bin/sh -e -# Move the daemon.json file into localdata folder -if [ ! -e localdata/daemon.json ] -then - mv empty_daemon.json localdata/daemon.json -else - rm empty_daemon.json -fi +# *** non-root user should be able to do this **** -# Make sure containerd is started before dockerd and set PATH -cat >> /etc/systemd/system/sdkdockerdwrapper.service << EOF -[Unit] -BindsTo=containerd.service -After=network-online.target containerd.service var-spool-storage-SD_DISK.mount -Wants=network-online.target -[Service] -Environment=PATH=/usr/local/packages/dockerdwrapper:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin -EOF +# Move the daemon.json file into localdata folder +mv -n empty_daemon.json localdata/daemon.json +rm -f empty_daemon.json diff --git a/build.sh b/build.sh index 2dd675a..888d980 100755 --- a/build.sh +++ b/build.sh @@ -1,22 +1,21 @@ #!/bin/sh -eu case "${1:-}" in - armv7hf|aarch64) - ;; - *) - # error - echo "Invalid argument '${1:-}', valid arguments are armv7hf or aarch64" - exit 1 - ;; +armv7hf | aarch64) ;; +*) + # error + echo "Invalid argument '${1:-}', valid arguments are armv7hf or aarch64" + exit 1 + ;; esac imagetag="${2:-docker-acap:1.0}" # Build and copy out the acap docker buildx build --build-arg ACAPARCH="$1" \ - --build-arg HTTP_PROXY="${HTTP_PROXY:-}" \ - --build-arg HTTPS_PROXY="${HTTPS_PROXY:-}" \ - --file Dockerfile \ - --no-cache \ - --tag "$imagetag" . + --build-arg HTTP_PROXY="${HTTP_PROXY:-}" \ + --build-arg HTTPS_PROXY="${HTTPS_PROXY:-}" \ + --file Dockerfile \ + --no-cache \ + --tag "$imagetag" . docker cp "$(docker create "$imagetag")":/opt/app/ ./build-"$1"