├── .clang-format ├── .dockerignore ├── .editorconfig ├── .github ├── actions │ ├── docker-build-action │ │ └── action.yml │ ├── metadata-action │ │ └── action.yml │ └── update-acap-manifest-action │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── cd.yml │ ├── lint.yml │ └── send-dispatch.yml ├── .gitignore ├── .hadolint.yaml ├── .markdownlint.yaml ├── .vscode ├── extensions.json └── settings.json ├── .yamllint.yml ├── CODEOWNERS ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── LICENSE ├── Makefile ├── app_paths.h ├── dockerdwrapper.c ├── fcgi_server.c ├── fcgi_server.h ├── fcgi_write_file_from_stream.c ├── fcgi_write_file_from_stream.h ├── html │ └── index.html ├── http_request.c ├── http_request.h ├── log.c ├── log.h ├── manifest.json ├── postinstallscript.sh ├── sd_disk_storage.c ├── sd_disk_storage.h ├── tls.c └── tls.h └── build.sh /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | # Most of the settings are inherited by LLVM default 3 | BasedOnStyle: LLVM 4 | # Cpp covers both C and C++. 5 | Language: Cpp 6 | AlignConsecutiveAssignments: None 7 | AlignConsecutiveBitFields: Consecutive 8 | AlignConsecutiveMacros: Consecutive 9 | AlignEscapedNewlines: Left 10 | AllowAllArgumentsOnNextLine: false 11 | AllowAllParametersOfDeclarationOnNextLine: false 12 | AllowShortIfStatementsOnASingleLine: Never 13 | AllowShortFunctionsOnASingleLine: Inline 14 | AllowShortLoopsOnASingleLine: true 15 | AlwaysBreakBeforeMultilineStrings: true 16 | BinPackArguments: false 17 | BinPackParameters: false 18 | ColumnLimit: 100 19 | IndentCaseLabels: true 20 | # Most files have used this indentation width and to not get to large diffs 21 | # at the first adaption to clang the width is kept at 4 instead of the LLVM and 22 | # Google default of 2. 23 | IndentWidth: 4 24 | PointerAlignment: Left 25 | KeepEmptyLinesAtTheStartOfBlocks: false 26 | SpacesBeforeTrailingComments: 2 27 | Standard: Auto 28 | ... 29 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | build-* 2 | tmp 3 | .vscode 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*.json] 7 | indent_style = space 8 | indent_size = 4 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | -------------------------------------------------------------------------------- /.github/actions/docker-build-action/action.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Docker build action 3 | description: > 4 | A local composite action for building a docker image. 5 | Uses docker/setup-buildx-action and 6 | docker/build-push-action. 7 | 8 | inputs: 9 | dockerfile: 10 | description: The Dockerfile to use for building the image. 11 | required: true 12 | build-args: 13 | description: List of build arguments to use when building the image. 14 | required: false 15 | default: '' 16 | target: 17 | description: Name of target build stage to build. 18 | required: false 19 | default: '' 20 | tags: 21 | description: List of tags to apply to the built image. 22 | It is recommended to use docker/metadata-action to generate these. 23 | required: true 24 | labels: 25 | description: List of labels to apply to the built image. 26 | It is recommended to use docker/metadata-action to generate these. 27 | required: false 28 | default: '' 29 | use_qemu: 30 | description: Set to use QEMU when building the image. Default false. 31 | required: false 32 | default: 'false' 33 | registry: 34 | description: Name of the remote registry to push the image to. 35 | Default docker.io. 36 | required: false 37 | default: 'docker.io' 38 | registry_user: 39 | description: Username for a user with push access on the remote registry. 40 | Required if push is true. 41 | required: false 42 | registry_token: 43 | description: Token/password for the user on the the remote registry. 44 | Required if push is true. 45 | required: false 46 | provenance: 47 | description: Generate provenance attestation for the build 48 | (shorthand for --attest=type=provenance). Default false. 49 | required: false 50 | default: 'false' 51 | outputs: 52 | description: List of output destinations (format type=local,dest=path) 53 | required: false 54 | default: '' 55 | 56 | runs: 57 | using: composite 58 | steps: 59 | - name: Set up Docker buildx 60 | uses: docker/setup-buildx-action@v3 61 | - name: Set up QEMU 62 | if: ${{ inputs.use_qemu == 'true'}} 63 | uses: docker/setup-qemu-action@v3 64 | - name: Build image 65 | uses: docker/build-push-action@v6 66 | env: 67 | DOCKER_BUILD_SUMMARY: false 68 | with: 69 | context: . 70 | push: false 71 | load: true 72 | file: ${{ inputs.dockerfile }} 73 | build-args: ${{ inputs.build-args }} 74 | tags: ${{ inputs.tags }} 75 | labels: ${{ inputs.labels }} 76 | target: ${{ inputs.target }} 77 | provenance: ${{ inputs.provenance }} 78 | outputs: ${{ inputs.outputs }} 79 | -------------------------------------------------------------------------------- /.github/actions/metadata-action/action.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Docker metadata action 3 | description: > 4 | A local composite action for creating docker image metadata. 5 | Uses docker/metadata-action and returns the tags and labels output. 6 | If get_version is set to true it also returns the base version for the image, 7 | i.e without suffix. 8 | 9 | inputs: 10 | suffix: 11 | description: The suffix to use when constructing the tag(s) for the image. 12 | Could be used to add required image info like - or --test. 13 | required: false 14 | default: '' 15 | latest: 16 | description: Set to allow creation of latest tags. Default auto. 17 | required: false 18 | default: 'auto' 19 | repository: 20 | description: Name of the repository of the image to build. 21 | required: true 22 | get_version: 23 | description: Set to true if the action should return the version tag without 24 | suffix. Default false 25 | required: false 26 | default: 'false' 27 | 28 | outputs: 29 | tags: 30 | description: The tags output from the metadata generation. 31 | value: ${{ steps.meta.outputs.tags }} 32 | labels: 33 | description: The tags output from the metadata generation. 34 | value: ${{ steps.meta.outputs.labels }} 35 | full_name: 36 | description: The name of the image (tags might be an array). 37 | value: ${{ inputs.repository }}:${{ steps.meta.outputs.version }} 38 | version: 39 | description: Version tag of the image if get_version is set to true 40 | value: ${{ steps.return_version.outputs.version }} 41 | 42 | runs: 43 | using: composite 44 | steps: 45 | - name: Create metadata for docker image 46 | id: meta 47 | uses: docker/metadata-action@v5 48 | with: 49 | images: ${{ inputs.repository }} 50 | # adds the suffix for all tags, even latest. 51 | flavor: | 52 | latest=${{ inputs.latest }} 53 | suffix=${{ inputs.suffix }}, onlatest=true 54 | tags: | 55 | type=ref, event=branch 56 | type=ref, event=pr 57 | type=semver, pattern={{version}}, event=tag 58 | type=semver, pattern={{major}}.{{minor}} 59 | type=semver, pattern={{major}} 60 | - name: Get base version without suffix 61 | id: return_version 62 | if: ${{ inputs.get_version == 'true'}} 63 | shell: bash 64 | run: | 65 | tmp_version=${{ steps.meta.outputs.version }} 66 | base_version=$(echo $tmp_version | sed -e "s/${{ inputs.suffix }}$//") 67 | echo "version=$base_version" >> $GITHUB_OUTPUT 68 | -------------------------------------------------------------------------------- /.github/actions/update-acap-manifest-action/action.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Update value in ACAP application manifest.json action 3 | description: > 4 | A local composite action to update one key-value pair in an ACAP application 5 | manifest.json file. The default usage is to update the version setting 6 | (i.e. acapPackageConf.setup.version). Override the input parameter 'key' if 7 | another setting should be updated. 8 | 9 | inputs: 10 | manifest_file: 11 | description: Path to the manifest file to update. 12 | required: true 13 | value: 14 | description: The new value to set. 15 | required: true 16 | key: 17 | description: The position in the file to update. 18 | Defaults to .acapPackageConf.setup.version. 19 | default: '.acapPackageConf.setup.version' 20 | required: false 21 | append: 22 | description: Set to true to append to the current value of 'key' 23 | default: 'false' 24 | required: false 25 | 26 | runs: 27 | using: composite 28 | steps: 29 | - name: update value 30 | shell: bash 31 | run: | 32 | if [ ${{ inputs.append }} == 'true' ]; then 33 | old_value=$(cat ${{ inputs.manifest_file }} | jq -r '${{ inputs.key }}') 34 | NEW_VALUE=$old_value${{ inputs.value }} \ 35 | jq '${{ inputs.key }} = env.NEW_VALUE' \ 36 | "${{ inputs.manifest_file }}" > manifest_file.tmp 37 | else 38 | jq '${{ inputs.key }} = "${{ inputs.value }}"' \ 39 | "${{ inputs.manifest_file }}" > manifest_file.tmp 40 | fi 41 | mv manifest_file.tmp "${{ inputs.manifest_file }}" 42 | echo $(cat ${{ inputs.manifest_file }} | jq -r '${{ inputs.key }}') 43 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Please see the documentation for all configuration options: 3 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 4 | 5 | version: 2 6 | updates: 7 | # Check status of workflows 8 | - package-ecosystem: "github-actions" 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | day: "monday" 13 | # Check status of composite actions 14 | # Depandabot doesn't support wildcards so each folder need to be listed 15 | # (See https://github.com/dependabot/dependabot-core/issues/2178) 16 | - package-ecosystem: "github-actions" 17 | directory: ".github/actions/docker-build-action" 18 | schedule: 19 | interval: "weekly" 20 | day: "monday" 21 | - package-ecosystem: "github-actions" 22 | directory: ".github/actions/metadata-action" 23 | schedule: 24 | interval: "weekly" 25 | day: "monday" 26 | - package-ecosystem: "github-actions" 27 | directory: ".github/actions/update-acap-manifest-action" 28 | schedule: 29 | interval: "weekly" 30 | day: "monday" 31 | # Check status of Dockerfile 32 | - package-ecosystem: "docker" 33 | directory: "/" 34 | schedule: 35 | interval: "weekly" 36 | day: "monday" 37 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | # yamllint disable rule:line-length 2 | --- 3 | name: Sign and prerelease 4 | 5 | # Run the workflow on when code or a semver tag is pushed to main branch, 6 | # and on pull requests towards main branch 7 | on: 8 | push: 9 | branches: 10 | - "main" 11 | tags: 12 | # semver, e.g. 1.2.0 (does not match 0.1.2) 13 | - "[1-9]+.[0-9]+.[0-9]+" 14 | # semver with prerelease info, e.g. 1.0.2-beta.1 or 1.2.3-rc.10 15 | - "[1-9]+.[0-9]+.[0-9]+-[a-z]+.[0-9]+" 16 | # do not match prerelease starting w/ 0, e.g. 1.0.2-beta.0 or 1.2.3-rc.01 17 | - "![1-9]+.[0-9]+.[0-9]+-[a-z]+.[0]*" 18 | # semver with date info, e.g. 1.0.2-20221125 19 | - "[1-9]+.[0-9]+.[0-9]+-[0-9]+" 20 | # do not match date starting w/ 0, e.g. 1.0.2-01232023 21 | - "![1-9]+.[0-9]+.[0-9]+-[0]*" 22 | pull_request: 23 | branches: 24 | - "main" 25 | 26 | env: 27 | PROJECT: "docker-acap" 28 | 29 | jobs: 30 | # Builds docker ACAP using the build.sh script, then signs the eap-file in 31 | # ACAP Portal and stores it as a build artifact. 32 | # This job runs for all triggers of the workflow 33 | build: 34 | runs-on: ubuntu-latest 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | arch: ["armv7hf", "aarch64"] 39 | outputs: 40 | EAP_FILE_ARMV7HF: ${{ steps.save_full_file_name.outputs.EAP_FILE_ARMV7HF }} 41 | EAP_FILE_AARCH64: ${{ steps.save_full_file_name.outputs.EAP_FILE_AARCH64 }} 42 | SHORT_SHA: ${{ steps.save_full_file_name.outputs.SHORT_SHA }} 43 | steps: 44 | - uses: actions/checkout@v4 45 | - name: get_short_sha 46 | run: | 47 | sha=${{ github.sha }} 48 | strip_sha=${sha:0:7} 49 | echo "SHORT_SHA=${strip_sha}" >> $GITHUB_ENV 50 | - uses: actions/cache@v4 51 | if: ${{ (github.ref_type == 'tag') || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} 52 | with: 53 | path: ${{ github.workspace }}/build-${{ matrix.arch }} 54 | key: key-${{ env.SHORT_SHA }}-${{ github.run_id }}-${{ matrix.arch }} 55 | - name: Create base image metadata 56 | id: meta 57 | uses: ./.github/actions/metadata-action 58 | with: 59 | suffix: -${{ matrix.arch }} 60 | repository: ${{ env.PROJECT }} 61 | get_version: "true" 62 | - name: Get changes for manifest 63 | id: manifest-settings 64 | if: ${{ (github.ref_type == 'tag') || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} 65 | run: | 66 | if [ ${{github.ref_type}} == tag ]; then 67 | echo "version_value=${{ steps.meta.outputs.version }}" >> $GITHUB_OUTPUT 68 | echo "append_sha='false'" >> $GITHUB_OUTPUT 69 | else 70 | echo "version_value=-${{ env.SHORT_SHA }}" >> $GITHUB_OUTPUT 71 | echo "append_sha='true'" >> $GITHUB_OUTPUT 72 | fi 73 | - name: Update manifest file 74 | if: ${{ (github.ref_type == 'tag') || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} 75 | uses: ./.github/actions/update-acap-manifest-action 76 | with: 77 | manifest_file: ./app/manifest.json 78 | append: ${{ steps.manifest-settings.outputs.append_sha}} 79 | value: ${{ steps.manifest-settings.outputs.version_value }} 80 | - name: Build ${{ env.PROJECT}} application 81 | uses: ./.github/actions/docker-build-action 82 | with: 83 | dockerfile: Dockerfile 84 | tags: ${{ steps.meta.outputs.tags }} 85 | labels: ${{ steps.meta.outputs.labels }} 86 | build-args: ARCH=${{ matrix.arch }} 87 | outputs: "type=local,dest=build" 88 | - name: Get name of EAP-file 89 | id: get_eap_file_name 90 | if: ${{ (github.ref_type == 'tag') || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} 91 | run: | 92 | export EAP_FILE=$(find build -type f -name "*.eap" -printf "%f\n") 93 | delimiter="$(openssl rand -hex 8)" 94 | echo "EAP_FILE<<${delimiter}" >> ${GITHUB_ENV} 95 | echo "${EAP_FILE}" >> ${GITHUB_ENV} 96 | echo "${delimiter}" >> ${GITHUB_ENV} 97 | - name: Add sha to EAP-file name 98 | if: ${{ (github.ref_type == 'tag') || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} 99 | run: | 100 | eap_file_w_sha=$(echo $"${{ env.EAP_FILE }}" | sed 's/\.eap/_${{ env.SHORT_SHA }}.eap/') 101 | echo "EAP_FILE_W_SHA=${eap_file_w_sha}" >> $GITHUB_ENV 102 | cp build/${{ env.EAP_FILE }} build/$eap_file_w_sha 103 | - name: Save full file name 104 | id: save_full_file_name 105 | if: ${{ (github.ref_type == 'tag') || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} 106 | run: | 107 | echo "SHORT_SHA=${{ env.SHORT_SHA }}" >> $GITHUB_OUTPUT 108 | if [ ${{ matrix.arch }} = armv7hf ] 109 | then 110 | echo "EAP_FILE_ARMV7HF=${{ env.EAP_FILE_W_SHA }}" >> $GITHUB_OUTPUT 111 | elif [ ${{ matrix.arch }} = aarch64 ] 112 | then 113 | echo "EAP_FILE_AARCH64=${{ env.EAP_FILE_W_SHA }}" >> $GITHUB_OUTPUT 114 | else 115 | echo "::error::Non valid architecture '${{ matrix.arch }}' encountered" 116 | fi 117 | - name: Move EAP-file to cache location 118 | if: ${{ (github.ref_type == 'tag') || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} 119 | run: | 120 | mkdir -p ${{ github.workspace }}/build-${{ matrix.arch }} 121 | #rm -f ${{ github.workspace }}/build-${{ matrix.arch }}/${{ env.EAP_FILE_W_SHA }} 122 | mv build/${{ env.EAP_FILE_W_SHA }} ${{ github.workspace }}/build-${{ matrix.arch }}/. 123 | 124 | # Sign the eap-file from the build 125 | sign-eap: 126 | runs-on: ubuntu-latest 127 | if: ${{ (github.ref_type == 'tag') || (github.event_name == 'push' && github.ref == 'refs/heads/main') }} 128 | needs: build 129 | permissions: 130 | contents: write 131 | strategy: 132 | fail-fast: false 133 | matrix: 134 | arch: ["armv7hf", "aarch64"] 135 | max-parallel: 1 136 | env: 137 | EAP_FILE_ARMV7HF: ${{ needs.build.outputs.EAP_FILE_ARMV7HF }} 138 | EAP_FILE_AARCH64: ${{ needs.build.outputs.EAP_FILE_AARCH64 }} 139 | SHORT_SHA: ${{ needs.build.outputs.SHORT_SHA }} 140 | outputs: 141 | EAP_FILE_SIGNED_ARMV7HF: ${{ steps.save_full_file_name.outputs.EAP_FILE_SIGNED_ARMV7HF }} 142 | EAP_FILE_SIGNED_AARCH64: ${{ steps.save_full_file_name.outputs.EAP_FILE_SIGNED_AARCH64 }} 143 | steps: 144 | - name: Get EAP file name 145 | id: full_eap_name 146 | run: | 147 | if [ ${{ matrix.arch }} = armv7hf ] 148 | then 149 | echo "EAP_FILE=${{ env.EAP_FILE_ARMV7HF }}" >> $GITHUB_ENV 150 | elif [ ${{ matrix.arch }} = aarch64 ] 151 | then 152 | echo "EAP_FILE=${{ env.EAP_FILE_AARCH64 }}" >> $GITHUB_ENV 153 | else 154 | echo "::error::Non valid architecture '${{ matrix.arch }}' encountered" 155 | fi 156 | - uses: actions/cache/restore@v4 157 | with: 158 | path: ${{ github.workspace }}/build-${{ matrix.arch }} 159 | key: key-${{ env.SHORT_SHA }}-${{ github.run_id }}-${{ matrix.arch }} 160 | - name: Get cached EAP-file 161 | run: | 162 | mkdir -p build 163 | mv ${{ github.workspace }}/build-${{ matrix.arch }}/${{ env.EAP_FILE }} build/. 164 | - name: Refactor naming of EAP-file for signed output 165 | run: | 166 | signed_output=$(echo "${{ env.EAP_FILE }}" | sed 's/\.eap/_signed.eap/') 167 | echo "SIGNED_EAP_FILE=${signed_output}" >> $GITHUB_ENV 168 | - name: Sign eap-file 169 | run: | 170 | cd build 171 | RESPONSE=$(curl -XPOST -H 'accept: */*' -H 'Content-Type: multipart/form-data' \ 172 | -H 'Authorization: Bearer ${{secrets.ACAP_PORTAL_SIGNING_BEARER_TOKEN}}' \ 173 | '${{ vars.ACAP_PORTAL_URL }}/${{secrets.ACAP_PORTAL_SIGNING_ID}}/sign/binary' \ 174 | -F uploadedFile=@"${{ env.EAP_FILE }}" --output ${{ env.SIGNED_EAP_FILE }} \ 175 | -w "%{http_code}\n" -o /dev/null --http1.1) 176 | echo "HTTP_RESPONSE=$RESPONSE" >> $GITHUB_ENV 177 | - name: Check that acap has been signed 178 | run: | 179 | if [[ -n "$HTTP_RESPONSE" && "$HTTP_RESPONSE" =~ ^[0-9]+$ ]]; then 180 | if [ "$HTTP_RESPONSE" -eq 200 ]; then 181 | echo "HTTP response code is 200, signing was successful" 182 | else 183 | echo "HTTP response code is: $HTTP_RESPONSE, signing was not successful" 184 | exit 1 185 | fi 186 | else 187 | echo "HTTP_RESPONSE is empty or not a valid integer: $HTTP_RESPONSE" 188 | fi 189 | - name: Upload artifact 190 | uses: actions/upload-artifact@v4 191 | with: 192 | name: ${{ env.SIGNED_EAP_FILE }} 193 | path: build/${{ env.SIGNED_EAP_FILE }} 194 | - name: Save full file name 195 | id: save_full_file_name 196 | run: | 197 | if [ ${{ matrix.arch }} = armv7hf ] 198 | then 199 | echo "EAP_FILE_SIGNED_ARMV7HF=${{ env.SIGNED_EAP_FILE }}" >> $GITHUB_OUTPUT 200 | elif [ ${{ matrix.arch }} = aarch64 ] 201 | then 202 | echo "EAP_FILE_SIGNED_AARCH64=${{ env.SIGNED_EAP_FILE }}" >> $GITHUB_OUTPUT 203 | else 204 | echo "::error::Non valid architecture '${{ matrix.arch }}' encountered" 205 | fi 206 | 207 | # Creates a pre-release in the repository. 208 | # This job runs if the workflow is triggered by a tag and the build job was successful. 209 | create_prerelease: 210 | if: (github.ref_type == 'tag') 211 | permissions: 212 | contents: write 213 | runs-on: ubuntu-latest 214 | needs: [build, sign-eap] 215 | outputs: 216 | RELEASE_ID: ${{ steps.prerelease.outputs.RELEASE_ID }} 217 | steps: 218 | - name: Set TAG 219 | id: vars 220 | run: echo "TAG=${GITHUB_REF#refs/*/}" >> ${GITHUB_ENV} 221 | - name: Create prerelease 222 | uses: actions/github-script@v7 223 | id: prerelease 224 | env: 225 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 226 | with: 227 | script: | 228 | try { 229 | const response = await github.rest.repos.createRelease({ 230 | draft: false, 231 | generate_release_notes: true, 232 | name: '${{ env.TAG }}', 233 | owner: context.repo.owner, 234 | prerelease: true, 235 | repo: context.repo.repo, 236 | tag_name: '${{ env.TAG }}', 237 | }); 238 | core.setOutput('RELEASE_ID', response.data.id); 239 | } catch (error) { 240 | core.setFailed(error.message); 241 | } 242 | 243 | # Uploads the signed eap files from artifacts to the pre-release. 244 | # This job runs if the create_prerelease job 245 | download-and-upload-artifacts: 246 | if: (github.ref_type == 'tag') 247 | permissions: 248 | contents: write 249 | runs-on: ubuntu-latest 250 | needs: [create_prerelease, build, sign-eap] 251 | strategy: 252 | matrix: 253 | arch: [armv7hf, aarch64] 254 | env: 255 | RELEASE_ID: ${{ needs.create_prerelease.outputs.RELEASE_ID }} 256 | EAP_FILE_SIGNED_ARMV7HF: ${{ needs.sign-eap.outputs.EAP_FILE_SIGNED_ARMV7HF }} 257 | EAP_FILE_SIGNED_AARCH64: ${{ needs.sign-eap.outputs.EAP_FILE_SIGNED_AARCH64 }} 258 | steps: 259 | - name: Get EAP file name 260 | id: full_eap_name 261 | run: | 262 | if [ ${{ matrix.arch }} = armv7hf ] 263 | then 264 | echo "EAP_FILE=${{ env.EAP_FILE_SIGNED_ARMV7HF }}" >> $GITHUB_ENV 265 | elif [ ${{ matrix.arch }} = aarch64 ] 266 | then 267 | echo "EAP_FILE=${{ env.EAP_FILE_SIGNED_AARCH64 }}" >> $GITHUB_ENV 268 | else 269 | echo "::error::Non valid architecture '${{ matrix.arch }}' encountered" 270 | fi 271 | - name: Download artifacts 272 | uses: actions/download-artifact@v4 273 | with: 274 | name: ${{ env.EAP_FILE }} 275 | path: ./ 276 | - name: Upload file to GitHub release 277 | env: 278 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 279 | run: | 280 | RESPONSE=$(curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ 281 | -H "Accept: application/vnd.github.manifold-preview" \ 282 | -H "Content-Type: application/zip" \ 283 | --data-binary @${{ env.EAP_FILE }} \ 284 | "https://uploads.github.com/repos/$GITHUB_REPOSITORY/releases/${{env.RELEASE_ID}}/assets?name=${{ env.EAP_FILE }}" \ 285 | -w "%{http_code}\n" -o /dev/null) 286 | echo "HTTP_RESPONSE=$RESPONSE" >> $GITHUB_ENV 287 | - name: Check that asset has been uploaded correctly 288 | run: | 289 | if [[ -n "$HTTP_RESPONSE" && "$HTTP_RESPONSE" =~ ^[0-9]+$ ]]; then 290 | if [ "$HTTP_RESPONSE" -eq 201 ]; then 291 | echo "HTTP response code is 201, upload was successful" 292 | else 293 | echo "HTTP response code is: $HTTP_RESPONSE, upload was not successful" 294 | exit 1 295 | fi 296 | else 297 | echo "HTTP_RESPONSE is empty or not a valid integer: $HTTP_RESPONSE" 298 | fi 299 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint codebase 3 | 4 | on: 5 | push: 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Lint codebase 19 | uses: super-linter/super-linter/slim@v7 20 | env: 21 | VALIDATE_ALL_CODEBASE: true 22 | DEFAULT_BRANCH: main 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | LINTER_RULES_PATH: / 25 | IGNORE_GITIGNORED_FILES: true 26 | VALIDATE_BASH: true 27 | VALIDATE_CLANG_FORMAT: true 28 | VALIDATE_DOCKERFILE_HADOLINT: true 29 | VALIDATE_MARKDOWN: true 30 | VALIDATE_SHELL_SHFMT: true 31 | VALIDATE_YAML: true 32 | -------------------------------------------------------------------------------- /.github/workflows/send-dispatch.yml: -------------------------------------------------------------------------------- 1 | # yamllint disable rule:line-length 2 | --- 3 | name: Send workflow dispatch 4 | 5 | on: 6 | pull_request: 7 | types: 8 | - closed 9 | branches: 10 | - "main" 11 | paths-ignore: 12 | - ".github/*" 13 | 14 | jobs: 15 | send-dispatch: 16 | if: github.event.pull_request.merged == true 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Send dispatch to 2nd repo 20 | run: | 21 | curl -L \ 22 | -X POST \ 23 | -H "Accept: application/vnd.github+json" \ 24 | -H "Authorization: Bearer $BEARER_TOKEN" \ 25 | -H "X-GitHub-Api-Version: 2022-11-28" \ 26 | https://api.github.com/repos/AxisCommunications/docker-compose-acap/actions/workflows/cherry-picker.yml/dispatches \ 27 | -d '{"ref":"main"}' 28 | env: 29 | BEARER_TOKEN: ${{ secrets.WORKFLOWDISPATCH_PAT }} 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build*/ 2 | tls/ 3 | tmp/ 4 | dockerd 5 | docker-proxy 6 | dockerdwrapper 7 | package.conf.orig 8 | *.eap 9 | *.eap.old 10 | *.pem 11 | *.srl 12 | -------------------------------------------------------------------------------- /.hadolint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ignored: 3 | - DL3003 # Use WORKDIR to switch to a directory 4 | - DL3008 # Pin versions in apt get install 5 | - DL3018 # Pin versions in apk add 6 | - DL3019 # se the '--no-cache' switch ... 7 | - SC2103 # Use a ( subshell ) to avoid having to cd back. 8 | - SC2164 # Use cd ... || exit in case cd fails. 9 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | MD013: 3 | code_blocks: false 4 | line_length: 100 5 | MD025: false 6 | MD033: false 7 | MD041: false -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "DavidAnson.vscode-markdownlint", 4 | "editorconfig.editorconfig", 5 | "streetsidesoftware.code-spell-checker" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "C_Cpp.clang_format_style": "file", 3 | "[markdown]": { 4 | "editor.defaultFormatter": "DavidAnson.vscode-markdownlint", 5 | "editor.formatOnSave": true, 6 | "editor.formatOnPaste": true 7 | }, 8 | "markdown.extension.list.indentationSize": "inherit", 9 | "markdown.extension.toc.levels": "1..3", 10 | "cSpell.words": [ 11 | "anyauth", 12 | "Buildx", 13 | "containerd", 14 | "rootpasswd", 15 | "VAPIX" 16 | ] 17 | } -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Extends the default conf from 3 | # https://yamllint.readthedocs.io/en/stable/configuration.html#default-configuration 4 | 5 | extends: default 6 | 7 | rules: 8 | document-start: disable 9 | line-length: disable 10 | new-line-at-end-of-file: disable 11 | truthy: 12 | allowed-values: ['true', 'false', 'on'] 13 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @AxisCommunications/docker-acap-owners @AxisCommunications/docker-acap-reviewers 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Regarding contributions 3 | 4 | All types of contributions are encouraged and valued. See the [Table of contents](#table-of-contents) 5 | for different ways to help and details about how this project handles them. Please make sure to read 6 | the relevant section before making your contribution. It will make it a lot easier for us maintainers 7 | and smooth out the experience for all involved. We look forward to your contributions. 8 | 9 | > And if you like the project, but just don't have time to contribute, that's fine. There are other 10 | > easy ways to support the project and show your appreciation, which we would also be very happy about: 11 | > 12 | > - Star the project 13 | > - Tweet about it 14 | > - Refer this project in your project's readme 15 | > - Mention the project at local meetups and tell your friends/colleagues 16 | 17 | 18 | ## Table of contents 19 | 20 | - [I have a question](#i-have-a-question) 21 | - [I want to contribute](#i-want-to-contribute) 22 | - [Reporting bugs](#reporting-bugs) 23 | - [Suggesting enhancements](#suggesting-enhancements) 24 | - [Your first code contribution](#your-first-code-contribution) 25 | - [Lint of codebase](#lint-of-codebase) 26 | 27 | ## I have a question 28 | 29 | Before you ask a question, it is best to search for existing [issues][issues] that might help you. 30 | In case you have found a suitable issue and still need clarification, you can write your question in 31 | this issue. It is also advisable to search the internet for answers first. 32 | 33 | If you then still feel the need to ask a question and need clarification, please 34 | follow the steps in [Reporting bugs](#reporting-bugs). 35 | 36 | ## I want to contribute 37 | 38 | ### Reporting bugs 39 | 40 | #### Before submitting a bug report 41 | 42 | A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we 43 | ask you to investigate carefully, collect information and describe the issue in detail in your report. 44 | Please complete the following steps in advance to help us fix any potential bug as fast as possible: 45 | 46 | - Make sure that you are using the latest version. 47 | - Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment 48 | components/versions. 49 | - To see if other users have experienced (and potentially already solved) the same issue you are having, 50 | check if there is not already a bug report existing for your bug or error in the [bug tracker][issues_bugs]. 51 | - Also make sure to search the internet to see if users outside of the GitHub community have discussed 52 | the issue. 53 | - Collect information about the bug: 54 | - Axis device model 55 | - Axis device firmware version 56 | - Stack trace 57 | - OS and version (Windows, Linux, macOS, x86, ARM) 58 | - Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what 59 | seems relevant 60 | - Possibly your input and the output 61 | - Can you reliably reproduce the issue? And can you also reproduce it with older versions? 62 | 63 | #### How do I submit a good bug report? 64 | 65 | We use GitHub issues to track bugs and errors. If you run into an issue with the project: 66 | 67 | - Open an [issue][issues_new]. 68 | - Explain the behavior you would expect and the actual behavior. 69 | - Please provide as much context as possible and describe the *reproduction steps* that someone else 70 | can follow to recreate the issue on their own. 71 | - Provide the information you collected in the previous section. 72 | 73 | Once it's filed: 74 | 75 | - The project team will label the issue accordingly. 76 | - A team member will try to reproduce the issue with your provided steps. If there are no reproduction 77 | steps or no obvious way to reproduce the issue, the team will ask you for those steps. Bugs without 78 | steps will not be addressed until they can be reproduced. 79 | - If the team is able to reproduce the issue, it will be prioritized according to severity. 80 | 81 | ### Suggesting enhancements 82 | 83 | This section guides you through submitting an enhancement suggestion, 84 | **including completely new features and minor improvements to existing functionality**. 85 | Following these guidelines will help maintainers and the community to understand your suggestion and 86 | find related suggestions. 87 | 88 | #### Before Submitting an Enhancement 89 | 90 | - Make sure that you are using the latest version. 91 | - Read the documentation carefully and find out if the functionality is already covered, maybe by an 92 | individual configuration. 93 | - Perform a [search][issues] to see if the enhancement has already been suggested. If it has, add a 94 | comment to the existing issue instead of opening a new one. 95 | - Find out whether your idea fits with the scope and aims of the project. Keep in mind that we want 96 | features that will be useful to the majority of our users and not just a small subset. 97 | 98 | #### How do I submit a good enhancement suggestion? 99 | 100 | Enhancement suggestions are tracked as [GitHub issues][issues]. 101 | 102 | - Use a **clear and descriptive title** for the issue to identify the suggestion. 103 | - Provide a **step-by-step description of the suggested enhancement** in as many details as possible. 104 | - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. 105 | At this point you can also tell which alternatives do not work for you. 106 | - You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or 107 | point out the part which the suggestion is related to. 108 | - **Explain why this enhancement would be useful** to most users. You may also want to point out the 109 | other projects that solved it better and which could serve as inspiration. 110 | 111 | ### Your first code contribution 112 | 113 | Start by [forking the repository](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo), 114 | i.e. copying the repository to your account to grant you write access. Continue with cloning the 115 | forked repository to your local machine. 116 | 117 | ```sh 118 | git clone https://github.com//AxisCommunications/docker-acap.git 119 | ``` 120 | 121 | Navigate into the cloned directory and create a new branch: 122 | 123 | ```sh 124 | cd docker-acap 125 | git switch -c 126 | ``` 127 | 128 | Update the code according to your requirements, and commit the changes using the 129 | [conventional commits](https://www.conventionalcommits.org) message style: 130 | 131 | ```sh 132 | git commit -a -m 'Follow the conventional commit messages style to write this message' 133 | ``` 134 | 135 | Continue with pushing the local commits to GitHub: 136 | 137 | ```sh 138 | git push origin 139 | ``` 140 | 141 | Before opening a Pull Request (PR), please consider the following guidelines: 142 | 143 | - Please make sure that the code builds perfectly fine on your local system. 144 | - Make sure that all linters pass, see [Lint of codebase](#lint-of-codebase) 145 | - The PR will have to meet the code standard already available in the repository. 146 | - Explanatory comments related to code functions are required. Please write code comments for a better 147 | understanding of the code for other developers. 148 | - Note that code changes or additions to the `.github` folder (or sub-folders) will not be accepted. 149 | 150 | And finally when you are satisfied with your changes, open a new PR. 151 | 152 | ### Lint of codebase 153 | 154 | A set of different linters test the codebase and these must pass in order to get a pull request approved. 155 | 156 | #### Linters in GitHub Action 157 | 158 | When you create a pull request, a set of linters will run syntax and format checks on different file 159 | types in GitHub actions by making use of a tool called [super-linter][super-linter]. If any of the 160 | linters gives an error, this will be shown in the action connected to the pull request. 161 | 162 | In order to speed up development, it's possible to run linters as part of your local development environment. 163 | 164 | #### Run super-linter locally 165 | 166 | Since super-linter is using a Docker image in GitHub Actions, users of other editors may run it locally 167 | to lint the codebase. For complete instructions and guidance, see super-linter page for [running locally][super-linter-local]. 168 | 169 | To run a number of linters on the codebase from command line: 170 | 171 | ```sh 172 | docker run --rm \ 173 | -v $PWD:/tmp/lint \ 174 | -e RUN_LOCAL=true \ 175 | -e LINTER_RULES_PATH=/ \ 176 | -e VALIDATE_BASH=true \ 177 | -e VALIDATE_DOCKERFILE_HADOLINT=true \ 178 | -e VALIDATE_MARKDOWN=true \ 179 | -e VALIDATE_SHELL_SHFMT=true \ 180 | -e VALIDATE_YAML=true \ 181 | ghcr.io/super-linter/super-linter:slim-v7 182 | ``` 183 | 184 | See [`.github/workflows/lint.yml`](.github/workflows/lint.yml) for the exact setup used by this project. 185 | 186 | #### Run super-linter interactively 187 | 188 | It might be more convenient to run super-linter interactively. Run container and enter command line: 189 | 190 | ```sh 191 | docker run --rm \ 192 | -v $PWD:/tmp/lint \ 193 | -w /tmp/lint \ 194 | --entrypoint /bin/bash \ 195 | -it ghcr.io/super-linter/super-linter:slim-v7 196 | ``` 197 | 198 | Then from the container terminal, the following commands can lint the the code base for different 199 | file types: 200 | 201 | ```sh 202 | # Lint Dockerfile files 203 | hadolint $(find -type f -name "Dockerfile*") 204 | 205 | # Lint Markdown files 206 | markdownlint . 207 | 208 | # Lint YAML files 209 | yamllint . 210 | 211 | # Lint shell script files 212 | shellcheck $(shfmt -f .) 213 | shfmt -d . 214 | ``` 215 | 216 | To lint only a specific file, replace `.` or `$(COMMAND)` with the file path. 217 | 218 | 219 | [issues]: https://github.com/AxisCommunications/docker-acap/issues 220 | [issues_new]: https://github.com/AxisCommunications/docker-acap/issues/new 221 | [issues_bugs]: https://github.com/AxisCommunications/docker-acap/issues?q=label%3Abug 222 | [super-linter]: https://github.com/super-linter/super-linter 223 | [super-linter-local]: https://github.com/super-linter/super-linter/blob/main/docs/run-linter-locally.md 224 | 225 | 226 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ARG DOCKER_IMAGE_VERSION=26.0.0 4 | ARG PROCPS_VERSION=v3.3.17 5 | ARG NSENTER_VERSION=v2.40 6 | ARG SLIRP4NETNS_VERSION=1.2.3 7 | 8 | ARG REPO=axisecp 9 | ARG ARCH=armv7hf 10 | 11 | ARG VERSION=1.14 12 | ARG UBUNTU_VERSION=22.04 13 | ARG NATIVE_SDK=acap-native-sdk 14 | 15 | FROM ${REPO}/${NATIVE_SDK}:${VERSION}-${ARCH}-ubuntu${UBUNTU_VERSION} AS sdk_image 16 | 17 | FROM sdk_image AS build_image 18 | 19 | # hadolint ignore=DL3009 20 | RUN <$BUILD_CACHE 45 | echo ac_cv_func_malloc_0_nonnull=yes >>$BUILD_CACHE 46 | EOF 47 | 48 | RUN <$BUILD_CACHE 74 | echo ac_cv_func_malloc_0_nonnull=yes >>$BUILD_CACHE 75 | EOF 76 | 77 | RUN < 2 | # The Docker ACAP application 3 | 4 | The Docker ACAP application, from here on called the application, provides the means to run Docker on 5 | a compatible Axis device. 6 | 7 | 8 | ## Notable Releases 9 | 10 | | Release | AXIS OS min. version | Dockerd version | Type | Comment | 11 | | -----------------------: | -------------------: | --------------: |----------|---------------------------------| 12 | | [3.0.0][latest-release] | 11.10 | 26.0.0 | rootless | Latest release | 13 | | [2.0.0][2.0.0-release] | 11.9 | 26.0.0 | rootful | Legacy release AXIS OS 2024 LTS | 14 | | [1.5.0][1.5.0-release] | 10.12 | 26.0.0 | rootful | Legacy release AXIS OS 2022 LTS | 15 | 16 | 17 | > [!IMPORTANT] 18 | > From AXIS OS 12.0, running 'rootful' ACAP applications, i.e. an application setup with the `root` user, 19 | > will no longer be supported. To install a 'rootful' ACAP application on a device running AXIS OS 20 | > versions between 11.5 and 11.11, allow root must be enabled. See the [VAPIX documentation][vapix-allow-root] 21 | > for details. Alternatively, on the web page of the device: 22 | > 23 | > 1. Go to the Apps page, toggle on `Allow root-privileged apps`. 24 | > 2. Go to System → Account page, under SSH accounts, toggle off `Restrict root access` to be able to 25 | > send the TLS certificates. Make sure to set the password of the `root` SSH user. 26 | 27 | 28 | ## Table of contents 29 | 30 | - [Overview](#overview) 31 | - [Requirements](#requirements) 32 | - [Substitutions](#substitutions) 33 | - [Installation and Usage](#installation-and-usage) 34 | - [Download a pre-built EAP file](#download-a-pre-built-eap-file) 35 | - [Installation](#installation) 36 | - [Settings](#settings) 37 | - [Using TLS to secure the application](#using-tls-to-secure-the-application) 38 | - [Using an SD card as storage](#using-an-sd-card-as-storage) 39 | - [Using the application](#using-the-application) 40 | - [Building the application](#building-the-application) 41 | - [Build options](#build-options) 42 | - [Contributing](#contributing) 43 | - [License](#license) 44 | 45 | ## Overview 46 | 47 | > [!NOTE] 48 | > 49 | > When TCP socket is selected, the application can be run with TLS authentication or without. 50 | > Be aware that running without TLS authentication is extremely insecure and we 51 | > strongly recommend against this. 52 | > See [Using TLS to secure the application](#using-tls-to-secure-the-application) 53 | > for information on how to generate certificates for TLS authentication. 54 | 55 | The application provides the means to run a Docker daemon on an Axis device, thereby 56 | making it possible to deploy and run Docker containers on it. When started, the daemon 57 | will run in rootless mode, i.e. the user owning the daemon process will not be root, 58 | and by extension, the containers will not have root access to the host system. 59 | See [Rootless Mode][docker-rootless-mode] on Docker.com for more information. That page also 60 | contains known limitations when running rootless Docker. 61 | 62 | 63 | ### Known Issues 64 | 65 | - When using the SD card for this application, the file permissions can sometimes be set incorrectly 66 | during an upgrade of the device firmware or the application. 67 | See [Using an SD card as storage](#using-an-sd-card-as-storage) for information on how to handle this. 68 | 69 | - Only uid and gid are properly mapped between device and containers, not the secondary groups that the 70 | user is a member of. This means that resources on the device, even if they are volume or device mounted, 71 | can be inaccessible inside the container. This can also affect usage of unsupported D-Bus methods from 72 | the container. See [Using host user secondary groups in container](#using-host-user-secondary-groups-in-container) 73 | for information on how to handle this. 74 | 75 | ## Requirements 76 | 77 | The following requirements need to be met for running the application built from the 78 | main branch. 79 | 80 | - Axis device: 81 | - AXIS OS version 11.10 or higher. 82 | - The device needs to have ACAP Native SDK support. See [Axis devices & compatibility][devices] 83 | for more information. 84 | - The device must be [container capable](#container-capability). 85 | - Computer: 86 | - Either [Docker Desktop][dockerDesktop] version 4.11.1 or higher, or 87 | [Docker Engine][dockerEngine] version 20.10.17 or higher. 88 | - To build the application locally it is required to have [Buildx][buildx] installed. 89 | 90 | 91 | ### Container capability 92 | 93 | A list of container capable Axis devices can be found with the Axis Product 94 | Selector. 95 | 96 | From AXIS OS 12.0, only products with architecture `aarch64` that 97 | existed before this release are supported. This 98 | [query][product-selector-container] lists products that cover both. 99 | 100 | In AXIS OS 11.11, both architectures `aarch64` and `armv7hf` are supported and 101 | are found with this [query][product-selector-container-11-11]. 102 | 103 | ## Substitutions 104 | 105 | The following substitutions will be used in this documentation: 106 | 107 | | | Meaning | 108 | |----------------------| :------------------------------------------------------| 109 | | `` | `dockerdwrapper` | 110 | | `` | Device architecture, either `armv7hf`or `aarch64` | 111 | | `` | The IP address of the device | 112 | | `` | The name of a user on the device with admin rights | 113 | | `` | The password of a user on the device with admin rights | 114 | 115 | ## Installation and Usage 116 | 117 | ### Download a pre-built EAP file 118 | 119 | Download the EAP file for the architecture of your device from [Releases][latest-release]. 120 | From the command line this can be done with: 121 | 122 | ```sh 123 | curl -s https://api.github.com/repos/AxisCommunications/docker-acap/releases/latest \ 124 | | grep "browser_download_url.*Docker_Daemon_.*_\_signed.eap" 125 | ``` 126 | 127 | The prebuilt application is signed. Read more about signing 128 | [here][signing-documentation]. 129 | 130 | ### Installation 131 | 132 | > [!NOTE] 133 | > **Migrating from rootful application** 134 | > 135 | > If you are upgrading from a rootful version of this application, i.e, any version before 3.0, 136 | > the following is recommended: 137 | > 138 | >- Copy any Docker images that you want to persist from the device to your computer. 139 | >- Stop the application. 140 | >- Uninstall the application. 141 | >- Format the SD card if you will use it with the application. Make sure to manually 142 | > back up any data you wish to keep first. 143 | >- Restart the device. 144 | >- Install the rootless application. 145 | 146 | Installation can be done by using either the [device web ui](#installation-via-the-device-web-ui) or 147 | the [VAPIX application API][vapix-install]. 148 | 149 | #### Installation via the device web ui 150 | 151 | Navigate to `/camera/index.html#/apps`, enable `Allow unsigned apps` toggle 152 | then click on the `+Add app` button on the page. 153 | In the popup window that appears, select the EAP file to install. 154 | 155 | ### Settings 156 | 157 | Settings can be accessed either in the device web ui, or via [VAPIX][vapix], eg. using curl: 158 | 159 | ```sh 160 | # To read "" 161 | curl -s anyauth -u ":" \ 162 | "http:///axis-cgi/param.cgi?action=list&group=root.." 163 | 164 | # To update "" to "" 165 | curl -s anyauth -u ":" \ 166 | "http:///axis-cgi/param.cgi?action=update&root..=" 167 | ``` 168 | 169 | Note that changing the settings while the application is running will lead to dockerd being restarted. 170 | 171 | The following settings are available 172 | 173 | | Setting | Type | Action | Possible values | 174 | | :----------------------------------- | :------ | :----: |---------------------------------------| 175 | | [SDCardSupport](#sd-card-support) | Boolean | RW | `yes`,`no` | 176 | | [UseTLS](#use-tls) | Boolean | RW | `yes`,`no` | 177 | | [TCPSocket](#tcp-socket--ipc-socket) | Boolean | RW | `yes`,`no` | 178 | | [IPCSocket](#tcp-socket--ipc-socket) | Boolean | RW | `yes`,`no` | 179 | | [ApplicationLogLevel](#log-levels) | Enum | RW | `debug`,`info` | 180 | | [DockerdLogLevel](#log-levels) | Enum | RW | `debug`,`info`,`warn`,`error`,`fatal` | 181 | | [Status](#status-codes) | String | R | See [Status Codes](#status-codes) | 182 | 183 | #### SD card support 184 | 185 | Selects if the docker daemon data-root should be on the internal storage of the device (default) or on 186 | an SD card. See [Using an SD card as storage](#using-an-sd-card-as-storage) for further information. 187 | 188 | #### TCP Socket / IPC Socket 189 | 190 | To be able to connect remotely to the docker daemon on the device, `TCP Socket` needs to be selected. 191 | `IPC Socket` needs to be selected for containers running on the device to be able to communicate with 192 | each other. At least one of the sockets needs to be selected for the application to start dockerd. 193 | 194 | #### Use TLS 195 | 196 | Toggle to select if TLS should be disabled when using `TCP Socket`. See 197 | [Using TLS to secure the application](#using-tls-to-secure-the-application) for further information. 198 | 199 | #### Log levels 200 | 201 | Log levels are set separately for the application and for dockerd. For rootlesskit the log level is 202 | set to `debug` if `DockerdLogLevel` is set to `debug`. 203 | 204 | #### Status codes 205 | 206 | The application use a parameter called `Status` to inform about what state it is currently in. 207 | 208 | Following are the possible values of `Status`: 209 | 210 | **-1 NOT STARTED** - The application is not started. 211 | 212 | **0 RUNNING** - The application is started and dockerd is running. 213 | 214 | **1 DOCKERD STOPPED** - Dockerd was stopped successfully and will soon be restarted. 215 | 216 | **2 DOCKERD RUNTIME ERROR** - Dockerd has reported an error during runtime that needs to be resolved 217 | by the operator. 218 | Change at least one parameter or restart the application in order to start 219 | dockerd again. 220 | 221 | **3 TLS CERT MISSING** - `UseTLS` is selected but there but certificates are missing on the device. 222 | The application is running but dockerd is stopped. 223 | Upload certificates and restart the application or de-select `UseTLS`. 224 | 225 | **4 NO SOCKET** - Neither `TCPSocket` or `IPCSocket` are selected. 226 | The application is running but dockerd is stopped. 227 | Select one or both sockets. 228 | 229 | **5 NO SD CARD** - `SDCardSupport` is selected but no SD card is mounted in the device. 230 | The application is running but dockerd is stopped. 231 | Insert and mount an SD card. 232 | 233 | **6 SD CARD WRONG FS** - `SDCardSupport` is selected but the mounted SD card has the wrong file system. 234 | The application is running but dockerd is stopped. 235 | Format the SD card with the correct file system. 236 | 237 | **7 SD CARD WRONG PERMISSION** - `SDCardSupport` is selected but the application user does not have the 238 | correct file permissions to use it. 239 | The application is running but dockerd is stopped. 240 | Make sure no directories with the wrong user permissions are left on 241 | the SD card, then restart the application. For further information see 242 | [Using an SD card as storage](#using-an-sd-card-as-storage). 243 | 244 | ### Using TLS to secure the application 245 | 246 | When using the application with TCP socket, the application can be run in either TLS or 247 | unsecured mode. The default selection is to use TLS mode. 248 | 249 | #### TLS Setup 250 | 251 | TLS requires the following keys and certificates on the device: 252 | 253 | - Certificate Authority certificate `ca.pem` 254 | - Server certificate `server-cert.pem` 255 | - Private server key `server-key.pem` 256 | 257 | For more information on how to generate these files, please consult the official 258 | [Docker documentation][docker_protect-access]. 259 | 260 | The files can be uploaded to the device using HTTP. The request will be rejected if the file 261 | being uploaded has the incorrect header or footer for that file type. The dockerd service will 262 | restart, or try to start, after each successful HTTP POST request. 263 | Uploading a new certificate will replace an already present file. 264 | 265 | ```sh 266 | curl --anyauth -u ":" -F file=@ -X POST \ 267 | http:///local// 268 | ``` 269 | 270 | To delete any of the certificates from the device HTTP DELETE can be used. Note 271 | that this will *not* restart dockerd. 272 | 273 | ```sh 274 | curl --anyauth -u ":" -X DELETE \ 275 | http:///local// 276 | ``` 277 | 278 | An alternative way to upload the certificates using `scp`. This method requires an 279 | an SSH user with write permissions to `/usr/local/packages//localdata`. 280 | In this case the application needs to be restarted for these certificates to be used. 281 | 282 | ```sh 283 | scp ca.pem server-cert.pem server-key.pem @:/usr/local/packages//localdata/ 284 | ``` 285 | 286 | ##### Client key and certificate 287 | 288 | When configured for TLS, the Docker daemon will listen to port 2376. 289 | A client will need to have its own private key, together with a certificate authorized by the CA. 290 | 291 | ```sh 292 | docker --tlsverify \ 293 | --tlscacert=ca.pem \ 294 | --tlscert=client-cert.pem \ 295 | --tlskey=client-key.pem \ 296 | --host tcp://:2376 \ 297 | version 298 | ``` 299 | 300 | Instead of specifying the files with each Docker command, 301 | Docker can be configured to use the keys and certificates from a directory of your choice 302 | by using the `DOCKER_CERT_PATH` environment variable: 303 | 304 | ```sh 305 | export DOCKER_CERT_PATH= 306 | docker --tlsverify \ 307 | --host tcp://:2376 version 308 | ``` 309 | 310 | where `` is the directory on your computer where the files `ca.pem`, 311 | `client-cert.pem` and `client-key.pem` are stored. 312 | 313 | ##### Usage example without TLS 314 | 315 | With `TCP Socket` active and `Use TLS` inactive, the Docker daemon will instead listen to port 2375. 316 | 317 | ```sh 318 | docker --host tcp://:2375 version 319 | ``` 320 | 321 | ### Using an SD card as storage 322 | 323 | An SD card might be necessary to run the application correctly. Docker 324 | containers and docker images can be quite large, and putting them on an SD card 325 | gives more freedom in how many and how large images that can be stored. 326 | 327 | Note that dockerd requires that Unix permissions are supported by the 328 | file system. Examples of file systems which support this are ext4, ext3 and xfs. 329 | It might be necessary to reformat the SD card to one of these file systems, for 330 | example if the original file system of the SD card is vfat. 331 | 332 | Make sure to use an SD card that has enough capacity to hold your applications. 333 | Other properties of the SD card, like the speed, might also affect the performance of your 334 | applications. For example, the Computer Vision SDK example 335 | [object-detector-python][object-detector-python] 336 | has a significantly higher inference time when using a small and slow SD card. 337 | To get more informed about specifications, check the 338 | [SD Card Standards][sd-card-standards]. 339 | 340 | > [!CAUTION] 341 | > 342 | >If this application with version before 3.0 has been used on the device with SD card as storage, 343 | >the storage directory might already be created with root permissions. 344 | >Since version 3.0 the application is run in rootless mode and it will then not be able 345 | >to access that directory. To solve this, either reformat the SD card or manually 346 | >remove the directory that is used by the application. 347 | >For versions before 2.0 the path was `/var/spool/storage/SD_DISK/dockerd`. 348 | >For versions from 2.0 the path is `/var/spool/storage/areas/SD_DISK/`. 349 | >Alternatively, this can be achieved by [allowing root-privileged apps][vapix-allow-root], 350 | >reinstalling the application, then disallowing root-privileged apps again, 351 | >since the post-install script will attempt to repair the permissions when running as root. 352 | 353 | ### Using the application 354 | 355 | #### Using the application remotely 356 | 357 | To interact with the Docker daemon from a remote machine the `TCPSocket` need to be 358 | selected and the `--host` option need to be used when running any docker command. 359 | 360 | The port used will change depending on if the application runs using TLS or not. 361 | The Docker daemon will be reachable on port 2375 when running unsecured, and on 362 | port 2376 when running secured using TLS. Please read section 363 | [Using TLS to secure the application](#using-tls-to-secure-the-application) for 364 | more information. 365 | 366 | #### Run a container 367 | 368 | Make sure the application, using TLS, is running, then pull and run the 369 | [hello-world][docker-hello-world] image from Docker Hub: 370 | 371 | ```sh 372 | $ docker --tlsverify --host tcp://:2376 pull hello-world 373 | Using default tag: latest 374 | latest: Pulling from library/hello-world 375 | 70f5ac315c5a: Pull complete 376 | Digest: sha256:88ec0acaa3ec199d3b7eaf73588f4518c25f9d34f58ce9a0df68429c5af48e8d 377 | Status: Downloaded newer image for hello-world:latest 378 | docker.io/library/hello-world:latest 379 | $ docker --tlsverify --host tcp://:2376 run hello-world 380 | 381 | Hello from Docker! 382 | This message shows that your installation appears to be working correctly. 383 | 384 | To generate this message, Docker took the following steps: 385 | 1. The Docker client contacted the Docker daemon. 386 | 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. 387 | (arm64v8) 388 | 3. The Docker daemon created a new container from that image which runs the 389 | executable that produces the output you are currently reading. 390 | 4. The Docker daemon streamed that output to the Docker client, which sent it 391 | to your terminal. 392 | 393 | To try something more ambitious, you can run an Ubuntu container with: 394 | $ docker run -it ubuntu bash 395 | 396 | Share images, automate workflows, and more with a free Docker ID: 397 | https://hub.docker.com/ 398 | 399 | For more examples and ideas, visit: 400 | https://docs.docker.com/get-started/ 401 | 402 | ``` 403 | 404 | #### Proxy Setup 405 | 406 | If the device is located behind a proxy the Docker daemon needs to be configured. 407 | This is done by configuring proxy behavior for dockerd in the daemon.json file as described in 408 | ['Configure the Docker daemon to use a proxy server'][docker-proxy]. 409 | 410 | The daemon.json file should be located at `/usr/local/packages/dockerdwrapper/localdata/daemon.json` 411 | on the device and should include the following properties: 412 | 413 | ```json 414 | { 415 | "proxies": { 416 | "http-proxy": "http://proxy.example.com:3128", 417 | "https-proxy": "https://proxy.example.com:3129", 418 | "no-proxy": "*.test.example.com,.example.org,127.0.0.0/8" 419 | } 420 | } 421 | ``` 422 | 423 | Setting the contents of the daemon.json file can be done either by adding it to the source code and 424 | rebuilding the application or by logging into the device over SSH with an already installed application and updating 425 | the file. 426 | In the latter case [Developer Mode][developermode] is needed, see that documentation for further details. 427 | Also note that, if the application is running when the file is updated, it needs to be restarted for 428 | the change to take effect. 429 | 430 | #### Loading images onto a device 431 | 432 | If you have images in a local repository that you want to transfer to a device, or 433 | if you have problems getting the `pull` command to work in your environment, `save` 434 | and `load` can be used. 435 | 436 | ```sh 437 | docker save | docker --tlsverify --host tcp://:2376 load 438 | ``` 439 | 440 | #### Using host user secondary groups in container 441 | 442 | The application is run by a non-root user on the device. This user is set 443 | up to be a member in a number of secondary groups as listed in the /app/manifest.json 444 | file. 445 | 446 | When running a container, a user called `root`, (uid 0), belonging to group `root`, (gid 0), 447 | will be the default user inside the container. It will be mapped to the non-root user on 448 | the device, and the group will be mapped to the non-root user's primary group. 449 | In order to get access inside the container to resources on the device that are group owned by any 450 | of the non-root users secondary groups, these need to be added for the container user. 451 | This can be done by using `group_add` in a docker-compose.yaml or `--group-add` if using the Docker cli. 452 | Unfortunately, adding the name of a secondary group is not supported. Instead the *mapped* id 453 | of the group need to be used. The current mappings are: 454 | 455 | | device group | container group id | 456 | | ------------ | :----------------: | 457 | | `sdk` | "1" | 458 | | `storage` | "2" | 459 | 460 | Note that the names of the groups will *not* be correctly displayed inside the container. 461 | 462 | ## Building the application 463 | 464 | Docker can be used to build the application and output the EAP file: 465 | 466 | ```sh 467 | docker buildx build --file Dockerfile --build-arg ARCH= --output . 468 | ``` 469 | 470 | where `` is the path to an output folder on your machine, eg. `build`. This will be 471 | created for you if not already existing. Once the build has completed the EAP file can be found 472 | in the ``. 473 | 474 | ### Build options 475 | 476 | In order to build with debug symbols and sanitizing instrumentation for detecting memory leaks and 477 | undefined behavior, add the option 478 | 479 | ```sh 480 | --build-arg BUILD_WITH_SANITIZERS=1 481 | ``` 482 | 483 | to the docker command line above. 484 | 485 | ## Contributing 486 | 487 | Take a look at the [CONTRIBUTING.md](CONTRIBUTING.md) file. 488 | 489 | ## License 490 | 491 | [Apache 2.0](LICENSE) 492 | 493 | 494 | 495 | [1.5.0-release]: https://github.com/AxisCommunications/docker-acap/releases/tag/1.5.0 496 | [2.0.0-release]: https://github.com/AxisCommunications/docker-acap/releases/tag/2.0.0 497 | [buildx]: https://docs.docker.com/build/install-buildx/ 498 | [devices]: https://axiscommunications.github.io/acap-documentation/docs/axis-devices-and-compatibility#sdk-and-device-compatibility 499 | [developermode]: http://axiscommunications.github.io/acap-documentation/docs/get-started/set-up-developer-environment/set-up-device-advanced.html#developer-mode 500 | [dockerDesktop]: https://docs.docker.com/desktop/ 501 | [docker_protect-access]: https://docs.docker.com/engine/security/protect-access/ 502 | [dockerEngine]: https://docs.docker.com/engine/ 503 | [docker-hello-world]: https://hub.docker.com/_/hello-world 504 | [docker-rootless-mode]: https://docs.docker.com/engine/security/rootless/ 505 | [docker-proxy]: https://docs.docker.com/config/daemon/systemd/#httphttps-proxy 506 | [latest-release]: https://github.com/AxisCommunications/docker-acap/releases/latest 507 | [object-detector-python]: https://github.com/AxisCommunications/acap-computer-vision-sdk-examples/tree/main/object-detector-python 508 | [product-selector-container-11-11]: https://www.axis.com/support/tools/product-selector/shared/%5B%7B%22index%22%3A%5B10%2C2%5D%2C%22value%22%3A%22Yes%22%7D%5D 509 | [product-selector-container]: https://www.axis.com/support/tools/product-selector/shared/%5B%7B%22index%22%3A%5B10%2C0%5D%2C%22value%22%3A%22ARTPEC-8%22%7D%2C%7B%22index%22%3A%5B10%2C2%5D%2C%22value%22%3A%22Yes%22%7D%5D 510 | [sd-card-standards]: https://www.sdcard.org/developers/sd-standard-overview/ 511 | [signing-documentation]: https://axiscommunications.github.io/acap-documentation/docs/faq/security.html#sign-acap-applications 512 | [vapix]: https://www.axis.com/vapix-library/ 513 | [vapix-install]: https://www.axis.com/vapix-library/subjects/t10102231/section/t10036126/display?section=t10036126-t10010609 514 | [vapix-allow-root]: https://www.axis.com/vapix-library/subjects/t10102231/section/t10036126/display?section=t10036126-t10185050 515 | 516 | -------------------------------------------------------------------------------- /app/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2022] [Axis Communications AB] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | ================================================================================ 204 | Third party licenses 205 | ================================================================================ 206 | 207 | -------------------------------------------------------------------------------- 208 | moby, including docker binaries and rootlesskit 209 | -------------------------------------------------------------------------------- 210 | 211 | Apache License 212 | Version 2.0, January 2004 213 | https://www.apache.org/licenses/ 214 | 215 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 216 | 217 | 1. Definitions. 218 | 219 | "License" shall mean the terms and conditions for use, reproduction, 220 | and distribution as defined by Sections 1 through 9 of this document. 221 | 222 | "Licensor" shall mean the copyright owner or entity authorized by 223 | the copyright owner that is granting the License. 224 | 225 | "Legal Entity" shall mean the union of the acting entity and all 226 | other entities that control, are controlled by, or are under common 227 | control with that entity. For the purposes of this definition, 228 | "control" means (i) the power, direct or indirect, to cause the 229 | direction or management of such entity, whether by contract or 230 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 231 | outstanding shares, or (iii) beneficial ownership of such entity. 232 | 233 | "You" (or "Your") shall mean an individual or Legal Entity 234 | exercising permissions granted by this License. 235 | 236 | "Source" form shall mean the preferred form for making modifications, 237 | including but not limited to software source code, documentation 238 | source, and configuration files. 239 | 240 | "Object" form shall mean any form resulting from mechanical 241 | transformation or translation of a Source form, including but 242 | not limited to compiled object code, generated documentation, 243 | and conversions to other media types. 244 | 245 | "Work" shall mean the work of authorship, whether in Source or 246 | Object form, made available under the License, as indicated by a 247 | copyright notice that is included in or attached to the work 248 | (an example is provided in the Appendix below). 249 | 250 | "Derivative Works" shall mean any work, whether in Source or Object 251 | form, that is based on (or derived from) the Work and for which the 252 | editorial revisions, annotations, elaborations, or other modifications 253 | represent, as a whole, an original work of authorship. For the purposes 254 | of this License, Derivative Works shall not include works that remain 255 | separable from, or merely link (or bind by name) to the interfaces of, 256 | the Work and Derivative Works thereof. 257 | 258 | "Contribution" shall mean any work of authorship, including 259 | the original version of the Work and any modifications or additions 260 | to that Work or Derivative Works thereof, that is intentionally 261 | submitted to Licensor for inclusion in the Work by the copyright owner 262 | or by an individual or Legal Entity authorized to submit on behalf of 263 | the copyright owner. For the purposes of this definition, "submitted" 264 | means any form of electronic, verbal, or written communication sent 265 | to the Licensor or its representatives, including but not limited to 266 | communication on electronic mailing lists, source code control systems, 267 | and issue tracking systems that are managed by, or on behalf of, the 268 | Licensor for the purpose of discussing and improving the Work, but 269 | excluding communication that is conspicuously marked or otherwise 270 | designated in writing by the copyright owner as "Not a Contribution." 271 | 272 | "Contributor" shall mean Licensor and any individual or Legal Entity 273 | on behalf of whom a Contribution has been received by Licensor and 274 | subsequently incorporated within the Work. 275 | 276 | 2. Grant of Copyright License. Subject to the terms and conditions of 277 | this License, each Contributor hereby grants to You a perpetual, 278 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 279 | copyright license to reproduce, prepare Derivative Works of, 280 | publicly display, publicly perform, sublicense, and distribute the 281 | Work and such Derivative Works in Source or Object form. 282 | 283 | 3. Grant of Patent License. Subject to the terms and conditions of 284 | this License, each Contributor hereby grants to You a perpetual, 285 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 286 | (except as stated in this section) patent license to make, have made, 287 | use, offer to sell, sell, import, and otherwise transfer the Work, 288 | where such license applies only to those patent claims licensable 289 | by such Contributor that are necessarily infringed by their 290 | Contribution(s) alone or by combination of their Contribution(s) 291 | with the Work to which such Contribution(s) was submitted. If You 292 | institute patent litigation against any entity (including a 293 | cross-claim or counterclaim in a lawsuit) alleging that the Work 294 | or a Contribution incorporated within the Work constitutes direct 295 | or contributory patent infringement, then any patent licenses 296 | granted to You under this License for that Work shall terminate 297 | as of the date such litigation is filed. 298 | 299 | 4. Redistribution. You may reproduce and distribute copies of the 300 | Work or Derivative Works thereof in any medium, with or without 301 | modifications, and in Source or Object form, provided that You 302 | meet the following conditions: 303 | 304 | (a) You must give any other recipients of the Work or 305 | Derivative Works a copy of this License; and 306 | 307 | (b) You must cause any modified files to carry prominent notices 308 | stating that You changed the files; and 309 | 310 | (c) You must retain, in the Source form of any Derivative Works 311 | that You distribute, all copyright, patent, trademark, and 312 | attribution notices from the Source form of the Work, 313 | excluding those notices that do not pertain to any part of 314 | the Derivative Works; and 315 | 316 | (d) If the Work includes a "NOTICE" text file as part of its 317 | distribution, then any Derivative Works that You distribute must 318 | include a readable copy of the attribution notices contained 319 | within such NOTICE file, excluding those notices that do not 320 | pertain to any part of the Derivative Works, in at least one 321 | of the following places: within a NOTICE text file distributed 322 | as part of the Derivative Works; within the Source form or 323 | documentation, if provided along with the Derivative Works; or, 324 | within a display generated by the Derivative Works, if and 325 | wherever such third-party notices normally appear. The contents 326 | of the NOTICE file are for informational purposes only and 327 | do not modify the License. You may add Your own attribution 328 | notices within Derivative Works that You distribute, alongside 329 | or as an addendum to the NOTICE text from the Work, provided 330 | that such additional attribution notices cannot be construed 331 | as modifying the License. 332 | 333 | You may add Your own copyright statement to Your modifications and 334 | may provide additional or different license terms and conditions 335 | for use, reproduction, or distribution of Your modifications, or 336 | for any such Derivative Works as a whole, provided Your use, 337 | reproduction, and distribution of the Work otherwise complies with 338 | the conditions stated in this License. 339 | 340 | 5. Submission of Contributions. Unless You explicitly state otherwise, 341 | any Contribution intentionally submitted for inclusion in the Work 342 | by You to the Licensor shall be under the terms and conditions of 343 | this License, without any additional terms or conditions. 344 | Notwithstanding the above, nothing herein shall supersede or modify 345 | the terms of any separate license agreement you may have executed 346 | with Licensor regarding such Contributions. 347 | 348 | 6. Trademarks. This License does not grant permission to use the trade 349 | names, trademarks, service marks, or product names of the Licensor, 350 | except as required for reasonable and customary use in describing the 351 | origin of the Work and reproducing the content of the NOTICE file. 352 | 353 | 7. Disclaimer of Warranty. Unless required by applicable law or 354 | agreed to in writing, Licensor provides the Work (and each 355 | Contributor provides its Contributions) on an "AS IS" BASIS, 356 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 357 | implied, including, without limitation, any warranties or conditions 358 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 359 | PARTICULAR PURPOSE. You are solely responsible for determining the 360 | appropriateness of using or redistributing the Work and assume any 361 | risks associated with Your exercise of permissions under this License. 362 | 363 | 8. Limitation of Liability. In no event and under no legal theory, 364 | whether in tort (including negligence), contract, or otherwise, 365 | unless required by applicable law (such as deliberate and grossly 366 | negligent acts) or agreed to in writing, shall any Contributor be 367 | liable to You for damages, including any direct, indirect, special, 368 | incidental, or consequential damages of any character arising as a 369 | result of this License or out of the use or inability to use the 370 | Work (including but not limited to damages for loss of goodwill, 371 | work stoppage, computer failure or malfunction, or any and all 372 | other commercial damages or losses), even if such Contributor 373 | has been advised of the possibility of such damages. 374 | 375 | 9. Accepting Warranty or Additional Liability. While redistributing 376 | the Work or Derivative Works thereof, You may choose to offer, 377 | and charge a fee for, acceptance of support, warranty, indemnity, 378 | or other liability obligations and/or rights consistent with this 379 | License. However, in accepting such obligations, You may act only 380 | on Your own behalf and on Your sole responsibility, not on behalf 381 | of any other Contributor, and only if You agree to indemnify, 382 | defend, and hold each Contributor harmless for any liability 383 | incurred by, or claims asserted against, such Contributor by reason 384 | of your accepting any such warranty or additional liability. 385 | 386 | END OF TERMS AND CONDITIONS 387 | 388 | Copyright 2013-2018 Docker, Inc. 389 | 390 | Licensed under the Apache License, Version 2.0 (the "License"); 391 | you may not use this file except in compliance with the License. 392 | You may obtain a copy of the License at 393 | 394 | https://www.apache.org/licenses/LICENSE-2.0 395 | 396 | Unless required by applicable law or agreed to in writing, software 397 | distributed under the License is distributed on an "AS IS" BASIS, 398 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 399 | See the License for the specific language governing permissions and 400 | limitations under the License. 401 | 402 | -------------------------------------------------------------------------------- 403 | nsenter from util-linux 404 | ps from procps-ng 405 | slirp4netns 406 | -------------------------------------------------------------------------------- 407 | 408 | GNU GENERAL PUBLIC LICENSE 409 | Version 2, June 1991 410 | 411 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 412 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 413 | Everyone is permitted to copy and distribute verbatim copies 414 | of this license document, but changing it is not allowed. 415 | 416 | Preamble 417 | 418 | The licenses for most software are designed to take away your 419 | freedom to share and change it. By contrast, the GNU General Public 420 | License is intended to guarantee your freedom to share and change free 421 | software--to make sure the software is free for all its users. This 422 | General Public License applies to most of the Free Software 423 | Foundation's software and to any other program whose authors commit to 424 | using it. (Some other Free Software Foundation software is covered by 425 | the GNU Lesser General Public License instead.) You can apply it to 426 | your programs, too. 427 | 428 | When we speak of free software, we are referring to freedom, not 429 | price. Our General Public Licenses are designed to make sure that you 430 | have the freedom to distribute copies of free software (and charge for 431 | this service if you wish), that you receive source code or can get it 432 | if you want it, that you can change the software or use pieces of it 433 | in new free programs; and that you know you can do these things. 434 | 435 | To protect your rights, we need to make restrictions that forbid 436 | anyone to deny you these rights or to ask you to surrender the rights. 437 | These restrictions translate to certain responsibilities for you if you 438 | distribute copies of the software, or if you modify it. 439 | 440 | For example, if you distribute copies of such a program, whether 441 | gratis or for a fee, you must give the recipients all the rights that 442 | you have. You must make sure that they, too, receive or can get the 443 | source code. And you must show them these terms so they know their 444 | rights. 445 | 446 | We protect your rights with two steps: (1) copyright the software, and 447 | (2) offer you this license which gives you legal permission to copy, 448 | distribute and/or modify the software. 449 | 450 | Also, for each author's protection and ours, we want to make certain 451 | that everyone understands that there is no warranty for this free 452 | software. If the software is modified by someone else and passed on, we 453 | want its recipients to know that what they have is not the original, so 454 | that any problems introduced by others will not reflect on the original 455 | authors' reputations. 456 | 457 | Finally, any free program is threatened constantly by software 458 | patents. We wish to avoid the danger that redistributors of a free 459 | program will individually obtain patent licenses, in effect making the 460 | program proprietary. To prevent this, we have made it clear that any 461 | patent must be licensed for everyone's free use or not licensed at all. 462 | 463 | The precise terms and conditions for copying, distribution and 464 | modification follow. 465 | 466 | GNU GENERAL PUBLIC LICENSE 467 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 468 | 469 | 0. This License applies to any program or other work which contains 470 | a notice placed by the copyright holder saying it may be distributed 471 | under the terms of this General Public License. The "Program", below, 472 | refers to any such program or work, and a "work based on the Program" 473 | means either the Program or any derivative work under copyright law: 474 | that is to say, a work containing the Program or a portion of it, 475 | either verbatim or with modifications and/or translated into another 476 | language. (Hereinafter, translation is included without limitation in 477 | the term "modification".) Each licensee is addressed as "you". 478 | 479 | Activities other than copying, distribution and modification are not 480 | covered by this License; they are outside its scope. The act of 481 | running the Program is not restricted, and the output from the Program 482 | is covered only if its contents constitute a work based on the 483 | Program (independent of having been made by running the Program). 484 | Whether that is true depends on what the Program does. 485 | 486 | 1. You may copy and distribute verbatim copies of the Program's 487 | source code as you receive it, in any medium, provided that you 488 | conspicuously and appropriately publish on each copy an appropriate 489 | copyright notice and disclaimer of warranty; keep intact all the 490 | notices that refer to this License and to the absence of any warranty; 491 | and give any other recipients of the Program a copy of this License 492 | along with the Program. 493 | 494 | You may charge a fee for the physical act of transferring a copy, and 495 | you may at your option offer warranty protection in exchange for a fee. 496 | 497 | 2. You may modify your copy or copies of the Program or any portion 498 | of it, thus forming a work based on the Program, and copy and 499 | distribute such modifications or work under the terms of Section 1 500 | above, provided that you also meet all of these conditions: 501 | 502 | a) You must cause the modified files to carry prominent notices 503 | stating that you changed the files and the date of any change. 504 | 505 | b) You must cause any work that you distribute or publish, that in 506 | whole or in part contains or is derived from the Program or any 507 | part thereof, to be licensed as a whole at no charge to all third 508 | parties under the terms of this License. 509 | 510 | c) If the modified program normally reads commands interactively 511 | when run, you must cause it, when started running for such 512 | interactive use in the most ordinary way, to print or display an 513 | announcement including an appropriate copyright notice and a 514 | notice that there is no warranty (or else, saying that you provide 515 | a warranty) and that users may redistribute the program under 516 | these conditions, and telling the user how to view a copy of this 517 | License. (Exception: if the Program itself is interactive but 518 | does not normally print such an announcement, your work based on 519 | the Program is not required to print an announcement.) 520 | 521 | These requirements apply to the modified work as a whole. If 522 | identifiable sections of that work are not derived from the Program, 523 | and can be reasonably considered independent and separate works in 524 | themselves, then this License, and its terms, do not apply to those 525 | sections when you distribute them as separate works. But when you 526 | distribute the same sections as part of a whole which is a work based 527 | on the Program, the distribution of the whole must be on the terms of 528 | this License, whose permissions for other licensees extend to the 529 | entire whole, and thus to each and every part regardless of who wrote it. 530 | 531 | Thus, it is not the intent of this section to claim rights or contest 532 | your rights to work written entirely by you; rather, the intent is to 533 | exercise the right to control the distribution of derivative or 534 | collective works based on the Program. 535 | 536 | In addition, mere aggregation of another work not based on the Program 537 | with the Program (or with a work based on the Program) on a volume of 538 | a storage or distribution medium does not bring the other work under 539 | the scope of this License. 540 | 541 | 3. You may copy and distribute the Program (or a work based on it, 542 | under Section 2) in object code or executable form under the terms of 543 | Sections 1 and 2 above provided that you also do one of the following: 544 | 545 | a) Accompany it with the complete corresponding machine-readable 546 | source code, which must be distributed under the terms of Sections 547 | 1 and 2 above on a medium customarily used for software interchange; or, 548 | 549 | b) Accompany it with a written offer, valid for at least three 550 | years, to give any third party, for a charge no more than your 551 | cost of physically performing source distribution, a complete 552 | machine-readable copy of the corresponding source code, to be 553 | distributed under the terms of Sections 1 and 2 above on a medium 554 | customarily used for software interchange; or, 555 | 556 | c) Accompany it with the information you received as to the offer 557 | to distribute corresponding source code. (This alternative is 558 | allowed only for noncommercial distribution and only if you 559 | received the program in object code or executable form with such 560 | an offer, in accord with Subsection b above.) 561 | 562 | The source code for a work means the preferred form of the work for 563 | making modifications to it. For an executable work, complete source 564 | code means all the source code for all modules it contains, plus any 565 | associated interface definition files, plus the scripts used to 566 | control compilation and installation of the executable. However, as a 567 | special exception, the source code distributed need not include 568 | anything that is normally distributed (in either source or binary 569 | form) with the major components (compiler, kernel, and so on) of the 570 | operating system on which the executable runs, unless that component 571 | itself accompanies the executable. 572 | 573 | If distribution of executable or object code is made by offering 574 | access to copy from a designated place, then offering equivalent 575 | access to copy the source code from the same place counts as 576 | distribution of the source code, even though third parties are not 577 | compelled to copy the source along with the object code. 578 | 579 | 4. You may not copy, modify, sublicense, or distribute the Program 580 | except as expressly provided under this License. Any attempt 581 | otherwise to copy, modify, sublicense or distribute the Program is 582 | void, and will automatically terminate your rights under this License. 583 | However, parties who have received copies, or rights, from you under 584 | this License will not have their licenses terminated so long as such 585 | parties remain in full compliance. 586 | 587 | 5. You are not required to accept this License, since you have not 588 | signed it. However, nothing else grants you permission to modify or 589 | distribute the Program or its derivative works. These actions are 590 | prohibited by law if you do not accept this License. Therefore, by 591 | modifying or distributing the Program (or any work based on the 592 | Program), you indicate your acceptance of this License to do so, and 593 | all its terms and conditions for copying, distributing or modifying 594 | the Program or works based on it. 595 | 596 | 6. Each time you redistribute the Program (or any work based on the 597 | Program), the recipient automatically receives a license from the 598 | original licensor to copy, distribute or modify the Program subject to 599 | these terms and conditions. You may not impose any further 600 | restrictions on the recipients' exercise of the rights granted herein. 601 | You are not responsible for enforcing compliance by third parties to 602 | this License. 603 | 604 | 7. If, as a consequence of a court judgment or allegation of patent 605 | infringement or for any other reason (not limited to patent issues), 606 | conditions are imposed on you (whether by court order, agreement or 607 | otherwise) that contradict the conditions of this License, they do not 608 | excuse you from the conditions of this License. If you cannot 609 | distribute so as to satisfy simultaneously your obligations under this 610 | License and any other pertinent obligations, then as a consequence you 611 | may not distribute the Program at all. For example, if a patent 612 | license would not permit royalty-free redistribution of the Program by 613 | all those who receive copies directly or indirectly through you, then 614 | the only way you could satisfy both it and this License would be to 615 | refrain entirely from distribution of the Program. 616 | 617 | If any portion of this section is held invalid or unenforceable under 618 | any particular circumstance, the balance of the section is intended to 619 | apply and the section as a whole is intended to apply in other 620 | circumstances. 621 | 622 | It is not the purpose of this section to induce you to infringe any 623 | patents or other property right claims or to contest validity of any 624 | such claims; this section has the sole purpose of protecting the 625 | integrity of the free software distribution system, which is 626 | implemented by public license practices. Many people have made 627 | generous contributions to the wide range of software distributed 628 | through that system in reliance on consistent application of that 629 | system; it is up to the author/donor to decide if he or she is willing 630 | to distribute software through any other system and a licensee cannot 631 | impose that choice. 632 | 633 | This section is intended to make thoroughly clear what is believed to 634 | be a consequence of the rest of this License. 635 | 636 | 8. If the distribution and/or use of the Program is restricted in 637 | certain countries either by patents or by copyrighted interfaces, the 638 | original copyright holder who places the Program under this License 639 | may add an explicit geographical distribution limitation excluding 640 | those countries, so that distribution is permitted only in or among 641 | countries not thus excluded. In such case, this License incorporates 642 | the limitation as if written in the body of this License. 643 | 644 | 9. The Free Software Foundation may publish revised and/or new versions 645 | of the General Public License from time to time. Such new versions will 646 | be similar in spirit to the present version, but may differ in detail to 647 | address new problems or concerns. 648 | 649 | Each version is given a distinguishing version number. If the Program 650 | specifies a version number of this License which applies to it and "any 651 | later version", you have the option of following the terms and conditions 652 | either of that version or of any later version published by the Free 653 | Software Foundation. If the Program does not specify a version number of 654 | this License, you may choose any version ever published by the Free Software 655 | Foundation. 656 | 657 | 10. If you wish to incorporate parts of the Program into other free 658 | programs whose distribution conditions are different, write to the author 659 | to ask for permission. For software which is copyrighted by the Free 660 | Software Foundation, write to the Free Software Foundation; we sometimes 661 | make exceptions for this. Our decision will be guided by the two goals 662 | of preserving the free status of all derivatives of our free software and 663 | of promoting the sharing and reuse of software generally. 664 | 665 | NO WARRANTY 666 | 667 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 668 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 669 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 670 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 671 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 672 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 673 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 674 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 675 | REPAIR OR CORRECTION. 676 | 677 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 678 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 679 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 680 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 681 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 682 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 683 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 684 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 685 | POSSIBILITY OF SUCH DAMAGES. 686 | 687 | END OF TERMS AND CONDITIONS 688 | 689 | How to Apply These Terms to Your New Programs 690 | 691 | If you develop a new program, and you want it to be of the greatest 692 | possible use to the public, the best way to achieve this is to make it 693 | free software which everyone can redistribute and change under these terms. 694 | 695 | To do so, attach the following notices to the program. It is safest 696 | to attach them to the start of each source file to most effectively 697 | convey the exclusion of warranty; and each file should have at least 698 | the "copyright" line and a pointer to where the full notice is found. 699 | 700 | 701 | Copyright (C) 702 | 703 | This program is free software; you can redistribute it and/or modify 704 | it under the terms of the GNU General Public License as published by 705 | the Free Software Foundation; either version 2 of the License, or 706 | (at your option) any later version. 707 | 708 | This program is distributed in the hope that it will be useful, 709 | but WITHOUT ANY WARRANTY; without even the implied warranty of 710 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 711 | GNU General Public License for more details. 712 | 713 | You should have received a copy of the GNU General Public License along 714 | with this program; if not, write to the Free Software Foundation, Inc., 715 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 716 | 717 | Also add information on how to contact you by electronic and paper mail. 718 | 719 | If the program is interactive, make it output a short notice like this 720 | when it starts in an interactive mode: 721 | 722 | Gnomovision version 69, Copyright (C) year name of author 723 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 724 | This is free software, and you are welcome to redistribute it 725 | under certain conditions; type `show c' for details. 726 | 727 | The hypothetical commands `show w' and `show c' should show the appropriate 728 | parts of the General Public License. Of course, the commands you use may 729 | be called something other than `show w' and `show c'; they could even be 730 | mouse-clicks or menu items--whatever suits your program. 731 | 732 | You should also get your employer (if you work as a programmer) or your 733 | school, if any, to sign a "copyright disclaimer" for the program, if 734 | necessary. Here is a sample; alter the names: 735 | 736 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 737 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 738 | 739 | , 1 April 1989 740 | Ty Coon, President of Vice 741 | 742 | This General Public License does not permit incorporating your program into 743 | proprietary programs. If your program is a subroutine library, you may 744 | consider it more useful to permit linking proprietary applications with the 745 | library. If this is what you want to do, use the GNU Lesser General 746 | Public License instead of this License. 747 | -------------------------------------------------------------------------------- /app/Makefile: -------------------------------------------------------------------------------- 1 | PROG1 = dockerdwrapper 2 | OBJS1 = $(PROG1).o fcgi_server.o fcgi_write_file_from_stream.o http_request.o log.o sd_disk_storage.o tls.o 3 | 4 | PKGS = gio-2.0 glib-2.0 axparameter axstorage fcgi 5 | CFLAGS += $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) pkg-config --cflags $(PKGS)) 6 | LDLIBS += $(shell PKG_CONFIG_PATH=$(PKG_CONFIG_PATH) pkg-config --libs $(PKGS)) 7 | 8 | CFLAGS += -W -Wformat=2 -Wpointer-arith -Wbad-function-cast -Wstrict-prototypes \ 9 | -Wmissing-prototypes -Winline -Wdisabled-optimization -Wfloat-equal -Wall -Werror \ 10 | -Wno-unused-variable \ 11 | -D APP_NAME=\"$(PROG1)\" 12 | 13 | ifdef BUILD_WITH_SANITIZERS 14 | CFLAGS += -g -fsanitize=address -fsanitize=leak -fsanitize=undefined 15 | LDFLAGS += -static-libasan -static-liblsan -static-libubsan 16 | endif 17 | 18 | all: $(PROG1) 19 | 20 | $(PROG1): $(OBJS1) 21 | $(CC) $(CFLAGS) $(LDFLAGS) $^ $(LIBS) $(LDLIBS) -o $@ 22 | 23 | $(PROG1).o tls.o: app_paths.h 24 | $(PROG1).o fcgi_server.o: fcgi_server.h 25 | fcgi_server.o fcgi_write_file_from_stream.o: fcgi_write_file_from_stream.h 26 | $(PROG1).o fcgi_server.o http_request.o log.o sd_disk_storage.o tls.o: log.h 27 | $(PROG1).o http_request.o: http_request.h 28 | $(PROG1).o sd_disk_storage.o: sd_disk_storage.h 29 | $(PROG1).o tls.o: tls.h 30 | 31 | clean: 32 | mv package.conf.orig package.conf || : 33 | rm -f $(PROG1) dockerd docker_binaries.tgz docker-init docker-proxy *.o *.eap 34 | -------------------------------------------------------------------------------- /app/app_paths.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define APP_DIRECTORY "/usr/local/packages/" APP_NAME 4 | #define APP_LOCALDATA APP_DIRECTORY "/localdata" 5 | #define DAEMON_JSON "daemon.json" 6 | #define TMP_LOCKFILE "/tmp/" APP_NAME "_xtables.lock" 7 | -------------------------------------------------------------------------------- /app/dockerdwrapper.c: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2021, Axis Communications AB, Lund, Sweden 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 | * use this file except in compliance with the License. You may obtain a copy 6 | * of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | 17 | #define _GNU_SOURCE // For sigabbrev_np() 18 | #include "app_paths.h" 19 | #include "fcgi_server.h" 20 | #include "http_request.h" 21 | #include "log.h" 22 | #include "sd_disk_storage.h" 23 | #include "tls.h" 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | #define PARAM_APPLICATION_LOG_LEVEL "ApplicationLogLevel" 39 | #define PARAM_DOCKERD_LOG_LEVEL "DockerdLogLevel" 40 | #define PARAM_IPC_SOCKET "IPCSocket" 41 | #define PARAM_SD_CARD_SUPPORT "SDCardSupport" 42 | #define PARAM_TCP_SOCKET "TCPSocket" 43 | #define PARAM_USE_TLS "UseTLS" 44 | #define PARAM_STATUS "Status" 45 | 46 | typedef enum { 47 | STATUS_NOT_STARTED = 0, // Index in the array, not the actual status code 48 | STATUS_RUNNING, 49 | STATUS_DOCKERD_STOPPED, 50 | STATUS_DOCKERD_RUNTIME_ERROR, 51 | STATUS_TLS_CERT_MISSING, 52 | STATUS_NO_SOCKET, 53 | STATUS_NO_SD_CARD, 54 | STATUS_SD_CARD_WRONG_FS, 55 | STATUS_SD_CARD_WRONG_PERMISSION, 56 | STATUS_CODE_COUNT, 57 | } status_code_t; 58 | 59 | static const char* const status_code_strs[STATUS_CODE_COUNT] = {"-1 NOT STARTED", 60 | "0 RUNNING", 61 | "1 DOCKERD STOPPED", 62 | "2 DOCKERD RUNTIME ERROR", 63 | "3 TLS CERT MISSING", 64 | "4 NO SOCKET", 65 | "5 NO SD CARD", 66 | "6 SD CARD WRONG FS", 67 | "7 SD CARD WRONG PERMISSION"}; 68 | 69 | struct settings { 70 | char* data_root; 71 | bool use_tls; 72 | bool use_tcp_socket; 73 | bool use_ipc_socket; 74 | }; 75 | 76 | struct app_state { 77 | volatile int allow_dockerd_to_start_atomic; 78 | char* sd_card_area; 79 | AXParameter* param_handle; 80 | }; 81 | 82 | static bool dockerd_allowed_to_start(const struct app_state* app_state) { 83 | return g_atomic_int_get(&app_state->allow_dockerd_to_start_atomic); 84 | } 85 | 86 | static void allow_dockerd_to_start(struct app_state* app_state, bool new_value) { 87 | g_atomic_int_set(&app_state->allow_dockerd_to_start_atomic, new_value); 88 | } 89 | 90 | // If process exited by a signal, code will be -1. 91 | // If process exited with an exit code, signal will be 0. 92 | struct exit_cause { 93 | int code; 94 | int signal; 95 | }; 96 | 97 | // Loop run on the main process 98 | static GMainLoop* loop = NULL; 99 | 100 | // Exit code of this program. Set using 'quit_program()'. 101 | #define EX_KEEP_RUNNING -1 102 | static int application_exit_code = EX_KEEP_RUNNING; 103 | 104 | static pid_t rootlesskit_pid = 0; 105 | 106 | static const char* params_that_restart_dockerd[] = {PARAM_APPLICATION_LOG_LEVEL, 107 | PARAM_DOCKERD_LOG_LEVEL, 108 | PARAM_IPC_SOCKET, 109 | PARAM_SD_CARD_SUPPORT, 110 | PARAM_TCP_SOCKET, 111 | PARAM_USE_TLS, 112 | NULL}; 113 | 114 | #define main_loop_run() \ 115 | do { \ 116 | log_debug("g_main_loop_run called by %s", __func__); \ 117 | g_main_loop_run(loop); \ 118 | log_debug("g_main_loop_run returned by %s", __func__); \ 119 | } while (0) 120 | 121 | #define main_loop_quit() \ 122 | do { \ 123 | log_debug("g_main_loop_quit called by %s", __func__); \ 124 | g_main_loop_quit(loop); \ 125 | } while (0) 126 | 127 | #define main_loop_unref() \ 128 | do { \ 129 | log_debug("g_main_loop_unref called by %s", __func__); \ 130 | g_main_loop_unref(loop); \ 131 | } while (0) 132 | 133 | static void quit_program(int exit_code) { 134 | application_exit_code = exit_code; 135 | main_loop_quit(); 136 | } 137 | 138 | static bool with_compose(void) { 139 | return strcmp(APP_NAME, "dockerdwrapperwithcompose") == 0; 140 | } 141 | 142 | static char* xdg_runtime_directory(void) { 143 | return g_strdup_printf("/var/run/user/%d", getuid()); 144 | } 145 | 146 | static char* xdg_runtime_file(const char* filename) { 147 | g_autofree char* xdg_runtime_dir = xdg_runtime_directory(); 148 | return g_strdup_printf("%s/%s", xdg_runtime_dir, filename); 149 | } 150 | 151 | static void remove_docker_pid_file(void) { 152 | g_autofree char* pid_path = xdg_runtime_file("docker.pid"); 153 | unlink(pid_path); 154 | } 155 | 156 | static bool set_xdg_directory_permisssions(mode_t mode) { 157 | g_autofree char* xdg_runtime_dir = xdg_runtime_directory(); 158 | if (chmod(xdg_runtime_dir, mode) != 0) { 159 | log_error("Failed to set permissions on %s: %s", xdg_runtime_dir, strerror(errno)); 160 | return false; 161 | } 162 | return true; 163 | } 164 | 165 | static bool let_other_apps_use_our_ipc_socket(void) { 166 | const mode_t group_read_and_exec_perms = 0750; 167 | return set_xdg_directory_permisssions(group_read_and_exec_perms); 168 | } 169 | 170 | static bool prevent_others_from_using_our_ipc_socket(void) { 171 | const mode_t user_read_and_exec_perms = 0700; 172 | return set_xdg_directory_permisssions(user_read_and_exec_perms); 173 | } 174 | 175 | /** 176 | * @brief Signals handling 177 | * 178 | * @param signal_num Signal number. 179 | */ 180 | static gboolean handle_signals(gpointer signal_num) { 181 | switch (GPOINTER_TO_INT(signal_num)) { 182 | case SIGINT: 183 | case SIGTERM: 184 | quit_program(EX_OK); 185 | } 186 | return G_SOURCE_REMOVE; 187 | } 188 | 189 | /** 190 | * @brief Initialize signals 191 | */ 192 | static void init_signals(void) { 193 | g_unix_signal_add(SIGINT, handle_signals, GINT_TO_POINTER(SIGINT)); 194 | g_unix_signal_add(SIGTERM, handle_signals, GINT_TO_POINTER(SIGTERM)); 195 | } 196 | 197 | /** 198 | * @brief Checks if the given process is alive. 199 | * 200 | * @return True if alive. False if dead or exited. 201 | */ 202 | static bool is_process_alive(int pid) { 203 | int status; 204 | pid_t return_pid = waitpid(pid, &status, WNOHANG); 205 | if (return_pid == -1) { 206 | // Report errors as dead. 207 | return false; 208 | } else if (return_pid == rootlesskit_pid) { 209 | // Child is already exited, so not alive. 210 | return false; 211 | } 212 | return true; 213 | } 214 | 215 | static bool 216 | set_parameter_value(AXParameter* param_handle, const char* parameter_name, const char* value) { 217 | log_debug("About to set %s to %s", parameter_name, value); 218 | GError* error = NULL; 219 | bool res = ax_parameter_set(param_handle, parameter_name, value, true, &error); 220 | if (!res) { 221 | log_error("Failed to write parameter value of %s to %s. Error: %s", 222 | parameter_name, 223 | value, 224 | error->message); 225 | } 226 | g_clear_error(&error); 227 | return res; 228 | } 229 | 230 | static void set_status_parameter(AXParameter* param_handle, status_code_t status) { 231 | set_parameter_value(param_handle, PARAM_STATUS, status_code_strs[status]); 232 | } 233 | 234 | /** 235 | * @brief Fetch the value of the parameter as a string 236 | * 237 | * @return The value of the parameter as string if successful, NULL otherwise 238 | */ 239 | static char* get_parameter_value(AXParameter* param_handle, const char* parameter_name) { 240 | GError* error = NULL; 241 | char* parameter_value = NULL; 242 | 243 | if (!ax_parameter_get(param_handle, parameter_name, ¶meter_value, &error)) { 244 | log_error("Failed to fetch parameter value of %s. Error: %s", 245 | parameter_name, 246 | error->message); 247 | 248 | free(parameter_value); 249 | parameter_value = NULL; 250 | } 251 | 252 | g_clear_error(&error); 253 | return parameter_value; 254 | } 255 | 256 | /** 257 | * @brief Retrieve the file system type of the device containing this path. 258 | * 259 | * @return The file system type as a string (ext4/ext3/vfat etc...) if 260 | * successful, NULL otherwise. 261 | */ 262 | static char* get_filesystem_of_path(const char* path) { 263 | char buf[PATH_MAX]; 264 | struct stat sd_card_stat; 265 | int stat_result = stat(path, &sd_card_stat); 266 | if (stat_result != 0) { 267 | log_error("Cannot store data on the SD card, no storage exists at %s", path); 268 | return NULL; 269 | } 270 | 271 | FILE* fp; 272 | dev_t dev; 273 | 274 | dev = sd_card_stat.st_dev; 275 | 276 | if ((fp = setmntent("/proc/mounts", "r")) == NULL) { 277 | return NULL; 278 | } 279 | 280 | struct mntent mnt; 281 | while (getmntent_r(fp, &mnt, buf, PATH_MAX)) { 282 | if (stat(mnt.mnt_dir, &sd_card_stat) != 0) { 283 | continue; 284 | } 285 | 286 | if (sd_card_stat.st_dev == dev) { 287 | endmntent(fp); 288 | char* return_value = strdup(mnt.mnt_type); 289 | return return_value; 290 | } 291 | } 292 | 293 | endmntent(fp); 294 | 295 | // Should never reach here. 296 | errno = EINVAL; 297 | return NULL; 298 | } 299 | 300 | // Set up the SD card. Call set_status_parameter() and return false on error. 301 | static bool setup_sdcard(AXParameter* param_handle, const char* data_root) { 302 | g_autofree char* sd_file_system = NULL; 303 | g_autofree char* create_droot_command = g_strdup_printf("mkdir -p %s", data_root); 304 | 305 | int res = system(create_droot_command); 306 | if (res != 0) { 307 | log_error("Failed to create data_root folder at: %s. Error code: %d", data_root, res); 308 | set_status_parameter(param_handle, STATUS_SD_CARD_WRONG_PERMISSION); 309 | return false; 310 | } 311 | 312 | // Confirm that the SD card is usable 313 | sd_file_system = get_filesystem_of_path(data_root); 314 | if (sd_file_system == NULL) { 315 | log_error("Couldn't identify the file system of the SD card at %s", data_root); 316 | set_status_parameter(param_handle, STATUS_NO_SD_CARD); 317 | return false; 318 | } 319 | 320 | if (strcmp(sd_file_system, "vfat") == 0 || strcmp(sd_file_system, "exfat") == 0) { 321 | log_error( 322 | "The SD card at %s uses file system %s which does not support " 323 | "Unix file permissions. Please reformat to a file system that " 324 | "support Unix file permissions, such as ext4 or xfs.", 325 | data_root, 326 | sd_file_system); 327 | set_status_parameter(param_handle, STATUS_SD_CARD_WRONG_FS); 328 | return false; 329 | } 330 | 331 | if (access(data_root, F_OK) == 0 && access(data_root, W_OK) != 0) { 332 | log_error( 333 | "The application user does not have write permissions to the SD " 334 | "card directory at %s. Please change the directory permissions or " 335 | "remove the directory.", 336 | data_root); 337 | set_status_parameter(param_handle, STATUS_SD_CARD_WRONG_PERMISSION); 338 | return false; 339 | } 340 | 341 | return true; 342 | } 343 | 344 | static bool 345 | is_parameter_equal_to(AXParameter* param_handle, const char* name, const char* value_to_equal) { 346 | g_autofree char* value = get_parameter_value(param_handle, name); 347 | return value && strcmp(value, value_to_equal) == 0; 348 | } 349 | 350 | // A parameter of type "bool:no,yes" is guaranteed to contain one of those 351 | // strings, but user code is still needed to interpret it as a Boolean type. 352 | static bool is_parameter_yes(AXParameter* param_handle, const char* name) { 353 | return is_parameter_equal_to(param_handle, name, "yes"); 354 | } 355 | 356 | static bool is_app_log_level_debug(AXParameter* param_handle) { 357 | return is_parameter_equal_to(param_handle, PARAM_APPLICATION_LOG_LEVEL, "debug"); 358 | } 359 | 360 | // Return data root matching the current SDCardSupport selection. 361 | // Call set_status_parameter() and return NULL on error. 362 | // 363 | // If SDCardSupport is "yes", data root will be located on the proved SD card 364 | // area. Passing NULL as SD card area signals that the SD card is not available. 365 | static char* prepare_data_root(AXParameter* param_handle, const char* sd_card_area) { 366 | if (is_parameter_yes(param_handle, PARAM_SD_CARD_SUPPORT)) { 367 | if (!sd_card_area) { 368 | log_warning("SD card was requested, but no SD card is available at the moment."); 369 | set_status_parameter(param_handle, STATUS_NO_SD_CARD); 370 | return NULL; 371 | } 372 | char* data_root = g_strdup_printf("%s/data", sd_card_area); 373 | if (!setup_sdcard(param_handle, data_root)) { 374 | free(data_root); 375 | return NULL; 376 | } 377 | return data_root; 378 | } else { 379 | return g_strdup_printf("%s/data", APP_LOCALDATA); // Use app-localdata if no SD Card 380 | } 381 | } 382 | 383 | // Read UseTLS parameter and verify that TLS files are present. Call set_status_parameter() and 384 | // return false on error. 385 | static gboolean get_and_verify_tls_selection(AXParameter* param_handle, bool* use_tls_ret) { 386 | const bool use_tls = is_parameter_yes(param_handle, PARAM_USE_TLS); 387 | 388 | if (use_tls && tls_missing_certs()) { 389 | tls_log_missing_cert_warnings(); 390 | set_status_parameter(param_handle, STATUS_TLS_CERT_MISSING); 391 | return false; 392 | } 393 | 394 | *use_tls_ret = use_tls; 395 | return true; 396 | } 397 | 398 | // Meant to be used as a one-shot call from g_timeout_add_seconds() 399 | static gboolean quit_main_loop(void*) { 400 | main_loop_quit(); 401 | return FALSE; 402 | } 403 | 404 | // Read and verify consistency of settings. Call set_status_parameter() or quit_program() and return 405 | // false on error. 406 | static bool read_settings(struct settings* settings, const struct app_state* app_state) { 407 | AXParameter* param_handle = app_state->param_handle; 408 | settings->use_tcp_socket = is_parameter_yes(param_handle, PARAM_TCP_SOCKET); 409 | 410 | if (!settings->use_tcp_socket) 411 | // Even if the user has selected UseTLS we do not need to check the certs 412 | // when TCP won't be used. If the setting is changed we will loop through 413 | // this function again. 414 | settings->use_tls = false; 415 | else if (!get_and_verify_tls_selection(param_handle, &settings->use_tls)) 416 | return false; 417 | 418 | settings->use_ipc_socket = is_parameter_yes(param_handle, PARAM_IPC_SOCKET); 419 | 420 | if (!settings->use_ipc_socket && !settings->use_tcp_socket) { 421 | log_error( 422 | "At least one of IPC socket or TCP socket must be set to \"yes\". " 423 | "dockerd will not be started."); 424 | set_status_parameter(param_handle, STATUS_NO_SOCKET); 425 | return false; 426 | } 427 | 428 | if (settings->use_ipc_socket && with_compose() && !let_other_apps_use_our_ipc_socket()) { 429 | quit_program(EX_SOFTWARE); 430 | return false; 431 | } 432 | 433 | // It takes a few seconds from sd_disk_storage_init() until sd_card_callback(), which is when 434 | // app_state->sd_card_area is set. Waiting here means we may avoid failure in the call to 435 | // prepare_data_root() below. 436 | if (is_parameter_yes(param_handle, PARAM_SD_CARD_SUPPORT) && !app_state->sd_card_area) { 437 | int id = g_timeout_add_seconds(5, quit_main_loop, NULL); 438 | g_main_loop_run(loop); // Wait until the timer or sd_card_callback() calls main_loop_quit() 439 | g_source_remove(id); // If it was sd_card_callback(), the timer must not restart dockerd. 440 | } 441 | 442 | if (!(settings->data_root = prepare_data_root(param_handle, app_state->sd_card_area))) 443 | return false; 444 | 445 | return true; 446 | } 447 | 448 | static struct exit_cause child_process_exit_cause(int status, GError** error) { 449 | struct exit_cause result; 450 | result.code = -1; 451 | result.signal = 0; 452 | 453 | if (g_spawn_check_wait_status(status, error) || (*error)->domain == G_SPAWN_EXIT_ERROR) 454 | result.code = *error ? (*error)->code : 0; 455 | else if ((*error)->domain == G_SPAWN_ERROR && (*error)->code == G_SPAWN_ERROR_FAILED) 456 | result.signal = status; 457 | 458 | return result; 459 | } 460 | 461 | static void log_child_process_exit_cause(const char* name, GPid pid, int status) { 462 | GError* error = NULL; 463 | struct exit_cause exit_cause = child_process_exit_cause(status, &error); 464 | 465 | char msg[128]; 466 | const char* end = msg + sizeof(msg); 467 | char* ptr = msg + g_snprintf(msg, end - msg, "Child process %s (%d)", name, pid); 468 | if (exit_cause.code >= 0) 469 | g_snprintf(ptr, end - ptr, " exited with exit code %d", exit_cause.code); 470 | else if (exit_cause.signal > 0) 471 | g_snprintf(ptr, end - ptr, " was killed by signal %d", exit_cause.signal); 472 | else 473 | g_snprintf(ptr, end - ptr, " terminated in an unexpected way: %s", error->message); 474 | g_clear_error(&error); 475 | log_debug("%s", msg); 476 | } 477 | 478 | static bool child_process_exited_with_error(int status) { 479 | GError* error = NULL; 480 | struct exit_cause exit_cause = child_process_exit_cause(status, &error); 481 | g_clear_error(&error); 482 | return exit_cause.code > 0; 483 | } 484 | 485 | static void 486 | check_child_process_exit_code_and_clean_up(GPid pid, gint status, gpointer app_state_void_ptr) { 487 | log_child_process_exit_cause("rootlesskit", pid, status); 488 | 489 | struct app_state* app_state = app_state_void_ptr; 490 | 491 | bool runtime_error = child_process_exited_with_error(status); 492 | allow_dockerd_to_start(app_state, !runtime_error); 493 | status_code_t s = runtime_error ? STATUS_DOCKERD_RUNTIME_ERROR : STATUS_DOCKERD_STOPPED; 494 | set_status_parameter(app_state->param_handle, s); 495 | 496 | rootlesskit_pid = 0; 497 | g_spawn_close_pid(pid); 498 | 499 | remove_docker_pid_file(); // Might have been left behind if dockerd crashed. 500 | 501 | prevent_others_from_using_our_ipc_socket(); 502 | 503 | main_loop_quit(); // Trigger a restart of dockerd from main() 504 | } 505 | 506 | // Return a command line with space-delimited argument based on the current settings. 507 | static const char* build_daemon_args(const struct settings* settings, AXParameter* param_handle) { 508 | static gchar args[1024]; // Pointer to args returned to caller on success. 509 | const char* args_end = args + sizeof(args); 510 | char* args_wr = args; // Points to location of next write 511 | 512 | const char* data_root = settings->data_root; 513 | const bool use_tls = settings->use_tls; 514 | const bool use_tcp_socket = settings->use_tcp_socket; 515 | const bool use_ipc_socket = settings->use_ipc_socket; 516 | 517 | gsize msg_len = 256; 518 | gchar msg[msg_len]; 519 | 520 | g_autofree char* log_level = get_parameter_value(param_handle, PARAM_DOCKERD_LOG_LEVEL); 521 | 522 | // get host ip 523 | char host_buffer[256]; 524 | char* IPbuffer; 525 | struct hostent* host_entry; 526 | gethostname(host_buffer, sizeof(host_buffer)); 527 | host_entry = gethostbyname(host_buffer); 528 | IPbuffer = inet_ntoa(*((struct in_addr*)host_entry->h_addr_list[0])); 529 | 530 | // construct the rootlesskit command 531 | args_wr += g_snprintf(args_wr, 532 | args_end - args_wr, 533 | "%s %s %s %s %s %s %s %s %s", 534 | "rootlesskit", 535 | "--subid-source=static", 536 | "--net=slirp4netns", 537 | "--disable-host-loopback", 538 | "--copy-up=/etc", 539 | "--copy-up=/run", 540 | "--propagation=rslave", 541 | "--port-driver slirp4netns", 542 | /* don't use same range as company proxy */ 543 | "--cidr=10.0.3.0/24"); 544 | 545 | if (strcmp(log_level, "debug") == 0) { 546 | args_wr += g_snprintf(args_wr, args_end - args_wr, " %s", "--debug"); 547 | } 548 | 549 | const uint port = use_tls ? 2376 : 2375; 550 | args_wr += g_snprintf(args_wr, args_end - args_wr, " -p %s:%d:%d/tcp", IPbuffer, port, port); 551 | 552 | // add dockerd command 553 | args_wr += g_snprintf(args_wr, 554 | args_end - args_wr, 555 | " dockerd %s", 556 | "--config-file " APP_LOCALDATA "/" DAEMON_JSON); 557 | 558 | g_strlcpy(msg, "Starting dockerd", msg_len); 559 | 560 | args_wr += g_snprintf(args_wr, args_end - args_wr, " --log-level=%s", log_level); 561 | 562 | if (use_ipc_socket) { 563 | g_strlcat(msg, " with IPC socket and", msg_len); 564 | // The socket should reside in the user directory and have same group as user. 565 | // If omitted, dockerd will log a warning about the 'docker' group not being find. 566 | // However, rootlesskit maps the user's primary group to the root group, so "--group 0" 567 | // means the socket will belong to the user's primary group. 568 | g_autofree char* ipc_socket = xdg_runtime_file("docker.sock"); 569 | args_wr += g_snprintf(args_wr, args_end - args_wr, " --group 0 -H unix://%s", ipc_socket); 570 | } else { 571 | g_strlcat(msg, " without IPC socket and", msg_len); 572 | } 573 | 574 | if (use_tcp_socket) { 575 | g_strlcat(msg, " with TCP socket", msg_len); 576 | g_strlcat(msg, use_tls ? " in TLS mode" : " in unsecured mode", msg_len); 577 | const uint port = use_tls ? 2376 : 2375; 578 | args_wr += g_snprintf(args_wr, args_end - args_wr, " -H tcp://0.0.0.0:%d", port); 579 | const char* tls_arg = use_tls ? "--tlsverify=true" : "--tls=false"; 580 | args_wr += g_snprintf(args_wr, args_end - args_wr, " %s", tls_arg); 581 | if (use_tls) 582 | args_wr += g_snprintf(args_wr, args_end - args_wr, " %s", tls_file_dockerd_args()); 583 | } else { 584 | g_strlcat(msg, " without TCP socket", msg_len); 585 | } 586 | 587 | g_autofree char* data_root_msg = g_strdup_printf(" using %s as storage.", data_root); 588 | g_strlcat(msg, data_root_msg, msg_len); 589 | args_wr += g_snprintf(args_wr, args_end - args_wr, " --data-root %s", data_root); 590 | 591 | log_info("%s", msg); 592 | return args; 593 | } 594 | 595 | // Start dockerd. On success, call set_status_parameter(STATUS_RUNNING) and on error, 596 | // call set_status_parameter(STATUS_NOT_STARTED). 597 | static bool start_dockerd(const struct settings* settings, struct app_state* app_state) { 598 | AXParameter* param_handle = app_state->param_handle; 599 | GError* error = NULL; 600 | bool result = false; 601 | bool return_value = false; 602 | 603 | const char* args = build_daemon_args(settings, param_handle); 604 | 605 | log_debug("Sending daemon start command: %s", args); 606 | char** args_split = g_strsplit(args, " ", 0); 607 | result = g_spawn_async(NULL, 608 | args_split, 609 | NULL, 610 | G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_SEARCH_PATH, 611 | NULL, 612 | NULL, 613 | &rootlesskit_pid, 614 | &error); 615 | if (!result) { 616 | log_error("Starting dockerd failed: execv returned: %d, error: %s", result, error->message); 617 | set_status_parameter(param_handle, STATUS_NOT_STARTED); 618 | goto end; 619 | } 620 | log_debug("Child process rootlesskit (%d) was started.", rootlesskit_pid); 621 | 622 | g_child_watch_add(rootlesskit_pid, check_child_process_exit_code_and_clean_up, app_state); 623 | 624 | set_status_parameter(param_handle, STATUS_RUNNING); 625 | return_value = true; 626 | 627 | end: 628 | g_strfreev(args_split); 629 | g_clear_error(&error); 630 | return return_value; 631 | } 632 | 633 | static void read_settings_and_start_dockerd(struct app_state* app_state) { 634 | struct settings settings = {0}; 635 | 636 | if (read_settings(&settings, app_state)) 637 | start_dockerd(&settings, app_state); 638 | 639 | free(settings.data_root); 640 | } 641 | 642 | static bool send_signal(const char* name, GPid pid, int sig) { 643 | log_debug("Sending SIG%s to %s (%d)", sigabbrev_np(sig), name, pid); 644 | if (kill(pid, sig) != 0) { 645 | log_error("Failed to send %s to %s (%d)", sigdescr_np(sig), name, pid); 646 | return FALSE; 647 | } 648 | return TRUE; 649 | } 650 | 651 | // Check if dockerd is still running. Launch this function using g_timeout_add_seconds() and pass a 652 | // pointer to a counter starting at 1. When dockerd has terminated, the counter will be set to zero. 653 | // Otherwise, it will be increased, and SIGTERM will be sent on the 20th call. 654 | static gboolean monitor_dockerd_termination(void* time_since_sigterm_void_ptr) { 655 | // dockerd usually sends SIGTERM to containers after 10 s, so we must wait a bit longer. 656 | const int time_to_wait_before_sigkill = 20; 657 | int* time_since_sigterm = (int*)time_since_sigterm_void_ptr; 658 | if (!rootlesskit_pid) { 659 | log_debug("rootlesskit exited after %d s", *time_since_sigterm); 660 | *time_since_sigterm = 0; // Tell caller that timer has ended. 661 | g_main_loop_quit(loop); // Release caller from its main loop. 662 | return FALSE; // Tell GLib that timer shall end. 663 | } else { 664 | log_debug("rootlesskit (%d) still running %d s after SIGTERM", 665 | rootlesskit_pid, 666 | *time_since_sigterm); 667 | (*time_since_sigterm)++; 668 | if (*time_since_sigterm > time_to_wait_before_sigkill) 669 | // Send SIGKILL but still wait for the process exit callback to clear the pid variable. 670 | send_signal("rootlesskit", rootlesskit_pid, SIGKILL); 671 | return TRUE; // Tell GLib to call timer again. 672 | } 673 | } 674 | 675 | // Send SIGTERM to dockerd, wait for it to terminate. 676 | // Send SIGKILL if that fails, but still wait for it to terminate. 677 | static void stop_dockerd(void) { 678 | if (!is_process_alive(rootlesskit_pid)) 679 | return; 680 | 681 | send_signal("rootlesskit", rootlesskit_pid, SIGTERM); 682 | 683 | int time_since_sigterm = 1; 684 | g_timeout_add_seconds(1, monitor_dockerd_termination, &time_since_sigterm); 685 | while (time_since_sigterm != 0) { // Loop until the timer callback has stopped running 686 | g_main_loop_run(loop); 687 | } 688 | log_info("Stopped dockerd."); 689 | } 690 | 691 | // Meant to be used as an AXParameter callback 692 | static void restart_dockerd_when_parameter_changed(const gchar* name, 693 | const gchar* value, 694 | gpointer app_state_void_ptr) { 695 | const gchar* parname = name += strlen("root." APP_NAME "."); 696 | 697 | log_info("%s changed to %s", parname, value); 698 | 699 | struct app_state* app_state = app_state_void_ptr; 700 | 701 | // If dockerd has failed before, this parameter change may have resolved the problem. 702 | allow_dockerd_to_start(app_state, true); 703 | 704 | // Trigger a restart of dockerd from main(), but delay it 1 second. 705 | // When there are multiple AXParameter callbacks in a queue, such as 706 | // during the first parameter change after installation, any parameter 707 | // usage, even outside a callback, will cause a 20 second deadlock per 708 | // queued callback. 709 | g_timeout_add_seconds(1, quit_main_loop, NULL); 710 | } 711 | 712 | static AXParameter* setup_axparameter(struct app_state* app_state) { 713 | bool success = false; 714 | GError* error = NULL; 715 | AXParameter* ax_parameter = ax_parameter_new(APP_NAME, &error); 716 | if (ax_parameter == NULL) { 717 | log_error("Error when creating AXParameter: %s", error->message); 718 | goto end; 719 | } 720 | 721 | for (const char** param = params_that_restart_dockerd; *param; param++) { 722 | if (!ax_parameter_register_callback(ax_parameter, 723 | *param, 724 | restart_dockerd_when_parameter_changed, 725 | app_state, 726 | &error)) { 727 | log_error("Could not register %s callback. Error: %s", *param, error->message); 728 | goto end; 729 | } 730 | } 731 | 732 | success = true; 733 | 734 | end: 735 | g_clear_error(&error); 736 | if (!success && ax_parameter != NULL) { 737 | ax_parameter_free(ax_parameter); 738 | ax_parameter = NULL; 739 | } 740 | return ax_parameter; 741 | } 742 | 743 | static void sd_card_callback(const char* sd_card_area, void* app_state_void_ptr) { 744 | struct app_state* app_state = app_state_void_ptr; 745 | const bool using_sd_card = is_parameter_yes(app_state->param_handle, PARAM_SD_CARD_SUPPORT); 746 | if (using_sd_card && !sd_card_area) { 747 | stop_dockerd(); // Block here until dockerd has stopped using the SD card. 748 | set_status_parameter(app_state->param_handle, STATUS_NO_SD_CARD); 749 | } 750 | app_state->sd_card_area = sd_card_area ? strdup(sd_card_area) : NULL; 751 | if (using_sd_card) 752 | main_loop_quit(); // Trigger a restart of dockerd from main() 753 | } 754 | 755 | static void restart_dockerd_after_file_upload(struct app_state* app_state) { 756 | // If dockerd has failed before, this file upload may have resolved the problem. 757 | allow_dockerd_to_start(app_state, true); 758 | 759 | main_loop_quit(); 760 | } 761 | 762 | // Stop the application and start it from an SSH prompt with 763 | // $ ./dockerdwrapper --stdout 764 | // in order to get log messages written to console rather than to syslog. 765 | static void parse_command_line(int argc, char** argv, struct log_settings* log_settings) { 766 | log_settings->destination = 767 | (argc == 2 && strcmp(argv[1], "--stdout") == 0) ? log_dest_stdout : log_dest_syslog; 768 | } 769 | 770 | static bool set_env_variable(const char* env_var, const char* value) { 771 | log_debug("Setting env: %s=%s", env_var, value); 772 | if (setenv(env_var, value, 1) != 0) { 773 | log_error("Error setting env variable %s to %s", env_var, value); 774 | return false; 775 | } 776 | return true; 777 | } 778 | 779 | static bool set_env_variables(void) { 780 | uid_t uid = getuid(); 781 | g_autofree char* path = 782 | g_strdup_printf("/bin:/usr/bin:%s:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin", 783 | APP_DIRECTORY); 784 | g_autofree char* xdg_runtime_dir = xdg_runtime_directory(); 785 | 786 | return set_env_variable("PATH", path) && set_env_variable("HOME", APP_DIRECTORY) && 787 | set_env_variable("XDG_RUNTIME_DIR", xdg_runtime_dir) && 788 | set_env_variable("XTABLES_LOCKFILE", TMP_LOCKFILE); 789 | } 790 | 791 | int main(int argc, char** argv) { 792 | struct app_state app_state = {0}; 793 | struct log_settings log_settings = {0}; 794 | 795 | loop = g_main_loop_new(NULL, FALSE); 796 | 797 | parse_command_line(argc, argv, &log_settings); 798 | log_init(&log_settings); 799 | 800 | allow_dockerd_to_start(&app_state, true); 801 | 802 | app_state.param_handle = setup_axparameter(&app_state); 803 | if (!app_state.param_handle) 804 | return EX_SOFTWARE; 805 | 806 | log_debug_set(is_app_log_level_debug(app_state.param_handle)); 807 | 808 | if (!set_env_variables()) 809 | return EX_SOFTWARE; 810 | 811 | init_signals(); 812 | 813 | struct restart_dockerd_context restart_dockerd_context; 814 | restart_dockerd_context.restart_dockerd = restart_dockerd_after_file_upload; 815 | restart_dockerd_context.app_state = &app_state; 816 | int fcgi_error = fcgi_start(http_request_callback, &restart_dockerd_context); 817 | if (fcgi_error) 818 | return fcgi_error; 819 | 820 | struct sd_disk_storage* sd_disk_storage = sd_disk_storage_init(sd_card_callback, &app_state); 821 | 822 | while (application_exit_code == EX_KEEP_RUNNING) { 823 | if (!rootlesskit_pid && dockerd_allowed_to_start(&app_state)) 824 | read_settings_and_start_dockerd(&app_state); 825 | 826 | main_loop_run(); 827 | 828 | log_debug_set(is_app_log_level_debug(app_state.param_handle)); 829 | 830 | stop_dockerd(); 831 | } 832 | 833 | sd_disk_storage_free(sd_disk_storage); 834 | 835 | fcgi_stop(); 836 | 837 | set_status_parameter(app_state.param_handle, STATUS_NOT_STARTED); 838 | ax_parameter_free(app_state.param_handle); 839 | 840 | free(app_state.sd_card_area); 841 | 842 | main_loop_unref(); 843 | 844 | log_debug("Application exited with exit code %d", application_exit_code); 845 | return application_exit_code; 846 | } 847 | -------------------------------------------------------------------------------- /app/fcgi_server.c: -------------------------------------------------------------------------------- 1 | #include "fcgi_server.h" 2 | #include "log.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #define FCGI_SOCKET_NAME "FCGI_SOCKET_NAME" 12 | 13 | static const char* g_socket_path = NULL; 14 | static int g_socket = -1; 15 | static GThread* g_thread = NULL; 16 | 17 | struct request_context { 18 | fcgi_request_callback callback; 19 | void* parameter; 20 | }; 21 | 22 | static void* handle_fcgi(void* request_context_void_ptr) { 23 | g_autofree struct request_context* request_context = 24 | (struct request_context*)request_context_void_ptr; 25 | while (true) { 26 | FCGX_Request request = {}; 27 | FCGX_InitRequest(&request, g_socket, FCGI_FAIL_ACCEPT_ON_INTR); 28 | if (FCGX_Accept_r(&request) < 0) { 29 | // shutdown() was called on g_socket, which causes FCGX_Accept_r() to fail. 30 | log_debug("Stopping FCGI server, because FCGX_Accept_r() returned %s", strerror(errno)); 31 | return NULL; 32 | } 33 | request_context->callback(&request, request_context->parameter); 34 | } 35 | } 36 | 37 | int fcgi_start(fcgi_request_callback request_callback, void* request_callback_parameter) { 38 | log_debug("Starting FCGI server"); 39 | 40 | g_socket_path = getenv(FCGI_SOCKET_NAME); 41 | if (!g_socket_path) { 42 | log_error("Failed to get environment variable FCGI_SOCKET_NAME"); 43 | return EX_SOFTWARE; 44 | } 45 | 46 | if (FCGX_Init() != 0) { 47 | log_error("FCGX_Init failed: %s", strerror(errno)); 48 | return EX_SOFTWARE; 49 | } 50 | 51 | if ((g_socket = FCGX_OpenSocket(g_socket_path, 5)) < 0) { 52 | log_error("FCGX_OpenSocket failed: %s", strerror(errno)); 53 | return EX_SOFTWARE; 54 | } 55 | chmod(g_socket_path, S_IRWXU | S_IRWXG | S_IRWXO); 56 | 57 | /* Create a thread for request handling */ 58 | struct request_context* request_context = malloc(sizeof(struct request_context)); 59 | request_context->callback = request_callback; 60 | request_context->parameter = request_callback_parameter; 61 | if ((g_thread = g_thread_new("fcgi_server", &handle_fcgi, request_context)) == NULL) { 62 | log_error("Failed to launch FCGI server thread"); 63 | return EX_SOFTWARE; 64 | } 65 | 66 | log_debug("Launched FCGI server thread."); 67 | return EX_OK; 68 | } 69 | 70 | void fcgi_stop(void) { 71 | log_debug("Stopping FCGI server."); 72 | FCGX_ShutdownPending(); 73 | 74 | if (g_socket != -1) { 75 | log_debug("Closing and removing FCGI socket."); 76 | if (shutdown(g_socket, SHUT_RD) != 0) { 77 | log_warning("Could not shutdown socket, err: %s", strerror(errno)); 78 | } 79 | if (unlink(g_socket_path) != 0) { 80 | log_warning("Could not unlink socket, err: %s", strerror(errno)); 81 | } 82 | } 83 | log_debug("Joining FCGI server thread."); 84 | g_thread_join(g_thread); 85 | 86 | g_socket_path = NULL; 87 | g_socket = -1; 88 | g_thread = NULL; 89 | log_debug("FCGI server has stopped."); 90 | } 91 | -------------------------------------------------------------------------------- /app/fcgi_server.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | typedef void (*fcgi_request_callback)(FCGX_Request* request, void* userdata); 5 | 6 | int fcgi_start(fcgi_request_callback request_callback, void* request_callback_parameter); 7 | void fcgi_stop(void); 8 | -------------------------------------------------------------------------------- /app/fcgi_write_file_from_stream.c: -------------------------------------------------------------------------------- 1 | #include "fcgi_write_file_from_stream.h" 2 | #include "fcgi_server.h" 3 | #include "log.h" 4 | #include 5 | 6 | static int request_content_length(const FCGX_Request* request) { 7 | const char* content_length_str = FCGX_GetParam("CONTENT_LENGTH", request->envp); 8 | if (!content_length_str) 9 | return 0; 10 | return strtol(content_length_str, NULL, 10); 11 | } 12 | 13 | char* fcgi_write_file_from_stream(FCGX_Request request) { 14 | char* temp_file = NULL; 15 | const int content_length = request_content_length(&request); 16 | const char* content_type = FCGX_GetParam("CONTENT_TYPE", request.envp); 17 | 18 | log_debug("Content-Type: %s", content_type); 19 | 20 | const char* MULTIPART_FORM_DATA = "multipart/form-data"; 21 | if (strncmp(content_type, MULTIPART_FORM_DATA, sizeof(MULTIPART_FORM_DATA) - 1) != 0) { 22 | log_error("Content type \"%s\" is not supported. Use \"%s\" instead.", 23 | content_type, 24 | MULTIPART_FORM_DATA); 25 | return NULL; 26 | } 27 | 28 | const char* BOUNDARY_KEY = "boundary="; 29 | const char* boundary_text = strstr(content_type, BOUNDARY_KEY); 30 | if (!boundary_text) { 31 | log_error("No multipart boundary found in content-type \"%s\".", content_type); 32 | return NULL; 33 | } 34 | boundary_text += strlen(BOUNDARY_KEY); 35 | const int boundary_len = strlen(boundary_text); 36 | 37 | temp_file = g_strdup_printf("/tmp/fcgi_upload.XXXXXX"); 38 | int file_des = mkstemp(temp_file); 39 | if (file_des == -1) { 40 | log_error("Failed to create %s, err %s.", temp_file, strerror(errno)); 41 | return NULL; 42 | } 43 | log_debug("Opened %s for writing.", temp_file); 44 | 45 | bool remove_temp_file = true; // Clear this to return the filename to the caller. 46 | 47 | const int bufferLen = 2048; 48 | char buffer[bufferLen + 1 /* Allow for NULL termination */]; 49 | 50 | const char* data_start = "\r\n\r\n"; 51 | const char* data_end = "\r\n--"; 52 | 53 | int total_bytes_processed = 0; 54 | bool pre_boundary_found = false; 55 | bool post_boundary_found = false; 56 | 57 | int loop_counter = 0; // First iteration is special. 58 | char* p_payload = buffer; 59 | char* p_payload_end = NULL; 60 | 61 | while (total_bytes_processed < content_length) { 62 | loop_counter++; 63 | const int available_len = bufferLen - (p_payload - buffer); 64 | 65 | const int bytes_read = FCGX_GetStr(p_payload, available_len, request.in); 66 | log_debug("FCGX_GetStr: bytes_read %d, p_payload %p(%d), available_len %d(%d)", 67 | bytes_read, 68 | p_payload, 69 | (int)(p_payload - buffer), 70 | available_len, 71 | available_len - bufferLen); 72 | if (bytes_read < 0) { 73 | log_error("Failed to read from FCGI stream: %s", strerror(errno)); 74 | break; 75 | } 76 | 77 | /* Look for pre boundary */ 78 | if (!pre_boundary_found) { 79 | buffer[bytes_read] = 0; /* NULL terminate */ 80 | p_payload = strstr(buffer + boundary_len + 1, data_start); 81 | if (p_payload == NULL) { 82 | log_error("Failed to find boundary in uploaded data."); 83 | } 84 | pre_boundary_found = true; 85 | p_payload += strlen(data_start); 86 | } else { 87 | log_debug("Pre boundary already found"); 88 | p_payload = buffer; 89 | } 90 | 91 | /* Look for post boundary */ 92 | if (!post_boundary_found) { 93 | char* pchar; 94 | for (pchar = (loop_counter == 1) ? p_payload : buffer; 95 | pchar < buffer + bytes_read - ((loop_counter == 1) ? boundary_len : 0); 96 | pchar++) { 97 | if (memcmp(pchar, boundary_text, boundary_len) == 0) { 98 | log_debug("Post boundary found for %.*s", boundary_len, pchar); 99 | pchar -= strlen(data_end); 100 | if (memcmp(pchar, data_end, strlen(data_end)) != 0) { 101 | log_error("Post boundary data end not found"); 102 | log_debug("Found %02x%02x%02x%02x at post boundary", 103 | pchar[0], 104 | pchar[1], 105 | pchar[2], 106 | pchar[3]); 107 | goto end; 108 | } 109 | post_boundary_found = true; 110 | break; 111 | } 112 | } 113 | p_payload_end = pchar; 114 | } 115 | 116 | int to_write = p_payload_end - p_payload; 117 | int written = 0; 118 | while (to_write > 0) { 119 | if ((written = write(file_des, p_payload + written, to_write)) < 0) { 120 | log_error("Failed to write %d bytes to %s: %s", 121 | to_write, 122 | temp_file, 123 | strerror(errno)); 124 | goto end; 125 | } 126 | total_bytes_processed += written; 127 | to_write -= written; 128 | } 129 | log_debug("write: p_payload %p, %d bytes", p_payload, (int)(p_payload_end - p_payload)); 130 | log_debug("loop %d, bytes_read %d, done %d", 131 | loop_counter, 132 | bytes_read, 133 | total_bytes_processed); 134 | 135 | if (post_boundary_found) { 136 | total_bytes_processed = content_length; 137 | remove_temp_file = false; // File has been successfully received. 138 | } else { 139 | if (!pre_boundary_found) { 140 | log_error("No pre boundary found"); 141 | goto end; 142 | } 143 | if (bytes_read != available_len) { 144 | log_error("No post boundary found"); 145 | goto end; 146 | } 147 | 148 | /* Post boundary may have been partial at payload end. Ensure possible rematch */ 149 | p_payload = buffer + boundary_len; 150 | memcpy(buffer, p_payload_end, boundary_len); 151 | } 152 | } 153 | 154 | end: 155 | if (file_des != -1) { 156 | log_debug("Closing %s after writing %ld bytes.", temp_file, lseek(file_des, 0, SEEK_CUR)); 157 | if (close(file_des) == -1) 158 | log_warning("Failed to close %s: %s", temp_file, strerror(errno)); 159 | } 160 | if (remove_temp_file && temp_file) { 161 | if (unlink(temp_file) != 0) 162 | log_error("Failed to remove %s: %s", temp_file, strerror(errno)); 163 | g_free(temp_file); 164 | temp_file = NULL; 165 | } 166 | return temp_file; 167 | } 168 | -------------------------------------------------------------------------------- /app/fcgi_write_file_from_stream.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | // Given a request with multipart/form-data, store incoming data in a file on /tmp. On success, 5 | // return the filename and let the caller do all cleanup. On failure, log the error, clean up the 6 | // file and return NULL. 7 | char* fcgi_write_file_from_stream(FCGX_Request request); 8 | -------------------------------------------------------------------------------- /app/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Docker Daemon Usage 4 | 5 |

