# A container for PyBitmessage daemon
FROM ubuntu:bionic

SHELL ["/bin/bash", "-exo", "pipefail", "-c"]

# Install dependencies. update and install share a layer on purpose: split
# across two, a cached update feeds install package lists that may be months
# stale, and the install then fails or pulls something unintended.
RUN apt-get update \
 && apt-get install -yq --no-install-suggests --no-install-recommends \
    build-essential libcap-dev libssl-dev \
    python-all-dev python-msgpack python-pip python-setuptools \
    git

## Do not use cache when building next layers of the image.
ARG NOCACHE=0

WORKDIR /root/PyBitmessage
RUN git clone https://github.com/Bitmessage/PyBitmessage .

# Install
RUN pip2 install jsonrpclib .

# Raise the SQL-thread startup timeout from the stock 60 s.
#
# PyBitmessage kills the daemon outright if the SQL thread is not ready within
# sql_timeout seconds (class_objectProcessor.py -> os._exit(1)). The startup
# VACUUM of a messages.dat that has grown to a few hundred MB does not fit in
# 60 s, and since the process dies mid-VACUUM lastvacuumtime is never updated,
# so every later start retries the same doomed VACUUM and the node never comes
# back. Measured: 26 s for a 264 MB database on an idle host, and the last
# start that did survive used 36 s of the 60.
#
# The greps are load-bearing: the clone above is unpinned, so if upstream ever
# moves or renames the constant, a silent no-op sed would ship an image that
# looks fixed and is not. Fail the build instead. The .pyc is refreshed because
# at runtime /usr/local is root-owned while the daemon runs as bitmessage, so a
# stale one can only be recompiled to memory on every start.
RUN f=/usr/local/lib/python2.7/dist-packages/pybitmessage/helper_sql.py \
 && grep -q '^sql_timeout = 60$' "$f" \
 && sed -i 's/^sql_timeout = 60$/sql_timeout = 600/' "$f" \
 && grep -q '^sql_timeout = 600$' "$f" \
 && rm -f "${f}c" \
 && python -c "import py_compile; py_compile.compile('$f')"

FROM ubuntu:bionic

# 8442 is the XML-RPC API (keep it on loopback), 8444 the Bitmessage P2P port.
# The daemon listens on both regardless; publishing 8444 is what makes the node
# reachable for inbound peers.
EXPOSE 8442/tcp
EXPOSE 8444/tcp

ENV USER_UID=2000
ENV USER_GID=2000
ENV HOME=/home/bitmessage
ENV BITMESSAGE_HOME=${HOME}

COPY --from=0 /usr/local/ /usr/local/
COPY ./docker/healthy_check.py /usr/local/bin/
COPY ./docker/seed_addr_gen.py /usr/local/bin/
COPY ./docker/watchdog.py /usr/local/bin/
COPY ./docker/drop_privs.py /usr/local/bin/
COPY ./docker/run.sh /usr/local/bin/

# Install dependencies. gosu is gone: run.sh drops privileges through
# drop_privs.py, which does the whole job rather than only the user and needs
# nothing beyond the Python already here. moreutils went with it -- nothing in
# this repository ever called any of its tools.
RUN apt-get update \
	&& apt-get install -yq --no-install-suggests --no-install-recommends python-setuptools \
	&& rm -rf /var/lib/apt/lists/*

# Create a user
RUN addgroup --gid $USER_GID bitmessage ;\
	useradd --uid $USER_UID --gid $USER_GID --skel /dev/null --create-home --home-dir $HOME bitmessage

WORKDIR ${HOME}

# Generate default config
RUN su bitmessage -c "pybitmessage -t"

# Prove that the two privilege drops in run.sh do what the script leans on, in
# the arrangement run.sh uses: the supervisor's on the outside, the daemon's on
# the inside. This base image and the PyBitmessage clone above are both
# unpinned, so a helper that quietly stopped working would otherwise ship as a
# container that looks confined and is not -- the trap the sql_timeout greps in
# the first stage avoid the same way, by failing the build instead.
#
# Expected: the daemon at uid 2000 with every capability set empty and
# no_new_privs on, and the supervisor holding 00000000000001e0 -- CAP_KILL,
# CAP_SETGID, CAP_SETUID and CAP_SETPCAP, bits 5 to 8, and nothing else.
RUN set -eu \
 && drop=/usr/local/bin/drop_privs.py \
 && d="$(python "$drop" --keep=kill,setgid,setuid,setpcap -- \
	python "$drop" --user -- \
	grep -E '^(Uid|Gid|CapPrm|CapEff|CapBnd|NoNewPrivs):' /proc/self/status)" \
 && printf '%s\n' "$d" \
 && printf '%s\n' "$d" | grep -qE '^Uid:[[:space:]]+2000[[:space:]]+2000[[:space:]]+2000' \
 && printf '%s\n' "$d" | grep -qE '^Gid:[[:space:]]+2000[[:space:]]+2000[[:space:]]+2000' \
 && printf '%s\n' "$d" | grep -qE '^CapPrm:[[:space:]]+0{16}$' \
 && printf '%s\n' "$d" | grep -qE '^CapEff:[[:space:]]+0{16}$' \
 && printf '%s\n' "$d" | grep -qE '^CapBnd:[[:space:]]+0{16}$' \
 && printf '%s\n' "$d" | grep -qE '^NoNewPrivs:[[:space:]]+1$' \
 && s="$(python "$drop" --keep=kill,setgid,setuid,setpcap -- \
	grep -E '^CapBnd:' /proc/self/status)" \
 && printf '%s\n' "$s" \
 && printf '%s\n' "$s" | grep -qE '^CapBnd:[[:space:]]+0{13}1e0$'

# Nothing here needs a setuid or setgid bit at run time, and every one of them
# is a way back for a daemon that has been taken over -- the more so because the
# daemon now runs with no_new_privs, which makes them the one thing that could
# still have raised its privileges. The privilege drop is not among them: it is
# an ordinary script that root runs. Strip them all, then insist none is left,
# so a package added here later cannot bring one back unnoticed.
RUN find / -xdev -type f -perm /6000 -exec chmod -s {} + \
 && [ -z "$(find / -xdev -type f -perm /6000)" ]

CMD ["sh", "/usr/local/bin/run.sh"]

## Check PyBitmessage active network connections.
## The start period covers the startup VACUUM of messages.dat, which takes tens
## of seconds once the database reaches a few hundred MB; without it the
## container reports unhealthy for that whole window on every restart.
HEALTHCHECK --retries=0 --interval=15s --start-period=180s \
  CMD ["python", "/usr/local/bin/healthy_check.py"]

