Skip to content

Commit

Permalink
Initial import.
Browse files Browse the repository at this point in the history
  • Loading branch information
kuba committed Nov 15, 2015
0 parents commit 473121e
Show file tree
Hide file tree
Showing 18 changed files with 1,846 additions and 0 deletions.
19 changes: 19 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Standard Python stuff
*.pyc
/build/
/dist/
/venv/
/*.egg-info/

# testing
/.tox/

# make sure users don't commit private key material
*.json
*.pem
*.der
# ... as well as external IO plugin script
/external_pem.sh

# `sshfs user@host:$ROOT public_html/` and `--default_root=public_html`
/public_html/
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
1. Read MANIFESTO from README.

2. https://google.github.io/styleguide/pyguide.html and
https://www.python.org/dev/peps/pep-0008/
674 changes: 674 additions & 0 deletions LICENSE.txt

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
include README.md
include CONTRIBUTING.md
include LICENSE.txt
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# simp_le

Simple [Let's Encrypt](https://letsencrypt.org) client.

```shell
simp_le -f fullchain.pem -f key.pem \
-d example.com -d www.example.com --default_root /var/www/html \
-d other.com:/var/www/other_html
```

For more info see `simp_le --help`.

## Manifest

1. [UNIX philosophy](https://en.wikipedia.org/wiki/Unix_philosophy):
Do one thing and do it well!

2. `simp_le --valid_min ${seconds?} -f cert.pem` implies that
`cert.pem` is valid for at at least `valid_min`. Register new ACME
CA account if necessary. Issue new certificate if no previous
key/certificate/chain found. Renew only if necessary.

3. (Sophisticated) "manager" for
`${webroot?}/.well-known/acme-challenges` only. No challenges other
than `http-01`. Existing web-server must be be running already.

4. No magical webserver auto-configuration.

5. Owner of `${webroot?}/.well-known/acme-challenges` must be able to
run the script, without privilege escalation (`sudo`, `root`,
etc.).

6. `crontab` friendly: fully automatable - no prompts, etc.

7. No configuration files. CLI flags as the sole interface! Users
should write their own wrapper scripts or use shell aliases if
necessary.

8. Support multiple domains with multiple roots. Always create single
SAN certificate per `simp_le` run.

9. Flexible storage capabilities. Built-in `simp_le -f fullchain.pem
-f privkey.pem`, `simp_le -f chain.pem -f cert.pem -d privkey.pem`,
etc. Extensions through `simp_le -f external_pem.sh`.

10. Do not allow specifying output file paths. Users should symlink if
necessary!

11. No need to allow arbitrary command when renewal has happened: just
compare cert before and after (`sha256sum`, `mtime`, etc.).

12. `--server` (support multiple CAs).

## Installation

```shell
sudo ./bootstrap.sh
./venv.sh
. venv/bin/activate
```

## Examples

Have a look into `./examples/`.
42 changes: 42 additions & 0 deletions bootstrap.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/bin/sh -xe

bootstrap_deb () {
apt-get update

install () {
apt-get install -y --no-install-recommends "$@"
}

install \
ca-certificates \
gcc \
libssl-dev \
libffi-dev \
python \
python-dev \
python-virtualenv

# virtualenv binary can be found in different packages depending on
# distro version
install virtualenv || true
}

bootstrap_rpm () {
installer=$(command -v dnf || command -v yum)
"${installer?}" install -y \
ca-certificates \
gcc \
libffi-devel \
openssl-devel \
python \
python-devel \
python-virtualenv
}

if [ -f /etc/debian_version ]
then
bootstrap_deb
elif [ -f /etc/redhat-release ]
then
bootstrap_rpm
fi
27 changes: 27 additions & 0 deletions examples/external_pem.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/bin/sh
# Dummy example external script that loads/saves key/cert/chain to
# /tmp/foo; `simp_le -f external_pem.sh`.

load () {
cat /tmp/foo
}

save () {
cat - > /tmp/foo
}

persisted () {
echo key cert chain
}

case $1 in
save)
save
;;
load)
load
;;
persisted)
persisted
;;
esac
2 changes: 2 additions & 0 deletions pyi/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/build/
/dist/
5 changes: 5 additions & 0 deletions pyi/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
all:
pyinstaller simp_le.spec

clean:
rm -rf build dist
8 changes: 8 additions & 0 deletions pyi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[PyInstaller](http://www.pyinstaller.org/) setup for simp_le.

```shell
pip install -r requirements.txt
make clean all
./dist/simp_le --test
./dist/simp_le --help
```
22 changes: 22 additions & 0 deletions pyi/entrypoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import collections
import json
import pkg_resources

def dump_entry_points(tmp_entry_points_path, *distribution_names):
"""Dump entry points database.
Compile a database by going through all entry points registered by
distributions listed in `distribution_names`. Serialize database to
JSON and dump to a file (located in `tmp_entry_points_path`) that
can be later copied to the one-folder/one-file distribution and used
by `rthook-entrypoints.iter_entry_points`.
"""
entry_points = collections.defaultdict(collections.defaultdict)
for name in distribution_names:
entry_map = pkg_resources.get_distribution(name).get_entry_map()
for group, eps in entry_map.iteritems():
entry_points[group][name] = [str(ep) for ep in eps.itervalues()]
with open(tmp_entry_points_path, 'w') as fp:
fp.write(json.dumps(entry_points))
return entry_points
1 change: 1 addition & 0 deletions pyi/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pyinstaller>=3.0
55 changes: 55 additions & 0 deletions pyi/rthook-entrypoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""PyInstaller runtime hook for setuptools entry points.
Monkey patches `pkg_resources.iter_entry_points` to return all entry
points as saved by `entrypoints.dump_entry_points` in the PyInstaller
spec file.
"""
import functools
import json
import os
import pkg_resources
import sys


class Distribution(object):
"""Fake setuptools distribution."""
def __init__(self, key):
self.key = key

def requires(self, *unused_args, **unused_kwargs):
return []

def patch(mod):
"""Monkey patch module's attributes."""
def wrapper(f): # pylint: disable=missing-docstring
old = getattr(mod, f.__name__, f)
def wrapper2(*args, **kwargs): # pylint: disable=missing-docstring
return f(old, *args, **kwargs)
setattr(mod, f.__name__, wrapper2)
return wrapper2
return wrapper

def iter_entry_points_factory(all_entry_points):
"""Make patchable ``iter_entry_points`` that uses ``all_entry_points``."""
def iter_entry_points(old_iter_entry_points, group, *args, **kwargs):
"""Monkey patched ``iter_entry_points``."""
if group in all_entry_points:
for dist_name, entry_points in all_entry_points[group].iteritems():
dist = Distribution(dist_name)
return [pkg_resources.EntryPoint.parse(entry_point, dist=dist)
for entry_point in entry_points]
else:
return old_iter_entry_points(group, *args, **kwargs)
return iter_entry_points

def main():
"""Monkey-patch `pkg_resources` with correct database."""
entry_points_path = os.path.join(sys._MEIPASS, 'entry_points.json')
with open(entry_points_path) as fp:
all_entry_points = json.loads(fp.read())
patch(pkg_resources)(iter_entry_points_factory(all_entry_points))


if __name__ == '__main__':
main() # pragma: no cover
58 changes: 58 additions & 0 deletions pyi/simp_le.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# -*- mode: python -*-
import os
import pkg_resources
import sys

sys.path.insert(0, '.')
import entrypoints

TMP_ENTRY_POINTS_PATH = os.path.join(workpath, 'entry_points.json')
ENTRY_POINTS = [('entry_points.json', TMP_ENTRY_POINTS_PATH, 'DATA')]
entrypoints.dump_entry_points(
TMP_ENTRY_POINTS_PATH,
'cryptography',
)

_CRYPTOGRAPHY_BACKENDS = [
ep.module_name for ep in pkg_resources.iter_entry_points(
'cryptography.backends')
]
_HIDDEN_IMPORTS = _CRYPTOGRAPHY_BACKENDS + [
'cffi',
'werkzeug.exceptions',
]

block_cipher = None

MAIN = Analysis(
[os.path.join('..', 'simp_le.py')],
binaries=None,
datas=None,
hiddenimports=_HIDDEN_IMPORTS,
hookspath=[],
runtime_hooks=['rthook-entrypoints.py'],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
)

MAIN_PYZ = PYZ(
MAIN.pure,
MAIN.zipped_data,
cipher=block_cipher,
)

MAIN_EXE = EXE(
MAIN_PYZ,
MAIN.scripts,
MAIN.binaries,
MAIN.zipfiles,
MAIN.datas,
ENTRY_POINTS,
name='simp_le',
debug=False,
strip=False,
upx=True,
console=True,
)
43 changes: 43 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import sys
import setuptools


install_requires = [
'acme==0.0.0.dev20151114',
'cryptography',
'pyOpenSSL',
'requests',
]

if sys.version_info < (2, 7):
install_requires.extend([
'argparse',
'mock<1.1.0',
])
else:
install_requires.extend([
'mock',
])

tests_require = [
'pep8',
'pylint',
]

setuptools.setup(
name='simp_le',
author='Jakub Warmuz',
author_email='jakub@warmuz.org',
license='GPLv3',

py_modules=['simp_le'],
install_requires=install_requires,
extras_require={
'tests': tests_require,
},
entry_points={
'console_scripts': [
'simp_le = simp_le:main',
],
},
)
Loading

0 comments on commit 473121e

Please sign in to comment.