Docker Daemon Usage

6 |

Upload TLS certificates and keys

7 | 8 | curl --anyauth -u $DEVICE_USER:$DEVICE_PASSWORD -F file=@ca.pem -X POST http://$DEVICE_IP/local/dockerdwrapper/ca.pem
9 | curl --anyauth -u $DEVICE_USER:$DEVICE_PASSWORD -F file=@server-cert.pem -X POST http://$DEVICE_IP/local/dockerdwrapper/server-cert.pem
10 | curl --anyauth -u $DEVICE_USER:$DEVICE_PASSWORD -F file=@server-key.pem -X POST http://$DEVICE_IP/local/dockerdwrapper/server-key.pem
11 |
12 |

Remove TLS certificates and keys

13 | 14 | curl --anyauth -u $DEVICE_USER:$DEVICE_PASSWORD -X DELETE http://$DEVICE_IP/local/dockerdwrapper/ca.pem
15 | curl --anyauth -u $DEVICE_USER:$DEVICE_PASSWORD -X DELETE http://$DEVICE_IP/local/dockerdwrapper/server-cert.pem
16 | curl --anyauth -u $DEVICE_USER:$DEVICE_PASSWORD -X DELETE http://$DEVICE_IP/local/dockerdwrapper/server-key.pem
17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /app/http_request.c: -------------------------------------------------------------------------------- 1 | #include "http_request.h" 2 | #include "app_paths.h" 3 | #include "fcgi_write_file_from_stream.h" 4 | #include "log.h" 5 | #include "tls.h" 6 | #include 7 | #include 8 | 9 | #define HTTP_200_OK "200 OK" 10 | #define HTTP_204_NO_CONTENT "204 No Content" 11 | #define HTTP_400_BAD_REQUEST "400 Bad Request" 12 | #define HTTP_404_NOT_FOUND "404 Not Found" 13 | #define HTTP_405_METHOD_NOT_ALLOWED "405 Method Not Allowed" 14 | #define HTTP_422_UNPROCESSABLE_CONTENT "422 Unprocessable Content" 15 | #define HTTP_500_INTERNAL_SERVER_ERROR "500 Internal Server Error" 16 | 17 | static char* localdata_full_path(const char* filename) { 18 | return g_strdup_printf("%s/%s", APP_LOCALDATA, filename); 19 | } 20 | 21 | static bool copy_to_localdata(const char* source_path, const char* destination_filename) { 22 | g_autofree char* full_path = localdata_full_path(destination_filename); 23 | log_debug("Copying %s to %s.", source_path, full_path); 24 | 25 | GFile* source = g_file_new_for_path(source_path); 26 | GFile* destination = g_file_new_for_path(full_path); 27 | GError* error = NULL; 28 | bool success = 29 | g_file_copy(source, destination, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, &error); 30 | if (!success) 31 | log_error("Failed to copy %s to %s: %s.", source_path, full_path, error->message); 32 | g_object_unref(source); 33 | g_object_unref(destination); 34 | g_clear_error(&error); 35 | 36 | return success; 37 | } 38 | 39 | static bool exists_in_localdata(const char* filename) { 40 | g_autofree char* full_path = localdata_full_path(filename); 41 | struct stat sb; 42 | return stat(full_path, &sb) == 0; 43 | } 44 | 45 | static bool remove_from_localdata(const char* filename) { 46 | g_autofree char* full_path = localdata_full_path(filename); 47 | log_debug("Removing %s.", full_path); 48 | bool success = !unlink(full_path); 49 | if (!success) 50 | // Log as warning rather than error, since 'No such file' is also treated as a failure. 51 | log_warning("Failed to remove %s: %s.", filename, strerror(errno)); 52 | return success; 53 | } 54 | 55 | static void 56 | response(FCGX_Request* request, const char* status, const char* content_type, const char* body) { 57 | FCGX_FPrintF(request->out, 58 | "Status: %s\r\n" 59 | "Content-Type: %s\r\n\r\n" 60 | "%s", 61 | status, 62 | content_type, 63 | body); 64 | } 65 | 66 | static void response_204_no_content(FCGX_Request* request) { 67 | const char* status = HTTP_204_NO_CONTENT; 68 | log_debug("Send response %s", status); 69 | FCGX_FPrintF(request->out, "Status: %s\r\n\r\n", status); 70 | } 71 | 72 | static void response_msg(FCGX_Request* request, const char* status, const char* message) { 73 | log_debug("Send response %s: %s", status, message); 74 | g_autofree char* body = g_strdup_printf("%s\r\n", message); 75 | response(request, status, "text/plain", body); 76 | } 77 | 78 | static void post_request(FCGX_Request* request, 79 | const char* filename, 80 | struct restart_dockerd_context* restart_dockerd_context) { 81 | g_autofree char* temp_file = fcgi_write_file_from_stream(*request); 82 | if (!temp_file) { 83 | response_msg(request, HTTP_422_UNPROCESSABLE_CONTENT, "Upload to temporary file failed."); 84 | return; 85 | } 86 | if (!tls_file_has_correct_format(filename, temp_file)) { 87 | g_autofree char* msg = 88 | g_strdup_printf("File is not a valid %s.", tls_file_description(filename)); 89 | response_msg(request, HTTP_400_BAD_REQUEST, msg); 90 | } else if (!copy_to_localdata(temp_file, filename)) 91 | response_msg(request, HTTP_500_INTERNAL_SERVER_ERROR, "Failed to copy file to localdata"); 92 | else { 93 | response_204_no_content(request); 94 | restart_dockerd_context->restart_dockerd(restart_dockerd_context->app_state); 95 | } 96 | 97 | if (unlink(temp_file) != 0) 98 | log_error("Failed to remove %s: %s", temp_file, strerror(errno)); 99 | } 100 | 101 | static void delete_request(FCGX_Request* request, const char* filename) { 102 | if (!exists_in_localdata(filename)) 103 | response_msg(request, HTTP_404_NOT_FOUND, "File not found in localdata"); 104 | else if (!remove_from_localdata(filename)) 105 | response_msg(request, 106 | HTTP_500_INTERNAL_SERVER_ERROR, 107 | "Failed to remove file from localdata"); 108 | else 109 | response_204_no_content(request); 110 | } 111 | 112 | static void unsupported_request(FCGX_Request* request, const char* method, const char* filename) { 113 | log_error("Unsupported request %s %s", method, filename); 114 | response_msg(request, HTTP_405_METHOD_NOT_ALLOWED, "Unsupported request method"); 115 | } 116 | 117 | static void malformed_request(FCGX_Request* request, const char* method, const char* uri) { 118 | log_error("Malformed request %s %s", method, uri); 119 | response_msg(request, HTTP_400_BAD_REQUEST, "Malformed request"); 120 | } 121 | 122 | void http_request_callback(FCGX_Request* request, void* restart_dockerd_context_void_ptr) { 123 | const char* method = FCGX_GetParam("REQUEST_METHOD", request->envp); 124 | const char* uri = FCGX_GetParam("REQUEST_URI", request->envp); 125 | 126 | log_info("Processing HTTP request %s %s", method, uri); 127 | 128 | const char* filename = strrchr(uri, '/'); 129 | if (!filename) { 130 | malformed_request(request, method, uri); 131 | } else { 132 | filename++; // Strip leading '/' 133 | 134 | if (strcmp(method, "POST") == 0) 135 | post_request(request, filename, restart_dockerd_context_void_ptr); 136 | else if (strcmp(method, "DELETE") == 0) 137 | delete_request(request, filename); 138 | else 139 | unsupported_request(request, method, filename); 140 | } 141 | FCGX_Finish_r(request); 142 | } 143 | -------------------------------------------------------------------------------- /app/http_request.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | struct app_state; 5 | 6 | typedef void (*restart_dockerd_t)(struct app_state*); 7 | 8 | struct restart_dockerd_context { 9 | restart_dockerd_t restart_dockerd; 10 | struct app_state* app_state; 11 | }; 12 | 13 | // Callback function called from a thread by the FCGI server 14 | void http_request_callback(FCGX_Request* request, void* restart_dockerd_context_void_ptr); 15 | -------------------------------------------------------------------------------- /app/log.c: -------------------------------------------------------------------------------- 1 | #include "log.h" 2 | #include 3 | #include 4 | 5 | static volatile int debug_log_enabled; // Accessed using g_atomic_int_get/set only 6 | 7 | static int log_level_to_syslog_priority(GLogLevelFlags log_level) { 8 | if (log_level == G_LOG_LEVEL_NON_FATAL_ERROR) 9 | log_level = G_LOG_LEVEL_ERROR; 10 | 11 | switch (log_level) { 12 | case G_LOG_LEVEL_DEBUG: 13 | return LOG_INFO; // ... since LOG_DEBUG doesn't show up in syslog 14 | case G_LOG_LEVEL_INFO: 15 | return LOG_INFO; 16 | case G_LOG_LEVEL_WARNING: 17 | return LOG_WARNING; 18 | case G_LOG_LEVEL_ERROR: 19 | return LOG_ERR; 20 | case G_LOG_LEVEL_CRITICAL: 21 | return LOG_CRIT; 22 | default: 23 | return LOG_NOTICE; 24 | } 25 | } 26 | 27 | // String representation has been chosen to match that of dockerd 28 | static const char* log_level_to_string(GLogLevelFlags log_level) { 29 | if (log_level == G_LOG_LEVEL_NON_FATAL_ERROR) 30 | log_level = G_LOG_LEVEL_ERROR; 31 | 32 | switch (log_level) { 33 | case G_LOG_LEVEL_DEBUG: 34 | return "DEBU"; 35 | case G_LOG_LEVEL_INFO: 36 | return "INFO"; 37 | case G_LOG_LEVEL_WARNING: 38 | return "WARN"; 39 | case G_LOG_LEVEL_ERROR: 40 | return "ERRO"; 41 | case G_LOG_LEVEL_CRITICAL: 42 | return "CRIT"; 43 | default: 44 | return "?"; 45 | } 46 | } 47 | 48 | static bool log_threshold_met(GLogLevelFlags log_level) { 49 | return g_atomic_int_get(&debug_log_enabled) || (log_level & ~G_LOG_LEVEL_DEBUG); 50 | } 51 | 52 | static void log_to_syslog(__attribute__((unused)) const char* log_domain, 53 | GLogLevelFlags log_level, 54 | const char* message, 55 | __attribute__((unused)) gpointer settings_void_ptr) { 56 | if (log_threshold_met(log_level)) 57 | syslog(log_level_to_syslog_priority(log_level), "%s", message); 58 | } 59 | 60 | // Timestamp format and log level have been chosen to match that of dockerd 61 | static void log_to_stdout(__attribute__((unused)) const char* log_domain, 62 | GLogLevelFlags log_level, 63 | const char* message, 64 | __attribute__((unused)) gpointer settings_void_ptr) { 65 | if (log_threshold_met(log_level)) { 66 | GDateTime* now = g_date_time_new_now_local(); 67 | g_autofree char* now_text = g_date_time_format(now, "%Y-%m-%dT%T.%f000%:z"); 68 | g_date_time_unref(now); 69 | printf("%s[%s] %s\n", log_level_to_string(log_level), now_text, message); 70 | } 71 | } 72 | 73 | void log_init(struct log_settings* settings) { 74 | if (settings->destination == log_dest_syslog) 75 | openlog(NULL, LOG_PID, LOG_USER); 76 | 77 | g_log_set_handler(NULL, 78 | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION | G_LOG_LEVEL_MASK, 79 | settings->destination == log_dest_syslog ? log_to_syslog : log_to_stdout, 80 | settings); 81 | } 82 | 83 | void log_debug_set(bool enabled) { 84 | g_atomic_int_set(&debug_log_enabled, enabled); 85 | } 86 | -------------------------------------------------------------------------------- /app/log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | enum log_destination { log_dest_stdout, log_dest_syslog }; 6 | 7 | struct log_settings { 8 | enum log_destination destination; 9 | }; 10 | 11 | // Set up g_log to log to either stdout or syslog. 12 | // The log destination cannot be changed after this call, but the debug level 13 | // can be adjusted at any time by changing the 'debug' member of the struct. A 14 | // pointer to the log_settings struct will be passed to g_log_set_handler(), so 15 | // the struct must live until the process exits. 16 | void log_init(struct log_settings* settings); 17 | 18 | void log_debug_set(bool enabled); 19 | 20 | // Replacement for G_LOG_LEVEL_ERROR, which is fatal. 21 | #define G_LOG_LEVEL_NON_FATAL_ERROR (1 << G_LOG_LEVEL_USER_SHIFT) 22 | 23 | #define log_debug(format, ...) g_debug(format, ##__VA_ARGS__) 24 | #define log_info(format, ...) g_info(format, ##__VA_ARGS__) 25 | #define log_warning(format, ...) g_warning(format, ##__VA_ARGS__) 26 | 27 | #define log_error(format, ...) \ 28 | g_log(G_LOG_DOMAIN, G_LOG_LEVEL_NON_FATAL_ERROR, format, ##__VA_ARGS__) 29 | -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.7.0", 3 | "resources": { 4 | "linux": { 5 | "user": { 6 | "groups": ["sdk", "storage"] 7 | } 8 | } 9 | }, 10 | "acapPackageConf": { 11 | "setup": { 12 | "friendlyName": "Docker Daemon", 13 | "appId": "414120", 14 | "appName": "dockerdwrapper", 15 | "vendor": "Axis Communications", 16 | "embeddedSdkVersion": "3.0", 17 | "vendorUrl": "https://www.axis.com", 18 | "runMode": "once", 19 | "version": "3.0.0" 20 | }, 21 | "installation": { 22 | "postInstallScript": "postinstallscript.sh" 23 | }, 24 | "configuration": { 25 | "paramConfig": [ 26 | { 27 | "name": "SDCardSupport", 28 | "default": "no", 29 | "type": "bool:no,yes" 30 | }, 31 | { 32 | "name": "UseTLS", 33 | "default": "yes", 34 | "type": "bool:no,yes" 35 | }, 36 | { 37 | "name": "TCPSocket", 38 | "default": "yes", 39 | "type": "bool:no,yes" 40 | }, 41 | { 42 | "name": "IPCSocket", 43 | "default": "no", 44 | "type": "bool:no,yes" 45 | }, 46 | { 47 | "name": "ApplicationLogLevel", 48 | "default": "info", 49 | "type": "enum:debug,info" 50 | }, 51 | { 52 | "name": "DockerdLogLevel", 53 | "default": "warn", 54 | "type": "enum:debug,info,warn,error,fatal" 55 | }, 56 | { 57 | "name": "Status", 58 | "default": "-1 No Status", 59 | "type": "hidden:string" 60 | } 61 | ], 62 | "containers": { 63 | "containerHost": true 64 | }, 65 | "settingPage": "index.html", 66 | "httpConfig": [ 67 | { 68 | "access": "admin", 69 | "name": "ca.pem", 70 | "type": "fastCgi" 71 | }, 72 | { 73 | "access": "admin", 74 | "name": "server-cert.pem", 75 | "type": "fastCgi" 76 | }, 77 | { 78 | "access": "admin", 79 | "name": "server-key.pem", 80 | "type": "fastCgi" 81 | } 82 | ] 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/postinstallscript.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | if [ ! -e /usr/bin/containerd ]; then 4 | logger -p user.warn "$0: Container support required to install application." 5 | exit 77 # EX_NOPERM 6 | fi 7 | 8 | UID_DOT_GID="$(stat -c %u.%g localdata)" 9 | IS_ROOT=$([ "$(id -u)" -eq 0 ] && echo true || echo false) 10 | 11 | # Create empty daemon.json 12 | DAEMON_JSON=localdata/daemon.json 13 | if [ ! -e "$DAEMON_JSON" ]; then 14 | umask 077 15 | echo "{}" >"$DAEMON_JSON" 16 | ! $IS_ROOT || chown "$UID_DOT_GID" "$DAEMON_JSON" 17 | fi 18 | 19 | # ACAP framework does not handle ownership on SD card, which causes problem when 20 | # the app user ID changes. If run as root, this script will repair the ownership. 21 | APP_NAME="$(basename "$(pwd)")" 22 | SD_CARD_AREA=/var/spool/storage/SD_DISK/areas/"$APP_NAME" 23 | if $IS_ROOT && [ -d "$SD_CARD_AREA" ]; then 24 | chown -R "$UID_DOT_GID" "$SD_CARD_AREA" 25 | fi 26 | -------------------------------------------------------------------------------- /app/sd_disk_storage.c: -------------------------------------------------------------------------------- 1 | #include "sd_disk_storage.h" 2 | #include "log.h" 3 | #include 4 | #include 5 | 6 | struct sd_disk_storage { 7 | SdDiskCallback callback; 8 | void* user_data; 9 | uint subscription_id; 10 | AXStorage* handle; 11 | }; 12 | 13 | static bool event_status_or_log(gchar* storage_id, AXStorageStatusEventId event) { 14 | GError* error = NULL; 15 | bool value = ax_storage_get_status(storage_id, event, &error); 16 | if (error) { 17 | log_warning("Could not read ax_storage status: %s", error->message); 18 | g_clear_error(&error); 19 | } 20 | return value; 21 | } 22 | 23 | static void setup_cb(AXStorage* handle, gpointer storage_void_ptr, GError* error) { 24 | struct sd_disk_storage* storage = storage_void_ptr; 25 | if (handle) 26 | storage->handle = handle; 27 | 28 | if (error) { 29 | log_warning("setup_cb error: %s", error->message); 30 | g_clear_error(&error); 31 | storage->callback(NULL, storage->user_data); 32 | return; 33 | } 34 | 35 | g_autofree char* path = ax_storage_get_path(handle, &error); 36 | if (!path) { 37 | log_warning("Failed to get storage path: %s", error->message); 38 | g_clear_error(&error); 39 | storage->callback(NULL, storage->user_data); 40 | return; 41 | } 42 | 43 | storage->callback(path, storage->user_data); 44 | } 45 | 46 | static void release_cb(gpointer, GError* error) { 47 | if (error) 48 | log_warning("Error while releasing storage: %s", error->message); 49 | } 50 | 51 | static void release(struct sd_disk_storage* storage) { 52 | GError* error = NULL; 53 | if (storage->handle) { 54 | if (!ax_storage_release_async(storage->handle, release_cb, NULL, &error)) { 55 | log_warning("Failed to release storage: %s", error->message); 56 | g_clear_error(&error); 57 | } 58 | storage->handle = NULL; 59 | } 60 | } 61 | 62 | static void release_and_unsubscribe(struct sd_disk_storage* storage) { 63 | GError* error = NULL; 64 | 65 | release(storage); 66 | 67 | if (storage->subscription_id) { 68 | if (!ax_storage_unsubscribe(storage->subscription_id, &error)) { 69 | log_warning("Failed to unsubscribe to storage events: %s", error->message); 70 | g_clear_error(&error); 71 | } 72 | storage->subscription_id = 0; 73 | } 74 | } 75 | 76 | void sd_disk_storage_free(struct sd_disk_storage* storage) { 77 | if (storage) 78 | release_and_unsubscribe(storage); 79 | free(storage); 80 | } 81 | 82 | static void subscribe_cb(gchar* storage_id, gpointer storage_void_ptr, GError* error) { 83 | struct sd_disk_storage* storage = storage_void_ptr; 84 | 85 | if (error) { 86 | log_warning("subscribe_cb error: %s", error->message); 87 | g_clear_error(&error); 88 | storage->callback(NULL, storage->user_data); 89 | } 90 | 91 | if (event_status_or_log(storage_id, AX_STORAGE_EXITING_EVENT)) { 92 | storage->callback(NULL, storage->user_data); 93 | release(storage); 94 | } 95 | 96 | if (event_status_or_log(storage_id, AX_STORAGE_WRITABLE_EVENT)) { 97 | if (!ax_storage_setup_async(storage_id, setup_cb, storage, &error)) { 98 | log_warning("ax_storage_setup_async error: %s", error->message); 99 | g_clear_error(&error); 100 | storage->callback(NULL, storage->user_data); 101 | } 102 | } 103 | } 104 | 105 | static bool subscribe(struct sd_disk_storage* storage, const char* storage_id) { 106 | GError* error = NULL; 107 | bool found = false; 108 | GList* devices = ax_storage_list(&error); 109 | for (GList* node = g_list_first(devices); node; node = g_list_next(node)) { 110 | if (strcmp(node->data, storage_id) == 0) { 111 | found = true; 112 | if (!(storage->subscription_id = 113 | ax_storage_subscribe(node->data, subscribe_cb, storage, &error))) { 114 | log_error("Failed to subscribe to events of %s: %s", 115 | (char*)node->data, 116 | error->message); 117 | g_clear_error(&error); 118 | return false; 119 | } 120 | } 121 | g_free(node->data); 122 | } 123 | g_list_free(devices); 124 | if (!found) 125 | log_info("No storage with id %s found", 126 | storage_id); // Not an error if products doesn't have SD card slot 127 | return true; 128 | } 129 | 130 | struct sd_disk_storage* sd_disk_storage_init(SdDiskCallback sd_disk_callback, void* user_data) { 131 | struct sd_disk_storage* storage = g_malloc0(sizeof(struct sd_disk_storage)); 132 | storage->callback = sd_disk_callback; 133 | storage->user_data = user_data; 134 | if (!subscribe(storage, "SD_DISK")) { 135 | sd_disk_storage_free(storage); 136 | return NULL; 137 | } 138 | return storage; 139 | } 140 | -------------------------------------------------------------------------------- /app/sd_disk_storage.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | typedef void (*SdDiskCallback)(const char* area_path, void* user_data); 4 | 5 | // Call sd_disk_callback with a path to the SD card when it has become 6 | // available. Call sd_disk_callback with NULL when it is about to be unmounted. 7 | // Unmounting will fail if the SD card area contains open files when the 8 | // callback returns. 9 | struct sd_disk_storage* sd_disk_storage_init(SdDiskCallback sd_disk_callback, void* user_data); 10 | 11 | void sd_disk_storage_free(struct sd_disk_storage* storage); 12 | -------------------------------------------------------------------------------- /app/tls.c: -------------------------------------------------------------------------------- 1 | #include "tls.h" 2 | #include "app_paths.h" 3 | #include "log.h" 4 | #include 5 | #include 6 | #include 7 | 8 | #define TLS_CERT_PATH APP_LOCALDATA 9 | 10 | struct cert { 11 | const char* dockerd_option; 12 | const char* filename; 13 | const char* description; 14 | }; 15 | 16 | static struct cert tls_certs[] = {{"--tlscacert", "ca.pem", "CA certificate"}, 17 | {"--tlscert", "server-cert.pem", "server certificate"}, 18 | {"--tlskey", "server-key.pem", "server key"}}; 19 | 20 | #define NUM_TLS_CERTS (sizeof(tls_certs) / sizeof(tls_certs[0])) 21 | 22 | #define BEGIN(x) "-----BEGIN " x "-----\n" 23 | #define END(x) "-----END " x "-----\n" 24 | #define CERTIFICATE "CERTIFICATE" 25 | #define PRIVATE_KEY "PRIVATE KEY" 26 | #define RSA_PRIVATE_KEY "RSA PRIVATE KEY" 27 | 28 | // Filename is assumed to be one of those listed in tls_certs[]. 29 | static bool is_key_file(const char* filename) { 30 | return strstr(filename, "key"); 31 | } 32 | 33 | static bool cert_file_exists(const struct cert* tls_cert) { 34 | g_autofree char* full_path = g_strdup_printf("%s/%s", TLS_CERT_PATH, tls_cert->filename); 35 | return access(full_path, F_OK) == 0; 36 | } 37 | 38 | bool tls_missing_certs(void) { 39 | for (size_t i = 0; i < NUM_TLS_CERTS; ++i) 40 | if (!cert_file_exists(&tls_certs[i])) 41 | return true; 42 | return false; 43 | } 44 | 45 | void tls_log_missing_cert_warnings(void) { 46 | for (size_t i = 0; i < NUM_TLS_CERTS; ++i) 47 | if (!cert_file_exists(&tls_certs[i])) 48 | log_warning("No %s found at %s/%s", 49 | tls_certs[i].description, 50 | TLS_CERT_PATH, 51 | tls_certs[i].filename); 52 | } 53 | 54 | const char* tls_file_description(const char* filename) { 55 | for (size_t i = 0; i < NUM_TLS_CERTS; ++i) 56 | if (strcmp(filename, tls_certs[i].filename) == 0) 57 | return tls_certs[i].description; 58 | return NULL; 59 | } 60 | 61 | const char* tls_file_dockerd_args(void) { 62 | static char args[512]; // Too small buffer will cause truncated options, nothing more. 63 | const char* end = args + sizeof(args); 64 | char* ptr = args; 65 | 66 | for (size_t i = 0; i < NUM_TLS_CERTS; ++i) 67 | ptr += g_snprintf(ptr, 68 | end - ptr, 69 | "%s %s/%s ", 70 | tls_certs[i].dockerd_option, 71 | TLS_CERT_PATH, 72 | tls_certs[i].filename); 73 | ptr[-1] = '\0'; // Remove space after last item. 74 | return args; 75 | } 76 | 77 | static bool read_bytes_from(FILE* fp, int whence, char* buffer, int num_bytes) { 78 | const long offset = whence == SEEK_SET ? 0 : -num_bytes; 79 | if (fseek(fp, offset, whence) != 0) { 80 | log_error("Could not reposition stream to %s%ld: %s", 81 | whence == SEEK_SET ? "SEEK_SET+" : "SEEK_END", 82 | offset, 83 | strerror(errno)); 84 | return false; 85 | } 86 | if (fread(buffer, num_bytes, 1, fp) != 1) { 87 | log_error("Could not read %d bytes: %s", num_bytes, strerror(errno)); 88 | return false; 89 | } 90 | return true; 91 | } 92 | 93 | static bool is_file_section_equal_to(FILE* fp, int whence, const char* section) { 94 | char buffer[128]; 95 | int to_read = strlen(section); 96 | if (!read_bytes_from(fp, whence, buffer, to_read)) 97 | return false; 98 | buffer[to_read] = '\0'; 99 | return strncmp(buffer, section, to_read) == 0; 100 | } 101 | 102 | static bool has_header_and_footer(FILE* fp, const char* header, const char* footer) { 103 | return is_file_section_equal_to(fp, SEEK_SET, header) && 104 | is_file_section_equal_to(fp, SEEK_END, footer); 105 | } 106 | 107 | bool tls_file_has_correct_format(const char* filename, const char* path_to_file) { 108 | FILE* fp = fopen(path_to_file, "r"); 109 | if (!fp) { 110 | log_error("Could not read %s", path_to_file); 111 | return false; 112 | } 113 | 114 | bool correct = is_key_file(filename) 115 | ? (has_header_and_footer(fp, BEGIN(PRIVATE_KEY), END(PRIVATE_KEY)) || 116 | has_header_and_footer(fp, BEGIN(RSA_PRIVATE_KEY), END(RSA_PRIVATE_KEY))) 117 | : has_header_and_footer(fp, BEGIN(CERTIFICATE), END(CERTIFICATE)); 118 | if (!correct) 119 | log_error("%s does not contain the headers and footers for a %s.", 120 | path_to_file, 121 | tls_file_description(filename)); 122 | fclose(fp); 123 | return correct; 124 | } 125 | -------------------------------------------------------------------------------- /app/tls.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | bool tls_missing_certs(void); 5 | void tls_log_missing_cert_warnings(void); 6 | const char* tls_file_description(const char* filename); 7 | const char* tls_file_dockerd_args(void); 8 | bool tls_file_has_correct_format(const char* filename, const char* path_to_file); 9 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | imagetag="docker-acap:1.0" 4 | 5 | function exit_with_message { 6 | echo "$1" 7 | echo 8 | echo "Usage: $0 ARCH [ --plain ] [ --cache ] [ --image-tag IMAGETAG ]" 9 | echo 10 | echo "ARCH must be 'armv7hf' or 'aarch64'" 11 | echo "--plain will simplify the Docker progress bar." 12 | echo "--cache will enable Docker's caching mechanism." 13 | echo "--image-tag sets the supplied tag of the image. Default is $imagetag." 14 | exit 1 15 | } 16 | 17 | arch="${1:-}" 18 | 19 | case "$arch" in 20 | armv7hf | aarch64) ;; 21 | *) exit_with_message "Invalid architecture '$arch'" ;; 22 | esac 23 | shift 24 | 25 | progress_arg= 26 | cache_arg=--no-cache 27 | 28 | while (($#)); do 29 | case "$1" in 30 | --plain) progress_arg="--progress plain" ;; 31 | --cache) cache_arg= ;; 32 | --image-tag) 33 | shift 34 | imagetag="$1" 35 | ;; 36 | *) exit_with_message "Invalid argument '$1'" ;; 37 | esac 38 | shift 39 | done 40 | 41 | # Build and copy out the acap 42 | # shellcheck disable=SC2086 43 | docker buildx build --build-arg ARCH="$arch" \ 44 | --build-arg HTTP_PROXY="${HTTP_PROXY:-}" \ 45 | --build-arg HTTPS_PROXY="${HTTPS_PROXY:-}" \ 46 | --build-arg BUILD_WITH_SANITIZERS="${BUILD_WITH_SANITIZERS:-}" \ 47 | --file Dockerfile \ 48 | $progress_arg \ 49 | $cache_arg \ 50 | --tag "$imagetag" \ 51 | --output build-"$arch" . 52 | --------------------------------------------------------------------------------