parent
68bc6cf294
commit
f382da89da
@ -1,12 +0,0 @@
|
||||
files:
|
||||
"/etc/profile.d/appenv.sh":
|
||||
mode: "000644"
|
||||
owner: root
|
||||
group: root
|
||||
content: |
|
||||
#!/bin/sh
|
||||
export PGHOST=$(/opt/elasticbeanstalk/bin/get-config environment -k PGHOST)
|
||||
export PGPORT=$(/opt/elasticbeanstalk/bin/get-config environment -k PGPORT)
|
||||
export PGDATABASE=$(/opt/elasticbeanstalk/bin/get-config environment -k PGDATABASE)
|
||||
export PGUSER=$(/opt/elasticbeanstalk/bin/get-config environment -k PGUSER)
|
||||
export PGPASSWORD=$(/opt/elasticbeanstalk/bin/get-config environment -k PGPASSWORD)
|
@ -1,3 +0,0 @@
|
||||
packages:
|
||||
yum:
|
||||
libffi-devel: [] # for misaka
|
1
.platform/confighooks
Symbolic link
1
.platform/confighooks
Symbolic link
@ -0,0 +1 @@
|
||||
hooks
|
5
.platform/files/cloudflared.conf
Normal file
5
.platform/files/cloudflared.conf
Normal file
@ -0,0 +1,5 @@
|
||||
tunnel: liberapay-prod
|
||||
credentials-file: /etc/cloudflared/liberapay-prod.json
|
||||
|
||||
ingress:
|
||||
- service: unix:/var/app/current/socket
|
43
.platform/files/cloudflared@.service
Normal file
43
.platform/files/cloudflared@.service
Normal file
@ -0,0 +1,43 @@
|
||||
[Unit]
|
||||
Description=Cloudflare Tunnel daemon for web app #%i
|
||||
After=network.target webapp@%i.service
|
||||
Wants=network.target
|
||||
BindsTo=webapp@%i.service
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
ExecStart=/usr/local/bin/cloudflared --no-autoupdate tunnel --config /etc/cloudflared/cloudflared.conf run
|
||||
User=cloudflared
|
||||
Group=cloudflared
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
CapabilityBoundingSet=
|
||||
AmbientCapabilities=
|
||||
PrivateUsers=true
|
||||
|
||||
NoNewPrivileges=true
|
||||
LimitNOFILE=1048576
|
||||
UMask=0077
|
||||
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectHostname=true
|
||||
ProtectClock=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelLogs=true
|
||||
ProtectControlGroups=true
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
RestrictNamespaces=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
RestrictRealtime=true
|
||||
RestrictSUIDSGID=true
|
||||
RemoveIPC=true
|
||||
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallFilter=~@privileged @resources
|
||||
SystemCallArchitectures=native
|
7
.platform/files/pgenv.sh
Executable file
7
.platform/files/pgenv.sh
Executable file
@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
export PGHOST=$(/opt/elasticbeanstalk/bin/get-config environment -k PGHOST)
|
||||
export PGPORT=$(/opt/elasticbeanstalk/bin/get-config environment -k PGPORT)
|
||||
export PGDATABASE=$(/opt/elasticbeanstalk/bin/get-config environment -k PGDATABASE)
|
||||
export PGUSER=$(/opt/elasticbeanstalk/bin/get-config environment -k PGUSER)
|
||||
export PGPASSWORD=$(/opt/elasticbeanstalk/bin/get-config environment -k PGPASSWORD)
|
21
.platform/files/webapp@.service
Normal file
21
.platform/files/webapp@.service
Normal file
@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
Description=Web application daemon #%i
|
||||
Requires=webapp@%i.socket
|
||||
|
||||
[Service]
|
||||
User=webapp
|
||||
Group=webapp
|
||||
Type=notify
|
||||
WorkingDirectory=/var/app/current/
|
||||
EnvironmentFile=/opt/elasticbeanstalk/deployment/env
|
||||
Sockets=webapp@%i.socket
|
||||
ExecStart=/var/app/venv/staging-LQM1lest/bin/python app.py --bind fd:3
|
||||
|
||||
Restart=always
|
||||
|
||||
StandardOutput=syslog
|
||||
StandardError=syslog
|
||||
SyslogIdentifier=web
|
||||
|
||||
# When stopping, send the initial SIGTERM to the main process only.
|
||||
KillMode=mixed
|
9
.platform/files/webapp@.socket
Normal file
9
.platform/files/webapp@.socket
Normal file
@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Socket of web application daemon #%i
|
||||
|
||||
[Socket]
|
||||
ListenStream=/var/app/%i/socket
|
||||
Service=webapp@%i.service
|
||||
SocketUser=webapp
|
||||
SocketGroup=cloudflared
|
||||
SocketMode=0660
|
37
.platform/hooks/postdeploy/01_deploy.sh
Executable file
37
.platform/hooks/postdeploy/01_deploy.sh
Executable file
@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Tell bash to be strict and log everything
|
||||
set -eux
|
||||
|
||||
# Compute the deployment ID
|
||||
deploy_id=$(($(cat /var/app/_deploy_id 2>/dev/null || echo 0) + 1))
|
||||
max_deploy_id=$((deploy_id + 99))
|
||||
while systemctl is-active --quiet webapp@$deploy_id.service; do
|
||||
let deploy_id++
|
||||
if [ $deploy_id -gt $max_deploy_id ]; then
|
||||
echo "this script appears to be stuck in an infinite loop, exiting"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Rename the app directory
|
||||
app_dir=$(pwd)
|
||||
rm -rf /var/app/$deploy_id
|
||||
mv $app_dir /var/app/$deploy_id
|
||||
ln -s /var/app/$deploy_id $app_dir
|
||||
|
||||
# Start the new instance and its proxy
|
||||
systemctl start webapp@$deploy_id.service cloudflared@$deploy_id.service
|
||||
|
||||
# Save the new deployment ID
|
||||
echo $deploy_id >/var/app/_deploy_id
|
||||
|
||||
# Stop the old instance(s) and their proxies
|
||||
let i=1
|
||||
while [ $i -lt $deploy_id ]; do
|
||||
systemctl stop cloudflared@$i.service
|
||||
systemctl stop webapp@$i.service
|
||||
systemctl stop webapp@$i.socket
|
||||
rm -rf /var/app/$i
|
||||
let i++
|
||||
done
|
44
.platform/hooks/prebuild/01_install.sh
Executable file
44
.platform/hooks/prebuild/01_install.sh
Executable file
@ -0,0 +1,44 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Tell bash to be strict and log everything
|
||||
set -eux
|
||||
|
||||
# Install libffi-devel for misaka, and htop for when I want to look at what's going on
|
||||
yum install -y libffi-devel htop
|
||||
# Install PostgreSQL client tools and libraries
|
||||
amazon-linux-extras install -y postgresql11
|
||||
|
||||
# Automatically set the PG* environment variables so that `psql` connects to the liberapay database by default
|
||||
install -m 644 -o root -g root -t /etc/profile.d .platform/files/pgenv.sh
|
||||
|
||||
# Install the systemd service files for the webapp and cloudflared
|
||||
install -m 644 -o root -g root -t /etc/systemd/system .platform/files/cloudflared@.service
|
||||
install -m 644 -o root -g root -t /etc/systemd/system .platform/files/webapp@.service
|
||||
install -m 644 -o root -g root -t /etc/systemd/system .platform/files/webapp@.socket
|
||||
systemctl daemon-reload
|
||||
|
||||
# Install cloudflared, directly from GitHub
|
||||
if ! which cloudflared 2>/dev/null || [ $(cloudflared version) != "cloudflared version 2021.5.8 "* ]; then
|
||||
wget https://github.com/cloudflare/cloudflared/releases/download/2021.5.8/cloudflared-linux-amd64
|
||||
hash=$(sha256sum cloudflared-linux-amd64 | cut -d' ' -f1)
|
||||
expected_hash=224cd850cb042a5da1d15432063ed04bf8764241de769338e65c44639ed6c28e
|
||||
if [ $hash != $expected_hash ]; then
|
||||
echo "cloudflared binary downloaded from GitHub doesn't match expected hash: $hash != $expected_hash"
|
||||
exit 1
|
||||
fi
|
||||
install -m 755 -o root -g root cloudflared-linux-amd64 /usr/local/bin/cloudflared
|
||||
rm cloudflared-linux-amd64
|
||||
fi
|
||||
|
||||
# Create the cloudflared system user and group
|
||||
groupadd -r cloudflared || true
|
||||
useradd -r -g cloudflared cloudflared || true
|
||||
|
||||
# Install the Cloudflare Tunnel configuration and credentials files
|
||||
install -o cloudflared -g cloudflared -m 755 -d /etc/cloudflared
|
||||
install -o cloudflared -g cloudflared -m 644 -t /etc/cloudflared .platform/files/cloudflared.conf
|
||||
if ! [ -f /etc/cloudflared/liberapay-prod.json ]; then
|
||||
aws s3 cp s3://serverfiles.liberapay.org/liberapay-prod.json liberapay-prod.json
|
||||
install -o cloudflared -g cloudflared -m 644 -t /etc/cloudflared liberapay-prod.json
|
||||
rm liberapay-prod.json
|
||||
fi
|
@ -1,3 +0,0 @@
|
||||
#!/bin/bash -eu
|
||||
|
||||
amazon-linux-extras install -y postgresql11
|
@ -60,7 +60,6 @@ from liberapay.utils.state_chain import (
|
||||
merge_responses,
|
||||
overwrite_status_code_of_gateway_errors,
|
||||
raise_response_to_OPTIONS_request,
|
||||
reject_requests_bypassing_proxy,
|
||||
return_500_for_exception,
|
||||
set_output_to_None,
|
||||
turn_socket_error_into_50X,
|
||||
@ -198,8 +197,6 @@ algorithm.functions = [
|
||||
raise_response_to_OPTIONS_request,
|
||||
set_output_to_None,
|
||||
|
||||
reject_requests_bypassing_proxy,
|
||||
|
||||
canonize,
|
||||
algorithm['extract_accept_header'],
|
||||
set_default_security_headers,
|
||||
@ -325,46 +322,19 @@ aspen.http.request.Querystring.serialize = _Querystring_serialize
|
||||
if hasattr(pando.http.request.Request, 'source'):
|
||||
raise Warning('pando.http.request.Request.source already exists')
|
||||
def _source(self):
|
||||
def f():
|
||||
addr = self.environ.get('REMOTE_ADDR') or self.environ[b'REMOTE_ADDR']
|
||||
addr = ip_address(addr.decode('ascii') if type(addr) is bytes else addr)
|
||||
trusted_proxies = getattr(self.website, 'trusted_proxies', None)
|
||||
forwarded_for = self.headers.get(b'X-Forwarded-For')
|
||||
self.__dict__['bypasses_proxy'] = bool(trusted_proxies)
|
||||
if not trusted_proxies or not forwarded_for:
|
||||
return addr
|
||||
for networks in trusted_proxies:
|
||||
is_trusted = False
|
||||
for network in networks:
|
||||
is_trusted = addr.is_private if network == 'private' else addr in network
|
||||
if is_trusted:
|
||||
break
|
||||
if not is_trusted:
|
||||
return addr
|
||||
i = forwarded_for.rfind(b',')
|
||||
try:
|
||||
addr = ip_address(forwarded_for[i+1:].decode('ascii').strip())
|
||||
except (UnicodeDecodeError, ValueError):
|
||||
return addr
|
||||
if i == -1:
|
||||
if networks is trusted_proxies[-1]:
|
||||
break
|
||||
return addr
|
||||
forwarded_for = forwarded_for[:i]
|
||||
self.__dict__['bypasses_proxy'] = False
|
||||
return addr
|
||||
r = f()
|
||||
self.__dict__['source'] = r
|
||||
return r
|
||||
if 'source' not in self.__dict__:
|
||||
addr = (
|
||||
self.headers.get(b'Cf-Connecting-Ip') or
|
||||
self.environ.get(b'REMOTE_ADDR') or
|
||||
self.environ.get('REMOTE_ADDR') or
|
||||
'0.0.0.0'
|
||||
)
|
||||
if isinstance(addr, bytes):
|
||||
addr = addr.decode()
|
||||
self.__dict__['source'] = ip_address(addr)
|
||||
return self.__dict__['source']
|
||||
pando.http.request.Request.source = property(_source)
|
||||
|
||||
if hasattr(pando.http.request.Request, 'bypasses_proxy'):
|
||||
raise Warning('pando.http.request.Request.bypasses_proxy already exists')
|
||||
def _bypasses_proxy(self):
|
||||
self.source
|
||||
return self.__dict__['bypasses_proxy']
|
||||
pando.http.request.Request.bypasses_proxy = property(_bypasses_proxy)
|
||||
|
||||
if hasattr(pando.http.request.Request, 'find_input_name'):
|
||||
raise Warning('pando.http.request.Request.find_input_name already exists')
|
||||
def _find_input_name(self, value):
|
||||
|
@ -37,13 +37,6 @@ def raise_response_to_OPTIONS_request(request, response):
|
||||
raise response
|
||||
|
||||
|
||||
def reject_requests_bypassing_proxy(request, response):
|
||||
"""Reject requests that bypass Cloudflare, except health checks.
|
||||
"""
|
||||
if request.bypasses_proxy and request.path.raw != '/callbacks/health':
|
||||
raise response.error(403, "The request bypassed a proxy.")
|
||||
|
||||
|
||||
def canonize(request, website):
|
||||
"""Enforce a certain scheme and hostname.
|
||||
|
||||
|
@ -1,13 +1,10 @@
|
||||
from decimal import Decimal
|
||||
from ipaddress import ip_network
|
||||
import json
|
||||
import logging
|
||||
from operator import itemgetter
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
from tempfile import mkstemp
|
||||
from time import time
|
||||
import traceback
|
||||
|
||||
import babel.localedata
|
||||
@ -20,7 +17,6 @@ from postgres.cursors import SimpleRowCursor
|
||||
import psycopg2
|
||||
from psycopg2.extensions import adapt, AsIs, new_type, register_adapter, register_type
|
||||
from psycopg2_pool import PoolError
|
||||
import requests
|
||||
import sass
|
||||
import sentry_sdk
|
||||
from state_chain import StateChain
|
||||
@ -43,7 +39,7 @@ from liberapay.models.payin import Payin
|
||||
from liberapay.models.repository import Repository
|
||||
from liberapay.models.tip import Tip
|
||||
from liberapay.security.crypto import Cryptograph
|
||||
from liberapay.utils import find_files, markdown, mkdir_p, resolve, urlquote
|
||||
from liberapay.utils import find_files, markdown, resolve
|
||||
from liberapay.utils.emails import compile_email_spt
|
||||
from liberapay.utils.http_caching import asset_etag
|
||||
from liberapay.utils.query_cache import QueryCache
|
||||
@ -269,7 +265,6 @@ class AppConf:
|
||||
stripe_connect_id=str,
|
||||
stripe_publishable_key=str,
|
||||
stripe_secret_key=str,
|
||||
trusted_proxies=list,
|
||||
twitch_id=str,
|
||||
twitch_secret=str,
|
||||
twitter_callback=str,
|
||||
@ -315,40 +310,6 @@ def app_conf(db):
|
||||
return {'app_conf': app_conf}
|
||||
|
||||
|
||||
def trusted_proxies(app_conf, env, tell_sentry):
|
||||
if not app_conf:
|
||||
return {'trusted_proxies': []}
|
||||
def parse_network(net):
|
||||
if net == 'private':
|
||||
return [net]
|
||||
elif net.startswith('https://'):
|
||||
d = env.log_dir + '/trusted_proxies/'
|
||||
mkdir_p(d)
|
||||
filename = d + urlquote(net, '')
|
||||
skip_download = (
|
||||
os.path.exists(filename) and
|
||||
os.stat(filename).st_size > 0 and
|
||||
os.stat(filename).st_mtime > time() - 60*60*24*7
|
||||
)
|
||||
if not skip_download:
|
||||
tmpfd, tmp_path = mkstemp(dir=d)
|
||||
with open(tmpfd, 'w') as f:
|
||||
f.write(requests.get(net).text)
|
||||
os.rename(tmp_path, filename)
|
||||
with open(filename, 'rb') as f:
|
||||
return [ip_network(x) for x in f.read().decode('ascii').strip().split()]
|
||||
else:
|
||||
return [ip_network(net)]
|
||||
try:
|
||||
return {'trusted_proxies': [
|
||||
sum((parse_network(net) for net in networks), [])
|
||||
for networks in (app_conf.trusted_proxies or ())
|
||||
]}
|
||||
except Exception as e:
|
||||
tell_sentry(e, {})
|
||||
return {'trusted_proxies': []}
|
||||
|
||||
|
||||
def mail(app_conf, env, project_root='.'):
|
||||
if not app_conf:
|
||||
return
|
||||
@ -873,7 +834,6 @@ full_chain = StateChain(
|
||||
accounts_elsewhere,
|
||||
load_scss_variables,
|
||||
s3,
|
||||
trusted_proxies,
|
||||
currency_exchange_rates,
|
||||
)
|
||||
|
||||
|
@ -52,7 +52,6 @@ INSERT INTO app_conf (key, value) VALUES
|
||||
('stripe_connect_id', '"ca_DEYxiYHBHZtGj32l9uczcsunbQOcRq8H"'::jsonb),
|
||||
('stripe_publishable_key', '"pk_test_rGZY3Q7ba61df50X0h70iHeZ"'::jsonb),
|
||||
('stripe_secret_key', '"sk_test_QTUa8AqWXyU2feC32glNgDQd"'::jsonb),
|
||||
('trusted_proxies', '[]'::jsonb),
|
||||
('twitch_id', '"9ro3g4slh0de5yijy6rqb2p0jgd7hi"'::jsonb),
|
||||
('twitch_secret', '"o090sc7828d7gljtrqc5n4vcpx3bfx"'::jsonb),
|
||||
('twitter_callback', '"http://127.0.0.1:8339/on/twitter/associate"'::jsonb),
|
||||
|
1
sql/branch.sql
Normal file
1
sql/branch.sql
Normal file
@ -0,0 +1 @@
|
||||
DELETE FROM app_conf WHERE key = 'trusted_proxies';
|
@ -1,63 +0,0 @@
|
||||
from ipaddress import IPv4Network
|
||||
|
||||
from liberapay.testing import Harness
|
||||
|
||||
|
||||
class Tests(Harness):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.website._trusted_proxies = getattr(cls.website, 'trusted_proxies', None)
|
||||
cls.website.trusted_proxies = [
|
||||
[IPv4Network('10.0.0.0/8')],
|
||||
[IPv4Network('141.101.64.0/18')],
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
cls.website.trusted_proxies = cls.website._trusted_proxies
|
||||
super().tearDownClass()
|
||||
|
||||
def request(self, forwarded_for, source, **kw):
|
||||
kw['HTTP_X_FORWARDED_FOR'] = forwarded_for
|
||||
kw['REMOTE_ADDR'] = source
|
||||
kw.setdefault('return_after', 'attach_environ_to_request')
|
||||
kw.setdefault('want', 'request')
|
||||
return self.client.GET('/', **kw)
|
||||
|
||||
def test_request_source_with_invalid_header_from_trusted_proxy(self):
|
||||
r = self.request(b'f\xc3\xa9e, \t bar', b'10.0.0.1')
|
||||
assert str(r.source) == '10.0.0.1'
|
||||
assert r.bypasses_proxy is True
|
||||
|
||||
def test_request_source_with_invalid_header_from_untrusted_proxy(self):
|
||||
r = self.request(b'f\xc3\xa9e, \tbar', b'8.8.8.8')
|
||||
assert str(r.source) == '8.8.8.8'
|
||||
assert r.bypasses_proxy is True
|
||||
|
||||
def test_request_source_with_valid_headers_from_trusted_proxies(self):
|
||||
r = self.request(b'8.8.8.8,141.101.69.139', b'10.0.0.1')
|
||||
assert str(r.source) == '8.8.8.8'
|
||||
assert r.bypasses_proxy is False
|
||||
r = self.request(b'8.8.8.8', b'10.0.0.2')
|
||||
assert str(r.source) == '8.8.8.8'
|
||||
assert r.bypasses_proxy is True
|
||||
|
||||
def test_request_source_with_valid_headers_from_untrusted_proxies(self):
|
||||
# 8.8.8.8 claims that the request came from 0.0.0.0, but we don't trust 8.8.8.8
|
||||
r = self.request(b'0.0.0.0, 8.8.8.8,141.101.69.140', b'10.0.0.1')
|
||||
assert str(r.source) == '8.8.8.8'
|
||||
assert r.bypasses_proxy is False
|
||||
r = self.request(b'0.0.0.0, 8.8.8.8', b'10.0.0.1')
|
||||
assert str(r.source) == '8.8.8.8'
|
||||
assert r.bypasses_proxy is True
|
||||
|
||||
def test_request_source_with_forged_headers_from_untrusted_client(self):
|
||||
# 8.8.8.8 claims that the request came from a trusted proxy, but we don't trust 8.8.8.8
|
||||
r = self.request(b'0.0.0.0,141.101.69.141, 8.8.8.8,141.101.69.142', b'10.0.0.1')
|
||||
assert str(r.source) == '8.8.8.8'
|
||||
assert r.bypasses_proxy is False
|
||||
r = self.request(b'0.0.0.0, 141.101.69.143, 8.8.8.8', b'10.0.0.1')
|
||||
assert str(r.source) == '8.8.8.8'
|
||||
assert r.bypasses_proxy is True
|
Loading…
x
Reference in New Issue
Block a user