Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM python:3.8-slim

RUN pip install serles-acme

COPY . /app

WORKDIR /app

EXPOSE 8443

# CMD ["python", "-m", "serles"]
CMD ["gunicorn", "-c", "gunicorn.conf.py", "serles:create_app()"]
10 changes: 10 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: '3'
services:
app:
build: .
ports:
- "8443:8443"
environment:
- CONFIG=./config.ini
volumes:
- .:/app
17 changes: 17 additions & 0 deletions gunicorn.conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-

import multiprocessing
import os

from distutils.util import strtobool


bind = os.getenv('WEB_BIND', '0.0.0.0:8443')
accesslog = '-'
access_log_format = "%(h)s %(l)s %(u)s %(t)s '%(r)s' %(s)s %(b)s '%(f)s' '%(a)s' in %(D)sµs" # noqa: E501
timeout = 300

workers = int(os.getenv('WEB_CONCURRENCY', multiprocessing.cpu_count() * 2))
threads = int(os.getenv('PYTHON_MAX_THREADS', 1))

reload = bool(strtobool(os.getenv('WEB_RELOAD', 'false')))
13 changes: 9 additions & 4 deletions serles/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,21 @@ def create_app():
This function should be passed to the WSGI server.
"""
config, _ = get_config()

# print(config)
app = Flask(__name__)
app.config["PROPAGATE_EXCEPTIONS"] = True # makes @app.errorhandler handle events
# makes @app.errorhandler handle events
app.config["PROPAGATE_EXCEPTIONS"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = config["database"]
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SERVER_NAME"] = config["server_name"]
app.config['PREFERRED_URL_SCHEME'] = 'https'

init_config() # views.init_config()
api.init_app(app)
db.init_app(app)
db.create_all(app=app) # Note: model classes must be defined at this point
# db.create_all(app=app) # Note: model classes must be defined at this point
with app.app_context():
db.create_all()

@app.route('/')
def HomePage():
Expand All @@ -36,7 +40,8 @@ def HomePage():
app.after_request(inject_nonce)
app.after_request(index_header)

@background_job(60) # purge unused nonces every minute (keeps database small)
# purge unused nonces every minute (keeps database small)
@background_job(60)
def purge_nonces():
with app.app_context():
Nonces.purge_expired()
Expand Down
10 changes: 7 additions & 3 deletions serles/configloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def get_config():
Returns:
(dict, class): A tuple of ``config``, ``backend``.
"""
print("path ", os.environ.get("CONFIG", "/etc/serles/config.ini"))
config, backend = load_config_and_backend(
os.environ.get("CONFIG", "/etc/serles/config.ini")
)
Expand Down Expand Up @@ -82,7 +83,8 @@ class and the parsed config (dict-like)
ipaddress.ip_network(cidr) for cidr in ranges if cidr
]
except KeyError:
config["allowedServerIpRanges"] = None # if not defined, allow from everywhere.
# if not defined, allow from everywhere.
config["allowedServerIpRanges"] = None

try:
ranges = cparser["serles"]["excludeServerIpRanges"].splitlines()
Expand All @@ -107,8 +109,10 @@ class and the parsed config (dict-like)
) from None

try:
config["verifyPTR"] = cparser["serles"].getboolean("verifyPTR", fallback=False)
config["verifyPTR"] = cparser["serles"].getboolean(
"verifyPTR", fallback=False)
except ValueError:
raise ConfigError("[serles]verifyPTR= must be 'true' or 'false'") from None
raise ConfigError(
"[serles]verifyPTR= must be 'true' or 'false'") from None

return config, backend
37 changes: 24 additions & 13 deletions serles/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ def get(self):
"""
Displays the URLs for accessing certain functions, and some metadata.
"""

