mirror of
https://github.com/shenxn/protonmail-bridge-docker.git
synced 2026-01-18 14:44:41 +01:00
Initial commit for http rest bridge
This commit is contained in:
11
.github/workflows/build.yaml
vendored
11
.github/workflows/build.yaml
vendored
@@ -5,13 +5,8 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- dev
|
- dev
|
||||||
paths:
|
- http-rest
|
||||||
- .github/workflows/build.yaml
|
|
||||||
- build/*
|
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
|
||||||
- .github/workflows/build.yaml
|
|
||||||
- build/*
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DOCKER_REPO: shenxn/protonmail-bridge
|
DOCKER_REPO: shenxn/protonmail-bridge
|
||||||
@@ -31,7 +26,7 @@ jobs:
|
|||||||
uses: actions/checkout@master
|
uses: actions/checkout@master
|
||||||
- name: Set version
|
- name: Set version
|
||||||
id: version
|
id: version
|
||||||
run: echo "::set-output name=version::`cat build/VERSION`"
|
run: echo "::set-output name=version::`cat VERSION`"
|
||||||
- name: Set repo
|
- name: Set repo
|
||||||
id: repo
|
id: repo
|
||||||
run: if [[ $GITHUB_REF == "refs/heads/master" ]]; then echo "::set-output name=repo::${DOCKER_REPO}"; else echo "::set-output name=repo::${DOCKER_REPO_DEV}"; fi
|
run: if [[ $GITHUB_REF == "refs/heads/master" ]]; then echo "::set-output name=repo::${DOCKER_REPO}"; else echo "::set-output name=repo::${DOCKER_REPO_DEV}"; fi
|
||||||
@@ -49,8 +44,6 @@ jobs:
|
|||||||
- name: Build image without push to registry
|
- name: Build image without push to registry
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
context: ./build
|
|
||||||
file: ./build/Dockerfile
|
|
||||||
platforms: ${{ env.PLATFORMS }}
|
platforms: ${{ env.PLATFORMS }}
|
||||||
push: true
|
push: true
|
||||||
tags: localhost:5000/protonmail-bridge:latest
|
tags: localhost:5000/protonmail-bridge:latest
|
||||||
|
|||||||
80
.github/workflows/deb.yaml
vendored
80
.github/workflows/deb.yaml
vendored
@@ -1,80 +0,0 @@
|
|||||||
name: pack from deb
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- dev
|
|
||||||
paths:
|
|
||||||
- .github/workflows/deb.yaml
|
|
||||||
- deb/*
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- .github/workflows/deb.yaml
|
|
||||||
- deb/*
|
|
||||||
|
|
||||||
env:
|
|
||||||
DOCKER_REPO: shenxn/protonmail-bridge
|
|
||||||
DOCKER_REPO_DEV: ghcr.io/shenxn/protonmail-bridge-dev
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deb:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@master
|
|
||||||
- name: Set version
|
|
||||||
id: version
|
|
||||||
run: echo "::set-output name=version::`cat deb/VERSION`"
|
|
||||||
- name: Set repo
|
|
||||||
id: repo
|
|
||||||
run: if [[ $GITHUB_REF == "refs/heads/master" ]]; then echo "::set-output name=repo::${DOCKER_REPO}"; else echo "::set-output name=repo::${DOCKER_REPO_DEV}"; fi
|
|
||||||
- name: Docker meta
|
|
||||||
id: docker_meta
|
|
||||||
uses: crazy-max/ghaction-docker-meta@v1
|
|
||||||
with:
|
|
||||||
images: ${{ steps.repo.outputs.repo }}
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v1
|
|
||||||
- name: Build image without push
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
context: ./deb
|
|
||||||
file: ./deb/Dockerfile
|
|
||||||
load: true
|
|
||||||
tags: protonmail-bridge:latest
|
|
||||||
- name: Scan image
|
|
||||||
id: scan
|
|
||||||
uses: anchore/scan-action@v2
|
|
||||||
with:
|
|
||||||
image: protonmail-bridge:latest
|
|
||||||
fail-build: true
|
|
||||||
severity-cutoff: critical
|
|
||||||
acs-report-enable: true
|
|
||||||
- name: Upload Anchore scan SARIF report
|
|
||||||
uses: github/codeql-action/upload-sarif@v1
|
|
||||||
with:
|
|
||||||
sarif_file: ${{ steps.scan.outputs.sarif }}
|
|
||||||
- name: Login to DockerHub
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/master' }}
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
|
||||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
|
||||||
- name: Login to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v1
|
|
||||||
if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/dev' }}
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.CR_PAT }}
|
|
||||||
- name: Push image
|
|
||||||
uses: docker/build-push-action@v2
|
|
||||||
with:
|
|
||||||
context: ./deb
|
|
||||||
file: ./deb/Dockerfile
|
|
||||||
tags: |
|
|
||||||
${{ steps.repo.outputs.repo }}:latest
|
|
||||||
${{ steps.repo.outputs.repo }}:${{ steps.version.outputs.version }}
|
|
||||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -0,0 +1 @@
|
|||||||
|
.vscode
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ RUN apt-get update && apt-get install -y libsecret-1-dev
|
|||||||
# Build
|
# Build
|
||||||
WORKDIR /build/
|
WORKDIR /build/
|
||||||
COPY build.sh VERSION /build/
|
COPY build.sh VERSION /build/
|
||||||
|
COPY http_rest_frontend /build/http_rest_frontend
|
||||||
RUN bash build.sh
|
RUN bash build.sh
|
||||||
|
|
||||||
FROM ubuntu:bionic
|
FROM ubuntu:bionic
|
||||||
@@ -16,13 +17,13 @@ EXPOSE 143/tcp
|
|||||||
|
|
||||||
# Install dependencies and protonmail bridge
|
# Install dependencies and protonmail bridge
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends socat pass libsecret-1-0 ca-certificates \
|
&& apt-get install -y --no-install-recommends socat pass libsecret-1-0 ca-certificates dbus \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy bash scripts
|
# Copy bash scripts
|
||||||
COPY gpgparams entrypoint.sh /protonmail/
|
COPY gpgparams entrypoint.sh /srv/protonmail/
|
||||||
|
|
||||||
# Copy protonmail
|
# Copy protonmail
|
||||||
COPY --from=build /build/proton-bridge/proton-bridge /protonmail/
|
COPY --from=build /build/proton-bridge/proton-bridge /srv/protonmail/
|
||||||
|
|
||||||
ENTRYPOINT ["bash", "/protonmail/entrypoint.sh"]
|
ENTRYPOINT ["bash", "/srv/protonmail/entrypoint.sh"]
|
||||||
@@ -9,14 +9,23 @@ git clone https://github.com/ProtonMail/proton-bridge.git
|
|||||||
cd proton-bridge
|
cd proton-bridge
|
||||||
git checkout v$VERSION
|
git checkout v$VERSION
|
||||||
|
|
||||||
|
ls /build
|
||||||
|
|
||||||
|
# Patch HTTP REST frontend
|
||||||
|
rm -rf internal/frontend/cli
|
||||||
|
cp -r /build/http_rest_frontend/cli internal/frontend/cli
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
if ! make build-nogui ; then
|
if ! make build-nogui ; then
|
||||||
# If build fails it's probably because it is a 32bit
|
# If build fails it's probably because it is a 32bit
|
||||||
# system and there was a overflow error on the parser
|
# system and there was a overflow error on the parser
|
||||||
# This is a workaround for this problem found at:
|
# This is a workaround for this problem found at:
|
||||||
# https://github.com/antlr/antlr4/issues/2433#issuecomment-774514106
|
# https://github.com/antlr/antlr4/issues/2433#issuecomment-774514106
|
||||||
find $(go env GOPATH)/pkg/mod/github.com/\!proton\!mail/go-rfc5322*/ -type f -exec sed -i.bak 's/(1<</(int64(1)<</g' {} +
|
# find $(go env GOPATH)/pkg/mod/github.com/\!proton\!mail/go-rfc5322*/ -type f -exec sed -i.bak 's/(1<</(int64(1)<</g' {} +
|
||||||
|
|
||||||
# Try again after implementing the workaround
|
# Try again after implementing the workaround
|
||||||
make build-nogui
|
# make build-nogui
|
||||||
|
|
||||||
|
uname -m
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
*
|
|
||||||
|
|
||||||
!.dockerignore
|
|
||||||
!VERSION
|
|
||||||
!entrypoint.sh
|
|
||||||
!gpgparams
|
|
||||||
!Dockerfile
|
|
||||||
!build.sh
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -ex
|
|
||||||
|
|
||||||
# Initialize
|
|
||||||
if [[ $1 == init ]]; then
|
|
||||||
|
|
||||||
# Initialize pass
|
|
||||||
gpg --generate-key --batch /protonmail/gpgparams
|
|
||||||
pass init pass-key
|
|
||||||
|
|
||||||
# Login
|
|
||||||
/protonmail/proton-bridge --cli $@
|
|
||||||
|
|
||||||
else
|
|
||||||
|
|
||||||
# socat will make the conn appear to come from 127.0.0.1
|
|
||||||
# ProtonMail Bridge currently expects that.
|
|
||||||
# It also allows us to bind to the real ports :)
|
|
||||||
socat TCP-LISTEN:25,fork TCP:127.0.0.1:1025 &
|
|
||||||
socat TCP-LISTEN:143,fork TCP:127.0.0.1:1143 &
|
|
||||||
|
|
||||||
# Start protonmail
|
|
||||||
# Fake a terminal, so it does not quit because of EOF...
|
|
||||||
rm -f faketty
|
|
||||||
mkfifo faketty
|
|
||||||
cat faketty | /protonmail/proton-bridge --cli $@
|
|
||||||
|
|
||||||
fi
|
|
||||||
77
cli.sh
Executable file
77
cli.sh
Executable file
@@ -0,0 +1,77 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
URL_BASE=http://127.0.0.1:1080
|
||||||
|
|
||||||
|
print_help() {
|
||||||
|
echo "Available commands:"
|
||||||
|
echo " login: Calls up the login procedure to add or connect accounts."
|
||||||
|
echo " delete <account>: Remove the account from keychain. You can use index or account name as the parameter."
|
||||||
|
echo " list: Print list of your accounts."
|
||||||
|
echo " info <account>: Print account configuration. You can use index or account name as the parameter."
|
||||||
|
echo " help: Print help messages."
|
||||||
|
echo " exit: Exit the CLI"
|
||||||
|
}
|
||||||
|
|
||||||
|
account_list() {
|
||||||
|
curl ${URL_BASE}/accounts
|
||||||
|
}
|
||||||
|
|
||||||
|
account_login() {
|
||||||
|
read -p "Username: " USERNAME
|
||||||
|
read -sp "Password: " PASSWORD
|
||||||
|
echo
|
||||||
|
read -p "2FA Code (leave empty if not set): " TWO_FACTOR
|
||||||
|
read -p "Mailbox Password (leave empty if not set): " MAILBOX_PASSWORD
|
||||||
|
|
||||||
|
curl ${URL_BASE}/accounts -XPUT \
|
||||||
|
--data-urlencode "username=${USERNAME}" \
|
||||||
|
--data-urlencode "password=${PASSWORD}" \
|
||||||
|
--data-urlencode "two-factor=${TWO_FACTOR}" \
|
||||||
|
--data-urlencode "mailbox-password=${MAILBOX_PASSWORD}"
|
||||||
|
}
|
||||||
|
|
||||||
|
account_delete() {
|
||||||
|
if [[ -z $1 ]]; then
|
||||||
|
echo "Error: delete requires one parameter, which is the index or account name."
|
||||||
|
else
|
||||||
|
read -p "Are you sure you want to delete account $1? " REPLY
|
||||||
|
if [[ $REPLY =~ ^[Yy] ]]; then
|
||||||
|
curl ${URL_BASE}/accounts/$1 -XDELETE
|
||||||
|
else
|
||||||
|
echo "Abort"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
account_info() {
|
||||||
|
if [[ -z $1 ]]; then
|
||||||
|
echo "Error: info requires one parameter, which is the index or account name."
|
||||||
|
else
|
||||||
|
curl ${URL_BASE}/accounts/$1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "CLI to interacte with Proton Bridge HTTP REST interface"
|
||||||
|
print_help
|
||||||
|
while true; do
|
||||||
|
echo
|
||||||
|
read -p ">> " COMMAND ARG
|
||||||
|
case "$COMMAND" in
|
||||||
|
login)
|
||||||
|
account_login;;
|
||||||
|
delete)
|
||||||
|
account_delete $ARG;;
|
||||||
|
list)
|
||||||
|
account_list;;
|
||||||
|
info)
|
||||||
|
account_info $ARG;;
|
||||||
|
help)
|
||||||
|
print_help;;
|
||||||
|
exit)
|
||||||
|
exit 0;;
|
||||||
|
*)
|
||||||
|
echo "Invalid command"
|
||||||
|
esac
|
||||||
|
done
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
*
|
|
||||||
|
|
||||||
!.dockerignore
|
|
||||||
!VERSION
|
|
||||||
!entrypoint.sh
|
|
||||||
!install.sh
|
|
||||||
!gpgparams
|
|
||||||
!Dockerfile
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
FROM ubuntu:bionic
|
|
||||||
LABEL maintainer="Xiaonan Shen <s@sxn.dev>"
|
|
||||||
|
|
||||||
EXPOSE 25/tcp
|
|
||||||
EXPOSE 143/tcp
|
|
||||||
|
|
||||||
WORKDIR /protonmail
|
|
||||||
|
|
||||||
# Copy bash scripts
|
|
||||||
COPY gpgparams install.sh entrypoint.sh VERSION /protonmail/
|
|
||||||
|
|
||||||
# Install dependencies and protonmail bridge
|
|
||||||
RUN bash install.sh
|
|
||||||
|
|
||||||
ENTRYPOINT ["bash", "/protonmail/entrypoint.sh"]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
2.1.1-1
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -ex
|
|
||||||
|
|
||||||
# Initialize
|
|
||||||
if [[ $1 == init ]]; then
|
|
||||||
|
|
||||||
# # Parse parameters
|
|
||||||
# TFP="" # Default empty two factor passcode
|
|
||||||
# shift # skip `init`
|
|
||||||
# while [[ $# -gt 0 ]]; do
|
|
||||||
# key="$1"
|
|
||||||
# case $key in
|
|
||||||
# -u|--username)
|
|
||||||
# USERNAME="$2"
|
|
||||||
# ;;
|
|
||||||
# -p|--password)
|
|
||||||
# PASSWORD="$2"
|
|
||||||
# ;;
|
|
||||||
# -t|--twofactor)
|
|
||||||
# TWOFACTOR="$2"
|
|
||||||
# ;;
|
|
||||||
# esac
|
|
||||||
# shift
|
|
||||||
# shift
|
|
||||||
# done
|
|
||||||
|
|
||||||
# Initialize pass
|
|
||||||
gpg --generate-key --batch /protonmail/gpgparams
|
|
||||||
pass init pass-key
|
|
||||||
|
|
||||||
# Login
|
|
||||||
protonmail-bridge --cli
|
|
||||||
|
|
||||||
else
|
|
||||||
|
|
||||||
# socat will make the conn appear to come from 127.0.0.1
|
|
||||||
# ProtonMail Bridge currently expects that.
|
|
||||||
# It also allows us to bind to the real ports :)
|
|
||||||
socat TCP-LISTEN:25,fork TCP:127.0.0.1:1025 &
|
|
||||||
socat TCP-LISTEN:143,fork TCP:127.0.0.1:1143 &
|
|
||||||
|
|
||||||
# Start protonmail
|
|
||||||
# Fake a terminal, so it does not quit because of EOF...
|
|
||||||
rm -f faketty
|
|
||||||
mkfifo faketty
|
|
||||||
cat faketty | protonmail-bridge --cli
|
|
||||||
|
|
||||||
fi
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
%no-protection
|
|
||||||
%echo Generating a basic OpenPGP key
|
|
||||||
Key-Type: RSA
|
|
||||||
Key-Length: 2048
|
|
||||||
Name-Real: pass-key
|
|
||||||
Expire-Date: 0
|
|
||||||
%commit
|
|
||||||
%echo done
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -ex
|
|
||||||
|
|
||||||
VERSION=`cat VERSION`
|
|
||||||
DEB_FILE=protonmail-bridge_${VERSION}_amd64.deb
|
|
||||||
|
|
||||||
# Install dependents
|
|
||||||
apt-get update
|
|
||||||
apt-get install -y --no-install-recommends socat pass ca-certificates
|
|
||||||
|
|
||||||
# Build time dependencies
|
|
||||||
apt-get install -y wget binutils xz-utils
|
|
||||||
|
|
||||||
# Repack deb (remove unnecessary dependencies)
|
|
||||||
mkdir deb
|
|
||||||
cd deb
|
|
||||||
wget -q https://protonmail.com/download/bridge/${DEB_FILE}
|
|
||||||
ar x -v ${DEB_FILE}
|
|
||||||
mkdir control
|
|
||||||
tar zxvf control.tar.gz -C control
|
|
||||||
sed -i "s/^Depends: .*$/Depends: libgl1, libc6, libsecret-1-0, libstdc++6, libgcc1/" control/control
|
|
||||||
cd control
|
|
||||||
tar zcvf ../control.tar.gz .
|
|
||||||
cd ../
|
|
||||||
ar rcs -v ${DEB_FILE} debian-binary control.tar.gz data.tar.gz
|
|
||||||
cd ../
|
|
||||||
|
|
||||||
# Install protonmail bridge
|
|
||||||
apt-get install -y --no-install-recommends ./deb/${DEB_FILE}
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
apt-get purge -y wget binutils xz-utils
|
|
||||||
apt-get autoremove -y
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
rm -rf deb
|
|
||||||
37
entrypoint.sh
Normal file
37
entrypoint.sh
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Generate gpg keys
|
||||||
|
if [ ! -f ${HOME}/.gnupg ]; then
|
||||||
|
echo "Generateing gpg keys..."
|
||||||
|
# set GNUPGHOME as a workaround for
|
||||||
|
#
|
||||||
|
# gpg-agent[106]: error binding socket to '/root/.gnupg/S.gpg-agent': File name too long
|
||||||
|
#
|
||||||
|
# when using docker volume mount
|
||||||
|
#
|
||||||
|
# ref: https://dev.gnupg.org/T2964
|
||||||
|
#
|
||||||
|
export GNUPGHOME=/tmp/gnupg
|
||||||
|
mkdir ${GNUPGHOME}
|
||||||
|
chmod 700 ${GNUPGHOME}
|
||||||
|
gpg --generate-key --batch /srv/protonmail/gpgparams
|
||||||
|
pkill gpg-agent
|
||||||
|
mv ${GNUPGHOME} ${HOME}/.gnupg
|
||||||
|
export GNUPGHOME=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Initialize pass
|
||||||
|
if [ ! -f ${HOME}/.password-store/.gpg-id ]; then
|
||||||
|
echo "Initializing pass"
|
||||||
|
pass init pass-key
|
||||||
|
fi
|
||||||
|
|
||||||
|
# socat will make the conn appear to come from 127.0.0.1
|
||||||
|
# ProtonMail Bridge currently expects that.
|
||||||
|
# It also allows us to bind to the real ports :)
|
||||||
|
socat TCP-LISTEN:25,fork TCP:127.0.0.1:1025 &
|
||||||
|
socat TCP-LISTEN:143,fork TCP:127.0.0.1:1143 &
|
||||||
|
|
||||||
|
/srv/protonmail/proton-bridge --cli $@
|
||||||
147
http_rest_frontend/cli/accounts.go
Normal file
147
http_rest_frontend/cli/accounts.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
// Package cli provides HTTP interface of the bridge
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||||
|
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f *frontendCLI) loginAccount(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
fmt.Fprintf(w, "ParseForm() err: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username := r.FormValue("username")
|
||||||
|
password := r.FormValue("password")
|
||||||
|
twoFactor := r.FormValue("two-factor")
|
||||||
|
mailboxPassword := r.FormValue("mailbox-password")
|
||||||
|
client, auth, err := f.bridge.Login(username, []byte(password))
|
||||||
|
if err != nil {
|
||||||
|
f.processAPIError(err)
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
fmt.Fprintln(w, "Server error:", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth.HasTwoFactor() {
|
||||||
|
if twoFactor == "" {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
fmt.Fprintln(w, "2FA enabled for the account but a 2FA code was not provided.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = client.Auth2FA(context.Background(), twoFactor)
|
||||||
|
if err != nil {
|
||||||
|
f.processAPIError(err)
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
fmt.Fprintln(w, "Server error:", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth.HasMailboxPassword() {
|
||||||
|
if mailboxPassword == "" {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
fmt.Fprintln(w, "Two password mode enabled but a mailbox password was not provided.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mailboxPassword = password
|
||||||
|
}
|
||||||
|
user, err := f.bridge.FinishLogin(client, auth, []byte(mailboxPassword))
|
||||||
|
if err != nil {
|
||||||
|
f.processAPIError(err)
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
fmt.Fprintln(w, "Server error:", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "Account %s was added successfully.\n", user.Username())
|
||||||
|
f.printAccountInfo(w, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) deleteAccount(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
|
account := params.ByName("account")
|
||||||
|
user := f.getUserByIndexOrName(account)
|
||||||
|
if user == nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
fmt.Fprintf(w, "Account %s does not exist.\n", account)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
account = user.Username()
|
||||||
|
if err := f.bridge.DeleteUser(user.ID(), true); err != nil {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprintln(w, "Cannot delete account: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "Account %s was deleted successfully.\n", account)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) listAccounts(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
users := f.bridge.GetUsers()
|
||||||
|
if len(users) == 0 {
|
||||||
|
fmt.Fprintln(w, "No account found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
spacing := "%-2d: %-20s (%-15s, %-15s)\n"
|
||||||
|
fmt.Fprintf(w, strings.ReplaceAll(spacing, "d", "s"), "#", "account", "status", "address mode")
|
||||||
|
for idx, user := range users {
|
||||||
|
connected := "disconnected"
|
||||||
|
if user.IsConnected() {
|
||||||
|
connected = "connected"
|
||||||
|
}
|
||||||
|
mode := "split"
|
||||||
|
if user.IsCombinedAddressMode() {
|
||||||
|
mode = "combined"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, spacing, idx, user.Username(), connected, mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) showAccountInfo(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
||||||
|
account := params.ByName("account")
|
||||||
|
user := f.getUserByIndexOrName(account)
|
||||||
|
if user == nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
fmt.Fprintf(w, "Account %s does not exist.\n", account)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !user.IsConnected() {
|
||||||
|
fmt.Fprintf(w, "Please login to %s to get email client configuration.\n", user.Username())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.printAccountInfo(w, user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) printAccountInfo(w io.Writer, user types.User) {
|
||||||
|
if user.IsCombinedAddressMode() {
|
||||||
|
f.printAccountAddressInfo(w, user, user.GetPrimaryAddress())
|
||||||
|
} else {
|
||||||
|
for _, address := range user.GetAddresses() {
|
||||||
|
f.printAccountAddressInfo(w, user, address)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) printAccountAddressInfo(w io.Writer, user types.User, address string) {
|
||||||
|
fmt.Fprintln(w, "Configuration for", address)
|
||||||
|
smtpSecurity := "STARTTLS"
|
||||||
|
if f.settings.GetBool(settings.SMTPSSLKey) {
|
||||||
|
smtpSecurity = "SSL"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "IMAP port: %d\nIMAP security: %s\nSMTP port: %d\nSMTP security: %s\nUsername: %s\nPassword: %s\n",
|
||||||
|
143,
|
||||||
|
"STARTTLS",
|
||||||
|
25,
|
||||||
|
smtpSecurity,
|
||||||
|
address,
|
||||||
|
user.GetBridgePassword(),
|
||||||
|
)
|
||||||
|
fmt.Fprintln(w, "")
|
||||||
|
}
|
||||||
24
http_rest_frontend/cli/accounts_util.go
Normal file
24
http_rest_frontend/cli/accounts_util.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f *frontendCLI) getUserByIndexOrName(account string) types.User {
|
||||||
|
users := f.bridge.GetUsers()
|
||||||
|
numberOfAccounts := len(users)
|
||||||
|
if index, err := strconv.Atoi(account); err == nil {
|
||||||
|
if index < 0 || index >= numberOfAccounts {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return users[index]
|
||||||
|
}
|
||||||
|
for _, user := range users {
|
||||||
|
if user.Username() == account {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
157
http_rest_frontend/cli/frontend.go
Normal file
157
http_rest_frontend/cli/frontend.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
// Package cli provides HTTP interface of the bridge
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/proton-bridge/internal/config/settings"
|
||||||
|
"github.com/ProtonMail/proton-bridge/internal/events"
|
||||||
|
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
|
||||||
|
"github.com/ProtonMail/proton-bridge/internal/locations"
|
||||||
|
"github.com/ProtonMail/proton-bridge/internal/updater"
|
||||||
|
"github.com/ProtonMail/proton-bridge/pkg/listener"
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type frontendCLI struct {
|
||||||
|
*httprouter.Router
|
||||||
|
|
||||||
|
locations *locations.Locations
|
||||||
|
settings *settings.Settings
|
||||||
|
eventListener listener.Listener
|
||||||
|
updater types.Updater
|
||||||
|
bridge types.Bridger
|
||||||
|
restarter types.Restarter
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(
|
||||||
|
panicHandler types.PanicHandler,
|
||||||
|
|
||||||
|
locations *locations.Locations,
|
||||||
|
settings *settings.Settings,
|
||||||
|
eventListener listener.Listener,
|
||||||
|
updater types.Updater,
|
||||||
|
bridge types.Bridger,
|
||||||
|
restarter types.Restarter,
|
||||||
|
) *frontendCLI {
|
||||||
|
fe := &frontendCLI{
|
||||||
|
Router: httprouter.New(),
|
||||||
|
locations: locations,
|
||||||
|
settings: settings,
|
||||||
|
eventListener: eventListener,
|
||||||
|
updater: updater,
|
||||||
|
bridge: bridge,
|
||||||
|
restarter: restarter,
|
||||||
|
}
|
||||||
|
|
||||||
|
fe.PUT("/accounts", fe.loginAccount)
|
||||||
|
fe.GET("/accounts", fe.listAccounts)
|
||||||
|
fe.GET("/accounts/:account", fe.showAccountInfo)
|
||||||
|
fe.DELETE("/accounts/:account", fe.deleteAccount)
|
||||||
|
|
||||||
|
return fe
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) loginWithEnv() {
|
||||||
|
if len(f.bridge.GetUsers()) > 0 {
|
||||||
|
fmt.Println("More than 0 accounts found. Skip auto login.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username := os.Getenv("PROTON_USERNAME")
|
||||||
|
password := os.Getenv("PROTON_PASSWORD")
|
||||||
|
if username == "" {
|
||||||
|
logrus.Info("PROTON_USERNAME and PROTON_PASSWORD are not set. Skip auto login.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client, auth, err := f.bridge.Login(username, []byte(password))
|
||||||
|
if err != nil {
|
||||||
|
f.processAPIError(err)
|
||||||
|
logrus.WithError(err).Warn("Login failed.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth.HasTwoFactor() {
|
||||||
|
twoFactor := os.Getenv("PROTON_2FA")
|
||||||
|
if twoFactor == "" {
|
||||||
|
logrus.Warn("Login failed: 2FA enabled for the account but PROTON_2FA was not set.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = client.Auth2FA(context.Background(), twoFactor)
|
||||||
|
if err != nil {
|
||||||
|
f.processAPIError(err)
|
||||||
|
logrus.WithError(err).Warn("Login failed.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mailboxPassword := password
|
||||||
|
if auth.HasMailboxPassword() {
|
||||||
|
mailboxPassword = os.Getenv("PROTON_MAILBOX_PASSWORD")
|
||||||
|
if mailboxPassword == "" {
|
||||||
|
logrus.Warn("Login failed: Two password mode enabled but PROTON_MAILBOX_PASSWORD was not set.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
user, err := f.bridge.FinishLogin(client, auth, []byte(mailboxPassword))
|
||||||
|
if err != nil {
|
||||||
|
f.processAPIError(err)
|
||||||
|
logrus.WithError(err).Warn("Login failed.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logrus.Infof("Account %s was added successfully.\n", user.Username())
|
||||||
|
if strings.ToLower(os.Getenv("PROTON_PRINT_ACCOUNT_INFO")) != "off" {
|
||||||
|
f.printAccountInfo(os.Stdout, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) watchEvents() {
|
||||||
|
errorCh := f.eventListener.ProvideChannel(events.ErrorEvent)
|
||||||
|
credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent)
|
||||||
|
internetOffCh := f.eventListener.ProvideChannel(events.InternetOffEvent)
|
||||||
|
internetOnCh := f.eventListener.ProvideChannel(events.InternetOnEvent)
|
||||||
|
addressChangedCh := f.eventListener.ProvideChannel(events.AddressChangedEvent)
|
||||||
|
addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
|
||||||
|
logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent)
|
||||||
|
certIssue := f.eventListener.ProvideChannel(events.TLSCertIssue)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case errorDetails := <-errorCh:
|
||||||
|
fmt.Println("Bridge failed:", errorDetails)
|
||||||
|
case <-credentialsErrorCh:
|
||||||
|
f.notifyCredentialsError()
|
||||||
|
case <-internetOffCh:
|
||||||
|
f.notifyInternetOff()
|
||||||
|
case <-internetOnCh:
|
||||||
|
f.notifyInternetOn()
|
||||||
|
case address := <-addressChangedCh:
|
||||||
|
fmt.Printf("Address changed for %s. You may need to reconfigure your email client.", address)
|
||||||
|
case address := <-addressChangedLogoutCh:
|
||||||
|
f.notifyLogout(address)
|
||||||
|
case userID := <-logoutCh:
|
||||||
|
user, err := f.bridge.GetUser(userID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.notifyLogout(user.Username())
|
||||||
|
case <-certIssue:
|
||||||
|
f.notifyCertIssue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) Loop() error {
|
||||||
|
f.loginWithEnv()
|
||||||
|
http.ListenAndServe(":1080", f)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) {}
|
||||||
|
func (f *frontendCLI) WaitUntilFrontendIsReady() {}
|
||||||
|
func (f *frontendCLI) SetVersion(version updater.VersionInfo) {}
|
||||||
|
func (f *frontendCLI) NotifySilentUpdateInstalled() {}
|
||||||
|
func (f *frontendCLI) NotifySilentUpdateError(err error) {}
|
||||||
57
http_rest_frontend/cli/utils.go
Normal file
57
http_rest_frontend/cli/utils.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (f *frontendCLI) processAPIError(err error) {
|
||||||
|
switch err {
|
||||||
|
case pmapi.ErrNoConnection:
|
||||||
|
f.notifyInternetOff()
|
||||||
|
case pmapi.ErrUpgradeApplication:
|
||||||
|
f.notifyNeedUpgrade()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) notifyInternetOff() {
|
||||||
|
logrus.Warn("Internet connection is not available.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) notifyInternetOn() {
|
||||||
|
logrus.Info("Internet connection is available again.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) notifyLogout(address string) {
|
||||||
|
logrus.Infof("Account %s is disconnected. Login to continue using this account with email client.", address)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) notifyNeedUpgrade() {
|
||||||
|
logrus.Info("Upgrade needed. Please download and install the newest version of application.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) notifyCredentialsError() {
|
||||||
|
logrus.Error(`ProtonMail Bridge is not able to detect a supported password manager
|
||||||
|
(secret-service or pass). Please install and set up a supported password manager
|
||||||
|
and restart the application.
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frontendCLI) notifyCertIssue() {
|
||||||
|
// Print in 80-column width.
|
||||||
|
logrus.Error(`Connection security error: Your network connection to Proton services may
|
||||||
|
be insecure.
|
||||||
|
|
||||||
|
Description:
|
||||||
|
ProtonMail Bridge was not able to establish a secure connection to Proton
|
||||||
|
servers due to a TLS certificate error. This means your connection may
|
||||||
|
potentially be insecure and susceptible to monitoring by third parties.
|
||||||
|
|
||||||
|
Recommendation:
|
||||||
|
* If you trust your network operator, you can continue to use ProtonMail
|
||||||
|
as usual.
|
||||||
|
* If you don't trust your network operator, reconnect to ProtonMail over a VPN
|
||||||
|
(such as ProtonVPN) which encrypts your Internet connection, or use
|
||||||
|
a different network to access ProtonMail.
|
||||||
|
`)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user