diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23e8dda..1e81923 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,5 +82,5 @@ jobs: docker run --privileged --rm tonistiigi/binfmt --install all podman manifest create build podman build --format docker --platform linux/amd64,linux/arm64,linux/arm/v7 --manifest localhost/build . - podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:${EPOCHSECONDS}-ci + podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:${EPOCHSECONDS}-rootless-ci diff --git a/.github/workflows/release-dev-version.yml b/.github/workflows/release-dev-version.yml index 8581b3d..9bb7a04 100644 --- a/.github/workflows/release-dev-version.yml +++ b/.github/workflows/release-dev-version.yml @@ -85,5 +85,5 @@ jobs: docker run --privileged --rm tonistiigi/binfmt --install all podman manifest create build podman build --format docker --build-arg BUILD_VERSION_ARG=${VERSION} --manifest localhost/build --platform linux/amd64,linux/arm64,linux/arm/v7 . - podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION}-dev - podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:latest-dev + podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION}-rootless-dev + podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:latest-rootless-dev diff --git a/.github/workflows/release-productive-version.yml b/.github/workflows/release-productive-version.yml index 357818c..b5c8ad9 100644 --- a/.github/workflows/release-productive-version.yml +++ b/.github/workflows/release-productive-version.yml @@ -85,5 +85,5 @@ jobs: docker run --privileged --rm tonistiigi/binfmt --install all podman manifest create build podman build --format docker --build-arg BUILD_VERSION_ARG=${VERSION} --manifest localhost/build --platform linux/amd64,linux/arm64,linux/arm/v7 . - podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION} - podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:latest + podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:${VERSION}-rootless + podman manifest push localhost/build docker://docker.io/bbernhard/signal-cli-rest-api:rootless-latest diff --git a/Dockerfile b/Dockerfile index 9d38bf1..54574ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,8 @@ ARG LIBSIGNAL_CLIENT_VERSION=0.99.1 ARG SWAG_VERSION=1.16.4 +ARG S6_OVERLAY_VERSION=v3.2.2.0 + ARG BUILD_VERSION_ARG=unset FROM golang:1.26-trixie AS buildcontainer @@ -38,7 +40,7 @@ RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ ENV JAVA_OPTS="-Djdk.lang.Process.launchMechanism=vfork" -ENV LANG en_US.UTF-8 +ENV LANG=en_US.UTF-8 RUN go install github.com/swaggo/swag/cmd/swag@v${SWAG_VERSION} @@ -120,43 +122,63 @@ RUN cd /tmp/signal-cli-rest-api-src/scripts && go build -o jsonrpc2-helper RUN cd /tmp/signal-cli-rest-api-src && go build -buildmode=plugin -o signal-cli-rest-api_plugin_loader.so plugin_loader.go # Start a fresh container for release container +FROM debian:trixie-slim -# eclipse-temurin doesn't provide a OpenJDK 21 image for armv7 (see https://github.com/adoptium/containers/issues/502). Until this -# is fixed we use the standard ubuntu image -#FROM eclipse-temurin:21-jre-jammy - -FROM ubuntu:noble - -ENV GIN_MODE=release - -ENV PORT=8080 - +ARG TARGETARCH # set by buildx ARG SIGNAL_CLI_VERSION ARG BUILD_VERSION_ARG +ARG S6_OVERLAY_VERSION +ENV GIN_MODE=release + +# Set environment variables to keep the image clean +ENV DEBIAN_FRONTEND=noninteractive + +ENV PORT=8080 ENV BUILD_VERSION=$BUILD_VERSION_ARG ENV SIGNAL_CLI_REST_API_PLUGIN_SHARED_OBJ_DIR=/usr/bin/ RUN dpkg-reconfigure debconf --frontend=noninteractive \ && apt-get update \ - && apt-get install -y --no-install-recommends util-linux supervisor openjdk-25-jre curl locales \ + && apt-get install -y --no-install-recommends util-linux openjdk-25-jre curl locales xz-utils \ + && apt-get clean \ && rm -rf /var/lib/apt/lists/* +RUN if [ -z "$TARGETARCH" ]; then \ + # Fallback for older Docker versions not using BuildKit + TARGETARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/'); \ + else \ + echo "Building for architecture: $TARGETARCH"; \ + fi; + +# install s6-overlay as service control system +RUN curl -fL -o /tmp/s6-overlay-noarch.tar.xz \ + "https://github.com/just-containers/s6-overlay/releases/download/${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz" && \ + tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz && \ + if [ "$TARGETARCH" = "amd64" ]; then S6_ARCH="x86_64"; \ + elif [ "$TARGETARCH" = "arm64" ]; then S6_ARCH="aarch64"; \ + elif [ "$TARGETARCH" = "arm" ]; then S6_ARCH="arm"; \ + else S6_ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/'); fi;\ + curl -fL -o /tmp/s6-overlay-bin.tar.xz \ + "https://github.com/just-containers/s6-overlay/releases/download/${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz" && \ + tar -C / -Jxpf /tmp/s6-overlay-bin.tar.xz && \ + rm /tmp/s6-overlay-*.tar.xz + COPY --from=buildcontainer /tmp/signal-cli-rest-api-src/signal-cli-rest-api /usr/bin/signal-cli-rest-api COPY --from=buildcontainer /opt/signal-cli-${SIGNAL_CLI_VERSION} /opt/signal-cli-${SIGNAL_CLI_VERSION} COPY --from=buildcontainer /tmp/signal-cli-native /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native COPY --from=buildcontainer /tmp/signal-cli-rest-api-src/scripts/jsonrpc2-helper /usr/bin/jsonrpc2-helper COPY --from=buildcontainer /tmp/signal-cli-rest-api-src/signal-cli-rest-api_plugin_loader.so /usr/bin/signal-cli-rest-api_plugin_loader.so -COPY entrypoint.sh /entrypoint.sh -RUN userdel ubuntu -r \ - && groupadd -g 1000 signal-api \ +RUN groupadd -g 1000 signal-api \ && useradd --no-log-init -M -d /home -s /bin/bash -u 1000 -g 1000 signal-api \ && ln -s /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli /usr/bin/signal-cli \ && ln -s /opt/signal-cli-${SIGNAL_CLI_VERSION}/bin/signal-cli-native /usr/bin/signal-cli-native \ - && mkdir -p /signal-cli-config/ \ - && mkdir -p /home/.local/share/signal-cli + && mkdir -p /home/.local/share/signal-cli \ + && chown -R signal-api:signal-api /home + +COPY --chmod=755 ./s6-services/ /etc/s6-overlay/s6-rc.d/ # remove the temporary created signal-cli-native on armv7, as GRAALVM doesn't support 32bit RUN arch="$(uname -m)"; \ @@ -168,16 +190,24 @@ RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ dpkg-reconfigure --frontend=noninteractive locales && \ update-locale LANG=en_US.UTF-8 -ENV LANG en_US.UTF-8 +ENV LANG=en_US.UTF-8 EXPOSE ${PORT} ENV SIGNAL_CLI_CONFIG_DIR=/home/.local/share/signal-cli -ENV SIGNAL_CLI_UID=1000 -ENV SIGNAL_CLI_GID=1000 -ENV SIGNAL_CLI_CHOWN_ON_STARTUP=true -ENTRYPOINT ["/entrypoint.sh"] +RUN mkdir -p /tmp/s6-runtime && chown -R signal-api:signal-api /tmp/s6-runtime /etc/s6-overlay + +USER signal-api + +# Mandatory ENV for non-root s6 +ENV S6_RUNTIME_PATH=/tmp/s6-runtime +ENV S6_READ_ONLY_ROOT=1 +ENV S6_VERBOSITY=2 + +WORKDIR /home + +ENTRYPOINT ["/init"] HEALTHCHECK --interval=20s --timeout=10s --retries=3 \ CMD curl -f http://localhost:${PORT}/v1/health || exit 1 diff --git a/docker-compose.yml b/docker-compose.yml index 958a194..1f019ed 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,11 +1,16 @@ services: signal-cli-rest-api: - image: bbernhard/signal-cli-rest-api:latest + user: "1000:1000" # change to UID:GID you preferre + image: bbernhard/signal-cli-rest-api:rootless-latest environment: - MODE=normal #supported modes: json-rpc, native, normal #- AUTO_RECEIVE_SCHEDULE=0 22 * * * #enable this parameter on demand (see description below) ports: - "8080:8080" #map docker port 8080 to host port 8080. volumes: - - "./signal-cli-config:/home/.local/share/signal-cli" #map "signal-cli-config" folder on host system into docker container. the folder contains the password and cryptographic keys when a new number is registered + - "./signal-cli-config:/home/.local/share/signal-cli" #map "signal-cli-config" folder on host system into docker container. the folder contains the password and cryptographic keys when a new number is registered. Make sure that the user has permissions to read and write here. + security_opt: + - no-new-privileges:true # additonal security control + tmpfs: + - /run:exec,size=64m,uid=1000,gid=1000,mode=0755 # Make sure this UID:GID fits those from above diff --git a/entrypoint.old.sh b/entrypoint.old.sh new file mode 100755 index 0000000..05363d3 --- /dev/null +++ b/entrypoint.old.sh @@ -0,0 +1,43 @@ +#!/bin/sh + +set -x +set -e + +[ -z "${SIGNAL_CLI_CONFIG_DIR}" ] && echo "SIGNAL_CLI_CONFIG_DIR environmental variable needs to be set! Aborting!" && exit 1; + +usermod -u ${SIGNAL_CLI_UID} signal-api +groupmod -o -g ${SIGNAL_CLI_GID} signal-api + +# Fix permissions to ensure backward compatibility if SIGNAL_CLI_CHOWN_ON_STARTUP is not set to "false" +if [ "$SIGNAL_CLI_CHOWN_ON_STARTUP" != "false" ]; then + echo "Changing ownership of ${SIGNAL_CLI_CONFIG_DIR} to ${SIGNAL_CLI_UID}:${SIGNAL_CLI_GID}" + chown ${SIGNAL_CLI_UID}:${SIGNAL_CLI_GID} -R ${SIGNAL_CLI_CONFIG_DIR} +else + echo "Skipping chown on startup since SIGNAL_CLI_CHOWN_ON_STARTUP is set to 'false'" +fi + +# Show warning on docker exec +cat <> /root/.bashrc +echo "WARNING: signal-cli-rest-api runs as signal-api (not as root!)" +echo "Run 'su signal-api' before using signal-cli!" +echo "If you want to use signal-cli directly, don't forget to specify the config directory. e.g: \"signal-cli --config ${SIGNAL_CLI_CONFIG_DIR}\"" +EOF + +cap_prefix="-cap_" +caps="$cap_prefix$(seq -s ",$cap_prefix" 0 $(cat /proc/sys/kernel/cap_last_cap))" + +# TODO: check mode +if [ "$MODE" = "json-rpc" ] +then +/usr/bin/jsonrpc2-helper +if [ -n "$JAVA_OPTS" ] ; then + echo "export JAVA_OPTS='$JAVA_OPTS'" >> /etc/default/supervisor +fi +service supervisor start +supervisorctl start all +fi + +export HOST_IP=$(hostname -I | awk '{print $1}') + +# Start API as signal-api user +exec setpriv --reuid=${SIGNAL_CLI_UID} --regid=${SIGNAL_CLI_GID} --init-groups --inh-caps=$caps signal-cli-rest-api -signal-cli-config=${SIGNAL_CLI_CONFIG_DIR} diff --git a/s6-services/signal-api/run b/s6-services/signal-api/run new file mode 100644 index 0000000..e93491c --- /dev/null +++ b/s6-services/signal-api/run @@ -0,0 +1,3 @@ +#!/command/with-contenv sh +# Use with-contenv to import environment variables like SIGNAL_CLI_CONFIG_DIR +exec signal-cli-rest-api -signal-cli-config="${SIGNAL_CLI_CONFIG_DIR}" \ No newline at end of file diff --git a/s6-services/signal-api/type b/s6-services/signal-api/type new file mode 100644 index 0000000..1780f9f --- /dev/null +++ b/s6-services/signal-api/type @@ -0,0 +1 @@ +longrun \ No newline at end of file diff --git a/s6-services/signal-json-rpc/run b/s6-services/signal-json-rpc/run new file mode 100644 index 0000000..2da0ec7 --- /dev/null +++ b/s6-services/signal-json-rpc/run @@ -0,0 +1,10 @@ +#!/command/with-contenv sh +# File: /etc/s6-overlay/s6-rc.d/signal-json-rpc/run + +if [ "$MODE" != "json-rpc" ] && [ "$MODE" != "json-rpc-native" ]; then + echo "Running as mode: $MODE - skipping json-rpc setup" + sleep infinity # do nothing, but keep service running + exit 0 +fi + +exec jsonrpc2-helper diff --git a/s6-services/signal-json-rpc/type b/s6-services/signal-json-rpc/type new file mode 100644 index 0000000..1780f9f --- /dev/null +++ b/s6-services/signal-json-rpc/type @@ -0,0 +1 @@ +longrun \ No newline at end of file diff --git a/s6-services/user/contents.d/signal-api b/s6-services/user/contents.d/signal-api new file mode 100644 index 0000000..e69de29 diff --git a/s6-services/user/contents.d/signal-json-rpc b/s6-services/user/contents.d/signal-json-rpc new file mode 100644 index 0000000..e69de29 diff --git a/s6-services/user/type b/s6-services/user/type new file mode 100644 index 0000000..3a8819b --- /dev/null +++ b/s6-services/user/type @@ -0,0 +1 @@ +bundle \ No newline at end of file diff --git a/src/scripts/jsonrpc2-helper.go b/src/scripts/jsonrpc2-helper.go index 96a4c34..9773a9b 100644 --- a/src/scripts/jsonrpc2-helper.go +++ b/src/scripts/jsonrpc2-helper.go @@ -1,32 +1,15 @@ package main import ( - "fmt" - "io/ioutil" - "os/exec" + "os" + "strconv" "strings" + "syscall" "github.com/bbernhard/signal-cli-rest-api/utils" log "github.com/sirupsen/logrus" ) -const supervisorctlConfigTemplate = ` -[program:%s] -process_name=%s -command=%s --output=json --config %s%s daemon %s%s%s%s --tcp 127.0.0.1:%d -autostart=true -autorestart=true -startretries=10 -user=signal-api -directory=/usr/bin/ -redirect_stderr=true -stdout_logfile=/var/log/%s/out.log -stderr_logfile=/var/log/%s/err.log -stdout_logfile_maxbytes=50MB -stdout_logfile_backups=10 -numprocs=1 -` - func main() { signalCliConfigDir := "/home/.local/share/signal-cli/" signalCliConfigDirEnv := utils.GetEnv("SIGNAL_CLI_CONFIG_DIR", "") @@ -40,9 +23,10 @@ func main() { jsonRpc2ClientConfig := utils.NewJsonRpc2ClientConfig() var tcpPort int64 = 6001 - jsonRpc2ClientConfig.AddEntry(utils.MULTI_ACCOUNT_NUMBER, utils.JsonRpc2ClientConfigEntry{TcpPort: tcpPort}) + args := []string{"--output=json", "--config", signalCliConfigDir} + signalCliBinary := "signal-cli" signalMode := utils.GetEnv("MODE", "json-rpc") if signalMode == "json-rpc-native" { @@ -51,67 +35,53 @@ func main() { log.Fatal("The mode needs to be either 'json-rpc' or 'json-rpc-native'") } - signalCliIgnoreAttachments := "" - ignoreAttachments := utils.GetEnv("JSON_RPC_IGNORE_ATTACHMENTS", "") - if ignoreAttachments == "true" { - signalCliIgnoreAttachments = " --ignore-attachments" - } - - signalCliIgnoreStories := "" - ignoreStories := utils.GetEnv("JSON_RPC_IGNORE_STORIES", "") - if ignoreStories == "true" { - signalCliIgnoreStories = " --ignore-stories" - } - - signalCliIgnoreAvatars := "" - ignoreAvatars := utils.GetEnv("JSON_RPC_IGNORE_AVATARS", "") - if ignoreAvatars == "true" { - signalCliIgnoreAvatars = " --ignore-avatars" - } - - signalCliIgnoreStickers := "" - ignoreStickers := utils.GetEnv("JSON_RPC_IGNORE_STICKERS", "") - if ignoreStickers == "true" { - signalCliIgnoreStickers = " --ignore-stickers" - } - - supervisorctlProgramName := "signal-cli-json-rpc-1" - supervisorctlLogFolder := "/var/log/" + supervisorctlProgramName - _, err := exec.Command("mkdir", "-p", supervisorctlLogFolder).Output() - if err != nil { - log.Fatal("Couldn't create log folder ", supervisorctlLogFolder, ": ", err.Error()) - } - - trustNewIdentities := "" trustNewIdentitiesEnv := utils.GetEnv("JSON_RPC_TRUST_NEW_IDENTITIES", "") if trustNewIdentitiesEnv == "on-first-use" { - trustNewIdentities = " --trust-new-identities on-first-use" + args = append(args, []string{"--trust-new-identities", "on-first-use"}...) } else if trustNewIdentitiesEnv == "always" { - trustNewIdentities = " --trust-new-identities always" + args = append(args, []string{"--trust-new-identities", "always"}...) } else if trustNewIdentitiesEnv == "never" { - trustNewIdentities = " --trust-new-identities never" + args = append(args, []string{"--trust-new-identities", "never"}...) } else if trustNewIdentitiesEnv != "" { log.Fatal("Invalid JSON_RPC_TRUST_NEW_IDENTITIES environment variable set!") } - log.Info("Updated jsonrpc2.yml") + args = append(args, "daemon") - //write supervisorctl config - supervisorctlConfigFilename := "/etc/supervisor/conf.d/" + "signal-cli-json-rpc-1.conf" - - supervisorctlConfig := fmt.Sprintf(supervisorctlConfigTemplate, supervisorctlProgramName, supervisorctlProgramName, signalCliBinary, - signalCliConfigDir, trustNewIdentities, signalCliIgnoreAttachments, signalCliIgnoreStories, - signalCliIgnoreAvatars, signalCliIgnoreStickers, tcpPort, - supervisorctlProgramName, supervisorctlProgramName) - - err = ioutil.WriteFile(supervisorctlConfigFilename, []byte(supervisorctlConfig), 0644) - if err != nil { - log.Fatal("Couldn't write ", supervisorctlConfigFilename, ": ", err.Error()) + ignoreStories := utils.GetEnv("JSON_RPC_IGNORE_STORIES", "") + if ignoreStories == "true" { + args = append(args, "--ignore-stories") } + ignoreAvatars := utils.GetEnv("JSON_RPC_IGNORE_AVATARS", "") + if ignoreAvatars == "true" { + args = append(args, "--ignore-avatars") + } + + ignoreStickers := utils.GetEnv("JSON_RPC_IGNORE_STICKERS", "") + if ignoreStickers == "true" { + args = append(args, "--ignore-stickers") + } + + ignoreAttachments := utils.GetEnv("JSON_RPC_IGNORE_ATTACHMENTS", "") + if ignoreAttachments == "true" { + args = append(args, "--ignore-attachments") + } + + args = append(args, []string{"--tcp", "127.0.0.1:" + strconv.FormatInt(tcpPort, 10)}...) + // write jsonrpc.yml config file - err = jsonRpc2ClientConfig.Persist(signalCliConfigDir + "jsonrpc2.yml") + err := jsonRpc2ClientConfig.Persist(signalCliConfigDir + "jsonrpc2.yml") if err != nil { log.Fatal("Couldn't persist jsonrpc2.yaml: ", err.Error()) } + + log.Info("Updated jsonrpc2.yml") + + env := os.Environ() + + err = syscall.Exec("/usr/bin/"+signalCliBinary, args, env) + if err != nil { + log.Fatal("Couldn't start signal-cli in json-rpc mode: ", err.Error()) + } }