return {
"newNonce": api.url_for(NewNonce, _external=True),
"newAccount": api.url_for(NewAccount, _external=True),
"newOrder": api.url_for(NewOrder, _external=True),
"newNonce": api.url_for(NewNonce, _external=True, _scheme='https'),
"newAccount": api.url_for(NewAccount, _external=True, _scheme='https'),
"newOrder": api.url_for(NewOrder, _external=True, _scheme='https'),
# "newAuthz": MUST be absent if pre-authorization not supported
# "revokeCert": not offered
# optional: meta:{termsOfService"",website"",caaIdentities[""],externalAccountRequired?}
Expand Down Expand Up @@ -61,7 +62,8 @@ def post(self):
contact = g.payload.get("contact", [])
contact = contact[0] if len(contact) > 0 else None # only 1 email!
if contact and not contact.startswith("mailto:"):
raise ACMEError("only (one) email supported", 400, "unsupportedContact")
raise ACMEError("only (one) email supported",
400, "unsupportedContact")
if contact:
contact = contact.replace("mailto:", "")
termsOfServiceAgreed = g.payload.get("termsOfServiceAgreed", False)
Expand All @@ -84,7 +86,8 @@ def post(self):
else: # At this point, the user has no account, but wants one
account = Account(jwk=jwk_pem, contact=contact)
db.session.add(account)
db.session.commit() # note: accessing `account` after the commit requires setting expire_on_commit=False
# note: accessing `account` after the commit requires setting expire_on_commit=False
db.session.commit()
preexisting = False

return (
Expand All @@ -101,7 +104,8 @@ def post(self):
Submit a new Order. The request will include a list of Identifers
(domain names) the client wants on the certificate.
"""
notBefore = g.payload.get("notBefore") # optional, we ignore it for now
notBefore = g.payload.get(
"notBefore") # optional, we ignore it for now
notAfter = g.payload.get("notAfter") # optional, we ignore it for now
identifiers = g.payload.get("identifiers")
if not identifiers:
Expand Down Expand Up @@ -144,11 +148,13 @@ def post(self):
)

db.session.add(order)
db.session.commit() # note: accessing `order` after the commit requires setting expire_on_commit=False
# note: accessing `order` after the commit requires setting expire_on_commit=False
db.session.commit()
return (
order.serialized,
201,
{"Location": api.url_for(OrderMain, orderid=order.id, _external=True)},
{"Location": api.url_for(
OrderMain, orderid=order.id, _external=True)},
)


Expand All @@ -158,7 +164,8 @@ class NewAuthz(Resource):
pass


@api.resource("/revokeCert") # RFC8555 §7.6 (Certificate Revocation, not offered)
# RFC8555 §7.6 (Certificate Revocation, not offered)
@api.resource("/revokeCert")
class RevokeCert(Resource):
"not offered."
pass
Expand All @@ -177,7 +184,8 @@ def post(self, kid):
JSON-serialized Account object (post-update).
"""
if kid != g.kid:
raise ACMEError(f"{kid}, {g.kid}Unexpected Account ID", 403, "unauthorized")
raise ACMEError(
f"{kid}, {g.kid}Unexpected Account ID", 403, "unauthorized")
account = Account.query.filter_by(id=kid).first()
if not account:
raise ACMEError("", 400, "accountDoesNotExist")
Expand All @@ -188,11 +196,13 @@ def post(self, kid):
if contact is not None:
contact = contact[0] if len(contact) > 0 else None # only 1 email!
if contact and not contact.startswith("mailto:"):
raise ACMEError("only (one) email supported", 400, "unsupportedContact")
raise ACMEError("only (one) email supported",
400, "unsupportedContact")
if contact:
contact = contact.replace("mailto:", "")
account.contact = contact
db.session.commit() # note: accessing `account` after the commit requires setting expire_on_commit=False
# note: accessing `account` after the commit requires setting expire_on_commit=False
db.session.commit()

return account.serialized

Expand Down Expand Up @@ -324,5 +334,6 @@ def post(self, certid):
pem_cert = pkcs7_to_pem_chain(cert)

return make_response(
pem_cert, 200, {"Content-Type": "application/pem-certificate-chain"}
pem_cert, 200, {
"Content-Type": "application/pem-certificate-chain"}
)